通俗的理解:“当用户在屏幕上完成一次点击、滑动等等触摸行为时,都会伴随着一次View事件的触发,所以这些行为可以被认为是View的事件”。
那么这些事件是怎么被区分、标记、传递的呢?这就不得不引入Android View体系中一个重要的概念 - MotionEvent
View 事件体系中几个重要的概念
MotionEvent
MotionEvent 是由ViewRootImpl包装生成的,用于在整个事件流程中传递信息。MotionEvent会将本次触摸事件所产生的信息包装起来,其中最重要的信息便是本次触摸事件的类型。主要的MotionEvent事件类型如下表所示。
表 2 - 1 常用触摸事件类型
| 类型 | 值 | 备注 |
| ACTION_DOWN | 0 | 当用户发生“按下”这一行为时触发。 |
| ACTION_MOVE | 2 | 当用户“按下”后产生有效滑动距离后触发。 |
| ACTION_UP | 1 | 当用户“抬起手指”时触发。 |
| ACTION_CANCEL | 3 | 当发生异常事件时触发,事件被中断。 |
注: 以上并非列举全部类型,仅为常用类型。
TouchSlop
TouchSlop 是View 中标识的最小有效滑动距离,如果实际的滑动距离小于该单位,则本次滑动是无效的。
TouchTarget
TouchTarget可以看作是一个映射关系,当某个事件被某个view消耗后,那么该事件序列中的后续所有事件都可以通过TouchTarget “直接” 到达接收事件的view,而无需重新通过遍历子view的方式进行分发,减少开销。
表 2 - 2 TouchTarget 主要属性
| 属性 | 备注 |
| child | viewgroup中被触摸的子veiw |
| pointerIdBits | 被target捕获的所有触点的id 组合掩码 |
| next | 指向下一个 target |
事件分发
不知道大家有没有好奇过,为什么用户在点击屏幕后,对应被点击的组件能做出反应呢?为什么不是包含该组件的外层组件响应呢?这个就要归功于Android中十分重要的一个机制--事件分发机制。

图 2 -1 view层级示例
那事件是如何分发的呢?按照图2 - 1 所示的结构来简单分析一下,假设被触发的事件是Down事件,当我们点击btn_A时,Down事件首先被触发,并且传递到view层(指DecorView的下一层:contentView)后,Group_A会第一个接到该事件,然后传递给Group_B,最后由Group_B传递给btn_A,也就是:

图 2 - 2 事件传递示例
那么整个事件分发机制具体是怎么运行的呢?前文有提到过MotionEvent是由ViewRootImpl包装生成的,所以笔者理解这里就是整个事件分发的源头,接下来我们看下详细一些的流程图。

图 2 - 3 事件整体分发运行流程
从图2-3可以看出,MotionEvent从ViewRootImpl传递到stage,再从stage传递到了顶级view (DecorView)中,在DecorView中会判断是否有Activity、Dialog等实现了window的回调(大多数情况下,会由activity实现该回调,所以示意流程图中写明了activity,其余情况暂不分析),如果有,则调用并将MotionEvent传递过去;若无,则直接调用父类(ViewGroup)的分发方法进行view层级的分发流程;但是从整体来看,最终都会回归到ViewGroup的分发方法。
“笔者认为:为什么会有从Activity -> window -> viewgroup 这条链路,主要是为了当所有view均不处理该事件时,可以由Activity进行兜底处理,避免事件流失,这一点可以从源码中看出。”
事件分发的整体流程的前半部分,不再进行详细分析了,后续主要分析 图 2-3 中的绿色部分,也就是viewGroup的dispatchTouchEvent方法。
ViewGroup的dispatchTouchEvent
ViewGroup中 dispatchTouchEvent方法的分发逻辑流程图

