一些零碎的知识的。
-
坐标系原点默认是屏幕左上角,向右为X轴正方向,向下为Y轴正方向。
-
View的getTop()、getLeft()、getBottom()、getRight()是相对父View来说的。
-
注意区分View的坐标系和Canvas的坐标系。View坐标系的原点是View的左上角;Canvas的坐标系默认是与View的重合,但是通过平移、旋转、缩放可以进行操作。
-
触摸事件MotionEvent的getX()、getY()是相对于View坐标系的;getRawX()、getRawY()是相对于屏幕坐标系的。
-
0°角与X轴正方向重合,角度沿着顺时针增大。
-
A R G B 的取值范围均为0~255(即16进制的0x00~0xff),A 从0x00到0xff表示从透明到不透明;RGB 从0x00到0xff表示颜色从浅到深。
-
bitmap大小计算公式,单位为Byte: bitmap.getWidth()*bitmap.getHeight()*(1/inSampleSize)^2*(目标设备分辨率/dpi文件夹分辨率)^2*色彩空间
-
merge标签必须是xml文件的根标签,merge标签与include标签一起用;通过LayoutInflater填充merge标签时必须指定父ViewGroup。
-
getMeasureWidth()方法在measure()过程结束后就可以获取到了,而getWidth()方法要在layout()过程结束后才能获取到。另外,getMeasureWidth()方法中的值是通过setMeasuredDimension()方法来进行设置的,而getWidth()方法中的值则是通过视图右边的坐标减去左边的坐标计算出来的。
-
onMeasure()用于使用父View传过来的widthMeasureSpec,heightMeasureSpec来确定自身能达到的最大尺寸(可以超,但是超过这个尺寸的部分将显示不出来)。至于View真正的尺寸还需要onLayout()的过程去确定,onLayout()中父View指定的矩形参数有可能使getWidth()比getMeasureWidth()大。最佳实践:遵守规范,通过数学计算控制参数把内容控制在屏幕之内。
-
View的绘制流程从ViewRootImpl的performTraversals()开始,performTraversals()里面会依次执行measure()、layout()、draw()的流程。
measure():先会获取根布局的MeasureSpec,如果是match_parent则为EXACTLY,如果是wrap_content则为AT_MOST,大小皆为窗口(window)的大小。也就意味着根视图总是会充满全屏的。View的onMeasure()接受父View传来的宽高参数,onMeasure的默认实现是调用getDefaultSize()来获取View的大小,如果MeasureSpec的mode为EXACTLY和AT_MOST则获取MeasureSpec中的尺寸信息。覆写onMesure()函数进行测量的时候记得调用setMeasuredDimension()。ViewGroup需要调用measureChildren()(最后会调用measureChild())去触发子View进行测量。
layout():接收四个参数,分别代表着左、上、右、下的坐标,当然这个坐标是相对于当前视图的父视图而言的。正如其名字所描述的一样,这个方法是用于给视图进行布局的,也就是确定视图的位置。ViewGroup中的onLayout()方法竟然是一个抽象方法,这就意味着所有ViewGroup的子类都必须重写这个方法。View中的onLayout()是空方法。
draw():measure和layout的过程都结束后,接下来就进入到draw的过程了。
-
调用顺序:onMeasure()-->onSizeChanged()-->onLayout()-->onDraw()。
-
视图重绘:invalidate()中先调用skipInvalidate()方法来判断当前View是否需要重绘,判断的逻辑也比较简单,如果View是不可见的且没有执行任何动画,就认为不需要重绘了。接着循环请求自己的父View去重绘,直到循环到最外层的根View后,调用ViewRoot的invalidateChildInParent()去重绘。最后会通过Handler发送一条DO_TRAVERSAL的消息给ViewRoot自身,再次调用performTraversals()进行绘制。
-
invalidate()方法虽然最终会调用到performTraversals()方法中,但这时measure和layout流程是不会重新执行的,因为视图没有强制重新测量的标志位,而且大小也没有发生过变化,所以这时只有draw流程可以得到执行。而如果你希望视图的绘制流程可以完完整整地重新走一遍,就不能使用invalidate()方法,而应该调用**requestLayout()**了。
-
自定义View有三种:自绘控件、组合控件、继承控件。
-
onDraw()自绘制图形的时候,通常会把Canvas坐标系移动到中央(或者是旋转等会使Canvas坐标系变化的操作),因为这样进行参数计算比较简单,但是这样会存在一个问题:如果想实现View内区域点击事件监控的话,存在坐标不一致的问题。因为MotionEvent的getX()、getY()是相对View的,如果取触摸点的getX()、getY()去跟圆的Region去判断的话,将会被错误的判断为触摸了圆,而事实上在Canvas坐标系中该触摸点为(-x,-y)。解决方法:对Canvas坐标系进行了变化的时候记录下它的逆矩阵,用逆矩阵对MotionEvent的**getRawX()、getRawY()**进行转化即可得到触摸点相对于该Canvas的坐标。原理:在绘制的时候Canvas会把自己的坐标转化为屏幕坐标进行绘制,所以想要还原回Canvas绘制状态时的坐标可以用它的逆矩阵进行逆向操作。
注意:如果Canvas坐标系与View坐标系重合则直接用MotionEvent的getX()、getY()即可。
- 事件分发流程:Activity->ViewGroup->View;对每个接收对象来说:dispatchTouchEvent()->onInterceptTouchEvent()->onTouchEvent()。
类型 | 函数 | Activity | ViewGroup | View | 返回值 |
---|---|---|---|---|---|
事件分发 | dispatchTouchEvent() | √ | √ | √ | true:不继续向下分发 |
事件拦截 | onInterceptTouchEvent() | x | √ | x | true:拦截事件 |
事件消费 | onTouchEvent() | √ | √ | √ | true:消费掉事件 |
-
ViewGroup事件分发伪代码:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean result = false; // 默认状态为没有消费过 if (!onInterceptTouchEvent(ev)) { // 如果没有拦截交给子View result = child.dispatchTouchEvent(ev); } if (!result) { // 如果事件没有被消费,询问自身onTouchEvent result = onTouchEvent(ev); } return result;}复制代码
可以看到只有onInterceptTouchEvent()的返回值没有赋给result,所以onInterceptTouchEvent()返回true只会中断事件dispatch,还是会继续从当前的View进行onTouch()反向回调。而dispatchTouchEvent()返回true中断掉所有流程;onTouchEvent()返回true则会中断掉回调过程,也即是表示事件被消费了。
-
View相关事件调用顺序:onTouchListener>onTouchEvent>onLongClickListener>onClickListener。
伪代码:
public boolean dispatchTouchEvent(MotionEvent event) { if (mOnTouchListener.onTouch(this, event)) { return true; } else if (onTouchEvent(event)) { return true; } return false;}复制代码
onTouchEvent()中处理onClickListener和onLongClickListener。
精简版源码:
public boolean onTouchEvent(MotionEvent event) { ... final int action = event.getAction(); // 检查各种 clickable if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: ... removeLongPressCallback(); // 移除长按 ... performClick(); // 检查单击 ... break; case MotionEvent.ACTION_DOWN: ... checkForLongClick(0); // 检测长按 ... break; ... } return true; // ◀︎表示事件被消费 } return false;}复制代码
上面可以看出:只要 View 可点击onTouchEvent()就返回 true,就表示事件被消费了。
举例,
现在你有了一个 RelativeLayout - View 你开开心心的为 RelativeLayout 设置了一个点击事件
myClick
,然而你会发现不论怎么点都不会接收到信息,仔细一看,发现内部的 View 有一个属性android:clickable="true"
正是这个看似不起眼的属性把事件给消费掉了,由此我们可以得出如下结论: 1. 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。 2. 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。 -
事件分发核心要点:
- 事件分发原理: 责任链模式,事件层层传递,直到被消费。
- View 的 dispatchTouchEvent 主要用于调度自身的监听器和 onTouchEvent。
- View的事件的调度顺序是 onTouchListener > onTouchEvent > onLongClickListener > onClickListener 。
- 不论 View 自身是否注册点击事件,只要 View 是可点击的就会消费事件。
- 事件是否被消费由返回值决定,true 表示消费,false 表示不消费,与是否使用了事件无关。
- ViewGroup 中可能有多个 ChildView 时,将事件分配给包含点击位置的 ChildView。
- ViewGroup 和 ChildView 同时注册了事件监听器(onClick等),由 ChildView 消费。
- 一次触摸流程中产生事件应被同一 View 消费,全部接收或者全部拒绝。
- 只要接受 ACTION_DOWN 就意味着接受所有的事件,拒绝 ACTION_DOWN 则不会收到后续内容。
- 如果当前正在处理的事件被上层 View 拦截,会收到一个 ACTION_CANCEL,后续事件不会再传递过来。