自定义View应该明白的基础知识

自定义View应该明白的基础知识

认识Android坐标系

Alt text

Android的坐标系是从左上角开始,向左为X正方向,向下为Y正方向,与普通的坐标系有些区别。

获取相对于父坐标的距离

getTop();       //获取子View左上角距父View顶部的距离
getLeft();      //获取子View左上角距父View左侧的距离
getBottom();    //获取子View右下角距父View顶部的距离
getRight();     //获取子View右下角距父View左侧的距离

Android3.0 之后还添加了 x,y,translationX,translationY 四个值,这四个值也是相对于父View的,默认 translationX和translationY 等于0,并且存在以下关系:

x = left + translationX
y = top + translationY

当View发生平移时,left 和 top 不会改变,改变的是 x,y,translationX,translationY 四个值

Alt text

MotionEvent中 event 获取的坐标

event.getX();       //触摸点相对于其所在组件坐标系的坐标
event.getY();
event.getRawX();    //触摸点相对于屏幕默认坐标系的坐标
event.getRawY();

自定义View绘制流程

Alt text

关于自定义View的构造函数

public void SloopView(Context context) {}
public void SloopView(Context context, AttributeSet attrs) {}
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr) {}
public void SloopView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {}

构造函数是以上四种,最常用的是带一个参数何两个参数的。调用的时机为:

SloopView view = new SloopView(this); // 调用一个参数的构造函数
  //调用两个参数的构造函数
  <com.sloop.study.SloopView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"/>

如果使用了自定义属性,则需要定义3个参数的构造函数,这里省略。

测量View的大小

@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
    int widthsize = MeasureSpec.getSize(widthMeasureSpec);      //取出宽度的确切数值
    int widthmode = MeasureSpec.getMode(widthMeasureSpec);      //取出宽度的测量模式
    int heightsize = MeasureSpec.getSize(heightMeasureSpec);    //取出高度的确切数值
    int heightmode = MeasureSpec.getMode(heightMeasureSpec);    //取出高度的测量模式
}

MeasureSpec 中的3种测量方式

模式二进制数值描述
UNSPECIFIED00默认值,父控件没有给子view任何限制,子View可以设置为任意大小。
EXACTLY01表示父控件已经确切的指定了子View的大小。
AT_MOST10表示子View具体大小没有尺寸限制,但是存在上限,上限一般为父View大小。

注意:
如果对View的宽高进行修改了,不要调用super.onMeasure(widthMeasureSpec,heightMeasureSpec);要调用setMeasuredDimension(widthsize,heightsize); 这个函数。

确定View的大小

 @Override
    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
        super.onSizeChanged(w, h, oldw, oldh);
    }

确定子View的位置

child.layout(l, t, r, b);
名称说明对应的函数
lView左侧距父View左侧的距离getLeft();
tView顶部距父View顶部的距离getTop();
rView右侧距父View左侧的距离getRight();
bView底部距父View顶部的距离getBottom();

绘制View( onDraw() )

真正绘制View的部分,当计算玩View的大小,并且通过onLayout确定了它的位置,就可以通过onDraw() 绘制出View。

View事件分发

这样一个View层级:

Alt text

结构如下:

Alt text

事件分发流程:

当有点击事件,首先是Activity捕获到,一直传递到View,如果这一个过程事件都没有被处理,则事件会被反向传播给Activity,如果还没有被处理,则抛弃。

Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View
Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

如果在View1 上发生touch事件,会依次调用下面的方法,红色是正向传播,绿色是回传:

Alt text

事件分发机制

当一个点击事件发生时,一般会经历下面3个重要的方法:

  1. dispatchTouchEvent(MotionEvent ev)
  2. onInterceptTouchEvent(MotionEvent ev)
  3. onTouchEvent(MotionEvent ev)

dispatchTouchEvent:用来进行事件分发,如果事件能传递给当前View,这个方法一定会被执行