图 2 - 4 ViewGroup dispatchTouchEvent方法流程图
图 2 - 4 是一个比较复杂的流程图,但是图中的每一步都是必不可少的,为了便于理解,可以将整个流程分为四部分:拦截事件、 遍历子view传递MotionEvent、自己消耗事件和遍历TouchTargets传递MotionEvent。
为什么会有两个地方去传递MotionEvent?这是因为当Down事件通过遍历子View被一个具体的View消耗后,TouchTarget会记录当前ViewGroup中消耗事件的子View,当后续事件过来时,会通过TouchTarget一层一层的传递给该具体的View,从而减少遍历子View带来的开销,所以理论上两条传递的链路只会走一条。那么这个机制就决定了 当一个View接收并处理了某个事件,那么该事件序列。后续的所有事件在正常情况下均会由该View继续处理。
拦截事件
该流程中最重要的一个方法就是 onInterceptTouchEvent,但是要走到该方法需要满足一定的条件: (事件为Down事件 或者 TouchTarget不为空) 且 子View允许viewGroup拦截,从这里我们也可以知道,onInterceptTouchEvent方法不是每次都会走到的,所以如果需要添加必做的操作时,只能在dispatchTouchEvent方法中处理。并且onInterceptTouchEvent方法在大部分的ViewGroup中都是默认不拦截的。在开发过程中如果需要拦截,则需要重写ViewGroup的onInterceptTouchEvent方法。
if (actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 防止事件在拦截过程中被改变了,进行复原。
} else {
intercepted = false;
}
} else {
// 如果当前事件不是Down事件 且 当前无TouchTarget,则表示该view继续拦截该事件。
intercepted = true;
}
从上述代码中,我们可以看到,不管ViewGroup是否拦截,都不会立即对该事件进行处理,而是将结果赋值给intercepted变量,这个变量将会决定是否进入 “遍历子view传递”流程。
遍历子View传递
进入到该流程,除了上述的未被拦截条件外,还需要该事件非Cancel事件,并且事件类型需要是ACTION_DOWN、ACTION_POINTER_DOWN和ACTION_HOVER_MOVE中的一种。我们来看下这块的代码实现:
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
}
}
为什么会有第二个if判断? 这里需要结合整体来看才能明白,这里先提前讲一下,主要是因为,当Down事件被子View接收并处理后,后续的Move等事件到来时,不应该再走子View遍历传递这条链路,而是走TouchTarget传递,但是由于事件不是取消事件并且ViewGroup不拦截,那么还是会进入到遍历子View的流程中,所以这里做了一层拦截,使得后续事件能够通过TouchTarget进行传递。
接下来我们看下遍历子View传递这个过程的具体实现:
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
...
// 如果子View无法接收触摸事件 或者 点击的xy不在子View的范围内,则跳过该子View
if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
...
// 将事件传递给子View,如果子View处理了,dispatchTransformedTouchEvent方法会返回true。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
// 新创建target,并初始化
newTouchTarget = addTouchTarget(child, idBitsToAssign);
// 标记事件已经分发给子View并处理,后续会用到
alreadyDispatchedToNewTouchTarget = true;
break;
}
}
自身消耗
if (mFirstTouchTarget == null) {
// 需要注意,这里child传了null 表示ViewGroup自身处理,不需要传递到子View
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
...
}
如果在前面的拦截中ViewGroup拦截了该事件或者子View未处理,那么ViewGroup自身就有可能会处理该事件,为什么说有可能?这里需要分情况分析:
1、 ViewGroup拦截了Down事件:这个时候由于拦截后不再分发给子View,所以mFirstTouchTarget == null,这个时候会直接进入到自己处理的流程。
2、 ViewGroup拦截了Down事件的后续事件,并且Down事件被子View处理了,那么这时mFirstTouchTarget != null,所以这时会进入到else分支,ViewGroup不会处理该事件。但是ViewGroup会处理该事件序列中的后续的事件。
3、 ViewGroup未拦截任何事件,但是没有子View处理该事件,那么会走自己处理的流程。
遍历TouchTargets传递
前面说过,当Down事件被子View消耗后,会生成一个绑定子View和id的TouchTarget,这个时候后续的事件都会通过TouchTarget的引用传递到下一层。我们来看下具体实现:
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// Dispatch to touch targets, excluding the new touch target if we already
// dispatched to it. Cancel touch targets if necessary.
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果在上述的遍历子View传递过程中,事件已经被消耗,这里会直接返回handled = true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 查看本次事件是否是取消事件 (包含事件被ViewGroup拦截的情况)
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
// 分发给target中的View
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
// 如果是拦截造成的cancel,会将mFirstTouchTarget置为null,如果是单个child被取消,则从mFirstTouchTarget中移除对应节点
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
这里可以印证上节自身消耗中的第二点,ViewGroup虽然拦截了Down事件后续的某个事件,但是拦截的第一个事件ViewGroup是不会去处理的,而是将其转换为cancel事件传递给子View,然后将mFirstTouchTarget中的touchtarget清除,那么后续的事件序列,将会交由ViewGroup处理。
认真看了会发现,整个dispatchTouchEvent的出口方法都是dispatchTransformedTouchEvent,那么这个方法的作用是啥呢?它的最大的作用就是将事件传递到下一层(也就是调用子View的dispatchTouchEvent方法),其次就是通过传入的child参数,决定该事件是传递给子View还是自身消耗,以及在ViewGroup拦截事件时,将事件转换为cancel事件并传递给子View也是在该方法中进行的,具体就不对该方法进行分析了,感兴趣可以自行查看源码。
至此,ViewGroup中的dispatchTouchEvent方法基本都分析完成了,咱们继续进入到View的dispatchTouchEvent方法分析,了解View对事件的处理流程是怎么样的。
View的dispatchTouchEvent
View的dispatchTouchEvent方法不会再拦截了(本身View也不需要拦截了,已经是叶子节点了。),那么在dispatchTouchEvent中会做些什么呢?
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 首先如果View实现了onTouchListener接口,那么会触发并调用onTouch方法。
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
// 注意,如果这里处理成功了,会给结果赋值true。
result = true;
}
// 如果上面未处理,才会进入 onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
从上面可以看出,对一个View设置 onTouchListener 的执行优先级高于 onTouchEvent(onClickListener 和onLongClickListener),并且执行了onTouchListener就不会执行后者。
回到最开始,事件分发是如何从Group_A 到btn_A的呢?看图!


1433

被折叠的 条评论
为什么被折叠?