onInterceptTouchEvent:在上述方法内部调用,用来判断是否拦截某个事件

onTouchEvent:在 dispatchTouchEvent 中调用,用于处理点击事件

三者的调用如下伪代码:
Alt text

View中的OnTouch事件

onTouchListener 的优先级比 onTouch 高,如果 listener中返回 true,表示已经处理,就不会将事件继续传递给 onTouch,这样做的好处是方便外界处理点击事件。

View的滑动冲突

这里先省略,详细请看《Android开发艺术探索》

View的滑动

一般有以下几种常用的方法:
1. scrollTo和scrollBy
2. 通过动画(平移动画,属性动画两种)
3. 改变View的LayoutParams使得View重新布局

ScrollTo和ScrollBy

  1. 如果要使View向右下方向滑动,那么 传入的 x,y的值应该为负数。
  2. 移动时,只有View的内容移动,但是View本身不移动

使用属性动画

mLauncher1.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    float x = mLauncher1.getX();
    float y = mLauncher1.getY();
    ObjectAnimator.ofFloat(mLauncher1, "translationX", x, x + 100).setDuration(100).start();
    ObjectAnimator.ofFloat(mLauncher1, "translationY", y, y + 100).setDuration(100).start();
  }
});

设置LayoutParams的值

需要注意的是,这种情况会引起View的重新绘制,效率要低一些

mlauncher2.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View v) {
    ViewGroup.MarginLayoutParams params = (ViewGroup.MarginLayoutParams) mlauncher2.getLayoutParams();
    params.leftMargin += 100;
    params.topMargin += 100;
    mlauncher2.requestLayout();
  }
});

View的工作原理

MeasureSpec 测量规则

MeasureSpec是一个int型的值, 由两个值 Mode(高2位) 和 Size(低30位)组成。

子View的 MeasureSpec由 父容器的MeasureSpec和子View的LayoutParams共同决定

SpecMode 由三类

UNSPECIFIED:父容器不对View由任何的限制,一般系统中用,不用关注。
EXACTLY:父容器已经测量出View所需的大小,View的大小由 SpecSize所指定,它对对应于LayoutParams中的match_parent
AT_MOST:父容器指定一个可用的大小 SpecSize给View,对应 wrap_content

3个流程 mesure, layout, draw

Alt text

Measure 过程

分为View的Measure过程和ViewGroup的Measure过程,ViewGroup的其实就是计算自己的,并且调用所有子View的Measure方法。

如何获取View的宽高:
1. 如果要获取某个View的宽高,在onCcreate,onResume,onStart这些方法中都是不能获取的。因为Activity和View的Measure不是同步执行的。可以在 onWindowFocusChanged 这个方法中获取,表示View的初始化已经完毕,会调用这个方法。但是它会被多吃调用,失去焦点和得到焦点都会被调用依次。

  1. view.post(runnable) 中获取
  2. ViewTreeObserver,onGlobalLayoutListener中可以,但是当View树状态改变时都会被调用。
  3. view.measure( measureSpecWidth, measureSpecHeight) 手动对View的measure中获得。根据LayoutParams获得,但是这种情况比较复杂,分为下面几种:
    • match_parent:基本无法获得
    • 具体值:width = MeasureSpec.makeMeasureSpec(100, MeasureSpec.EXACTLY)
    • wrap_content:width = MeasureSpec.makeMeasureSpec((1<<30)-1, MeasureSpec.AT_MOST)

Layout过程

测量自己应该在父容器中的位置

一般在onMeasure 中可以获取View的最终大小,但是极端情况也可能不正确。例如 onLayout中调用:super.layout(l,t,r+100,b+100),就会导致view的宽高都加100

Draw过程

一般分为下面4个步骤:
1. background.draw(canvas) 画背景
2. onDraw 画自己
3. dispatchDraw 画children
4. onDrawScrollBar 画装饰

参考

《Android开发艺术探索》
自定义View合集

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值