Android 事件分发之源码分析系列
Android 之事件分发基础篇 [ 一 ]
Android 之 ViewGroup 事件分发深入源码分析 [ 二 ]
Android 之 View 事件分发深入源码分析 [ 三 ]
Android 之 View 事件分发深入源码分析 [ 总结 ]
友情提示:
本章内容主要是从源码上分析 ViewGroup.dispatchTouchEvent().
阅读本篇文章会引起强烈不适, 可能会带来头晕, 恶心, 干呕等一系列症状.
开篇先抛出几个问题:
- 为什么子
View在ACTION_DOWN中调用了requestDisallowInterceptTouchEvent(true);设置不允许父容器拦截会失效 ? - 如果子
View消费掉DOWN事件后, 为什么后续的事件都会直接传给它? 是怎么实现的 ? - 什么情况下会发送
ACTION_CANCEL事件 ? - ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗?
先整理这几个吧, 后续有需要会继续添加.
下面开始惊险刺激之旅
在上一篇有说, 事件分发是从 Activity 开始的. 也就是说当一个触摸事件发生后, 事件首先传到的是 Activity 的 dispatchTouchEvent() 方法, 现在进入到 Activity 中,
1. Activity.dispatchTouchEvent
//Activity.java 3396 行
public boolean dispatchTouchEvent(MotionEvent ev) {
//1
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
//2
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//3
return onTouchEvent(ev);
}
//Activity.java 3216 行
public void onUserInteraction() {
}
//Activity.java 3141 行
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
//window.java 1260行
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
final boolean isOutside =
event.getAction() == MotionEvent.ACTION_DOWN && isOutOfBounds(context, event)
|| event.getAction() == MotionEvent.ACTION_OUTSIDE;
if (mCloseOnTouchOutside && peekDecorView() != null && isOutside) {
return true;
}
return false;
}
分析 1
当Activity的dispatchTouchEvent方法接收到按下的事件后, 会先调用onUserInteraction方法. 这个方法一般为null, 如果开发者希望知道用户与设备的交互情况, 可以覆写这个方法. 但是当前Activity需要处于栈顶.分析 2
getWindow().superDispatchTouchEvent(ev)
首先getWindow()我们都知道了, 返回的是window的唯一实现类PhoneWindow. 如果PhoneWindow.superDispatchTouchEvent()方法返回了true, 这里就直接返回true, 这里先不跟进去, 接着看第三点. 返回false的情况下. 调用onTouchEvent()分析 3
在第三点处又调用了window.shouldCloseOnTouch()方法, 这方法主要是判断是不是按下事件, 是不是在边界之外.
shouldCloseOnTouch()返回值为True表示事件在边界外. 就消费事件.然后调用Activity的finish()方法.
返回值为False表示 不消费,小结
- 当一个点击事件发生时, 会从
Activity的事件分发开始. 即调用Activity.dispatchTouchEvent()开始事件分发.- 如果是按下事件
(DOWN), 就调用onUserInteraction方法, 这方法一般为空.- 接着就调用了
PhoneWindow的dispatchTouchEvent()事件分发方法将事件分发到Activity内部的ViewGroup/View, 看它们处理不处理这个事件, 如果不处理, 则会返回false, 然后Activity接收到返回值后会调用自身的onTouchEvent()方法自己处理.- 在
Activity.onTouchEvent()中会判断是不是应该关闭Activity,
2. PhoneWindow.superDispatchTouchEvent
PhoneWindow.java 1829行
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
内部直接调用了
mDecor的superDispatchTouchEvent(event)方法.(mDecor 就是 DecorView).进入到DecorView中看superDispatchTouchEvent.
3. DecorView.superDispatchTouchEvent
DecorView.java 439 行
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
看到这里又调用了
super.dispatchTouchEvent(event).DecorView的继承自Framelayout,但是FrameLayout中, 并没有这个方法.FrameLayout又继承自ViewGroup, 所以这里调用的直接是ViewGroup的dispatchTouchEvent方法.直接跟进去.
4. ViewGroup.dispatchTouchEvent
先大致了解下 dispatchTouchEvent 大概都做了什么
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//要不要分发本次触摸事件
if (onFilterTouchEventForSecurity(ev)) {
...
//是否取消事件或者拦截事件
if (!canceled && !intercepted) {
...
//只处理 DOWN 事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
//循环子 View 分发 Down 事件
for (int i = childrenCount - 1; i >= 0; i--) {
...
}
...
}
//没有找到新的可以消费事件的子View,那就找最近消费事件的子View来接受事件
if (newTouchTarget == null && mFirstTouchTarget != null) {
...
}
}
}
if (mFirstTouchTarget == null) {
// 父 View 自己处理事件
} else {
//判断是否已经分发过 DOWN.
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
}else {
//判断是否需要分发 CANCEL 事件
}
}
}
return handler;
}
可以看到其实在 ViewGrou.dispatchTouchEvent 中基本步骤就是这些. 无非就是细节比较多罢了.
- 一个判断需要不需要分发本次触摸事件包含了全部逻辑.
- 在内部接着判断当前事件没有被取消并且也没有被拦截.
- 如果都没有, 则进入
DOWN的事件分发. (没有else逻辑)
- 如果都没有, 则进入
3.最后判断是否有子 View 消费了事件, 没有的话父 View 自己处理事件, 有的话则发送 DOWN 的后续事件列. 包括 CANCEL 事件.
从上面的代码片段中可以看出, 只有在是 DOWN 事件的时候, 才会进入到 for 循环中去遍历当前 ViewGroup 下的所有子 View. 找到能处理 DOWN 事件的 View 并且添加到 mFirstTouchTarget 链表的头部.
然后会在最后判断有没有子 View 处理过事件(mFirstTouchTarget == null) .没有处理的话 ViewGroup 自己处理. 如果有处理, 就根据条件直接分发 DOWN 事件后的剩余事件包括 CANCEL 事件,
4.1 深度分析开始
由于这个方法太长, 我会采用分段分析.
ViewGroup.java 2542 行
// 1
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 2
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// 3
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
// 4
boolean handled = false;
分析 1
mFirstTouchTarget用来记录当前触摸目标链表的起始对象.
这里要说一下能够接收触摸事件流的子View是怎么被记录的. 其实就是使用一个TouchTarget来记录. 它是一个单链表结构, 并且有复用机制.TouchTarget中与我们关联最大的两个成员就是[ public View child ] :用来保存能够处理触摸事件的View. 另外一个是[ public TouchTarget next ] :这个是指向下一个TouchTarget对象.分析 2
mInputEventConsistencyVerifier
mInputEventConsistencyVerifier是InputEventConsistencyVerifier类型. 声明在 View 中, 官方翻译大概为: 输入一致性校验. 在InputEventConsistencyVerifier中isInstrumentationEnabled方法为True的时候.会在View中初始化mInputEventConsistencyVerifier对象. 在 View.java 4739 行查看初始化分析 3
通过触摸事件判断是否应该被有焦点的View处理事件, 如果同时存在拥有焦点的View, 则设置为False分析 4
boolean handled = false;
该变量表示是否处理了该事件, 这个变量也是最后的返回值.
下面继续
// 5
if (onFilterTouchEventForSecurity(ev)) {
// 6
final int action = ev.getAction();
// 7
final int actionMasked = action & MotionEvent.ACTION_MASK;
注: 这个 if 判断贯穿了整个 dispatchTouchEvent 方法
分析 5
主要检查要不要分发本次触摸事件, 检测通过才会执行该if中的逻辑, 否则就放弃对这次事件的处理. 在这个方法内部会先检查View有没有设置被遮挡时不处理触摸事件的flag, 再检查收到事件的窗口是否被其他窗口遮挡. 都检查通过返回True, 检查不通过则直接返回handled. (上面在分析 4 中handled, 默认为False)分析 6
获得事件, 包括触摸事件的类型和触摸事件的索引.
Android 会在一条 16 位指令的高 8 位中存储触摸事件的索引, 低 8 位是触摸事件的类型.分析 7
保存 上面分析 6 中action变量中的低 8 位, 其余为 0, 作为actionMasked, 也就是获取事件类型.
接着开始一系列的初始化及重置工作.
// 8
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 9
cancelAndClearTouchTargets(ev);
// 10
resetTouchState();
}
分析 8
只有在DOWN事件的时候才会进入if内逻辑-
分析 9
因为每次事件流的开始, 都是从DOWN事件开始的, 所以需要清除上一次接收触摸事件View的状态. 主要就是清除上一次触摸事件事件流中能够接收事件的所有子View的PFLAG_CANCEL_NEXT_UP_EVENT标志, 并且模拟了一个ACTION_CANCEL事件分发给他们, 可以重置这些子View的触摸状态, 例如取消它们的长按或者点击事件. 下面看下cancelAndClearTouchTargets()代码private void cancelAndClearTouchTargets(MotionEvent event) { // 如果触摸事件目标队列不为空才执行后面的逻辑 if (mFirstTouchTarget != null) { boolean syntheticEvent = false; if (event == null) { final long now = SystemClock.uptimeMillis(); // 自己创建一个ACTION_CANCEL事件 event = MotionEvent.obtain(now, now,MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0); // 设置事件源类型为触摸屏幕 event.setSource(InputDevice.SOURCE_TOUCHSCREEN); // 标记一下,这是一个合成事件 syntheticEvent = true; } // TouchTarget是一个链表结构,保存了事件传递的子一系列目标View for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { // 检查View是否设置了暂时不再接收事件的标志位,如果有清除该标志位 // 这样该View就能够接收下一次事件了。 resetCancelNextUpFlag(target.child); // 将这个取消事件传给子View dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits); } // 清空触摸事件目标队列 clearTouchTargets(); if (syntheticEvent) { // 如果是合成事件,需要回收它 event.recycle(); } } } -
分析 10
resetTouchState()清除ViewGroup触摸的相关状态. 在方法内, 会再调用一次clearTouchTargets()方法清除触摸事件队列. 然后再次清除View中不接收TouchEvent的标志位. 最后最重要的来了, 设置为允许拦截事件. 下面看resetTouchState()方法private void resetTouchState() { // 再清除一次事件传递链中的View clearTouchTargets(); // 再次清除View中不接收TouchEvent的标志 resetCancelNextUpFlag(this); // 设置为允许拦截事件 // FLAG_DISALLOW_INTERCEPT 通过子 VIew 调用 requestDisallowInterceptTouchEvent 设置 mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; mNestedScrollAxes = SCROLL_AXIS_NONE; }这里也验证了上面第一个问题:
问: 为什么子View在ACTION_DOWN中调用了requestDisallowInterceptTouchEvent(true);设置不允许父容器拦截会失效 ?
答: 因为ViewGroup中的dispatchTouchEvent方法在分发事件时, 如果是DOWN事件的时候, 就会重置FLAG_DISALLOW_INTERCEPT这个标记位, 将导致子View中设置的这个标记位无效. 因此子View在调用requestDisallowInterceptTouchEvent(true);方法并不能影响ViewGroup对DOWN事件的处理.
补充:requestDisallowInterceptTouchEvent(true);一旦设置后,ViewGroup将无法拦截除了DOWN事件以外的其他事件.
接着向下看. 下面这段代码主要是判断 VIewGroup 是否拦截当前事件.
// 11
final boolean intercepted;
// 12
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 13
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
// 14
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 15
final boolean canceled = resetCancelNextUpFlag(this) || actionMasked == MotionEvent.ACTION_CANCEL;
// 16
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
// 17
TouchTarget newTouchTarget = null;
// 18
boolean alreadyDispatchedToNewTouchTarget = false;
分析 11
intercepted这个变量用于检查是否拦截当前事件.分析 12
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)
这个判断限制了必须是DOWN事件, 或者mFirstTouchTarget != null才会进入内部逻辑去判断要不要拦截事件.否则直接认定为拦截.intercepted = true
如果当前事件是DOWN事件, 那么第一个条件成立 (其实这个时候mFirstTouchTarget是等于null的, 因为在分析 10 中清除了触摸链中的目标)
如果当前事件不是DOWN, 那么肯定是DOWN的后续事件, 那么第一个条件不成立, 看第二个条件mFirstTouchTarget != null, 之前说过mFirstTouchTarget是用来记录当前触摸目标链表的起始对象. 只有子View处理了事件时,mFirstTouchTarget才会被赋值 (后面会有分析到)-
分析 13
其中FLAG_DISALLOW_INTERCEPT是一个常量的标记位, 意思是对于父ViewGroup向内部的子View传递事件不允许拦截的标记位.
默认的mGroupFlags对应的位置只 0, 在View初始化代码里mGroupFlags并没有被初始化相对应位置的值.
FLAG_DISALLOW_INTERCEPT的值是一个十六进制 0x80000 转换成二进制就是0000 0000 0000 1000 0000 0000 0000 0000, 共 32 位, 而mGroupFlags & FLAG_DISALLOW_INTERCEPT位运算之后, 0 的位置全部变为了 0 , 对于 1 的那个位置,mGroupFlags的对应位置是 1, 才是 1. 否则是 0.
也就是说, 在正常没有外力影响的情况下,boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;结果是False. 因为mGroupFlags & FLAG_DISALLOW_INTERCEPT的结果一定是 0.
那么什么是外力影响呢, 就是上面说的子View通过调用getParent.requestDisallowInterceptTouchEvent(true)来改变mGroupFlags对应位置的值. 该方法在 ViewGroup.3136 行.若这个方法的参数是
True的话( 传入True表示子View不允许ViewGroup拦截 ), 在这个方法内会执行mGroupFlags |= FLAG_DISALLOW_INTERCEPT;这个时候mGroupFlags对应位置的值就变为了 1. 那么在这里再进行位运算disallowIntercept就会为True, 然后再进行取反进行判断为False,if不成立, 直接进else,intercepted被赋值为False.若这个方法参数是
False(子View允许拦截), 则会在requestDisallowInterceptTouchEvent方法内执行mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;这个时候mGroupFlags对应的位置就变成了0. 那么这里的disallowIntercept结果就是False, 意味着允许拦截 . 再取反为True, 执行if内逻辑.if中会先调用onInterceptTouchEvent拦截方法并把返回值给intercepted, 这是什么意思呢, 就是说虽然说你子View允许我拦截了, 但是我需要确定一下自己是否需要拦截( 调用onInterceptTouchEvent).关于 分析 13 处 简单理解就是, 你让不让我拦截, 不让我拦截
intercepted = false, 让我拦截的话, 我还要看看我自己是否真的需要拦截, 拦截intercepted = true, 不拦截那么intercepted还是为false关于上面的问题 4 : ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗?
问题 4 最起码有一点可以确认了, 那就是ACTION_DOWN被子View消 费了, 那么ViewGroup还是能拦截剩下后续的事件的. (前提是子View没有 调用requestDisallowInterceptTouchEvent方法传入True)
为什么呢? 看分析 12,DOWN被子View消费了, 那么mFirstTouchTarget肯定是会被赋值的. 这样还是会进入到if中去. 执行分析 13 处的逻辑. 剩下的自己分析分析. 分析 14
如果ViewGroup拦截了事件, 或者事件已有目标组件进行处理, 那么就去除辅助功能标记, 进行普通的事件分发.
分析 15
canceled标识本次事件是否需要取消.分析 16
split检查父ViewGroup是否支持多点触控, 即将多个TouchEvent分发给子View. 在 Android 3.0 以后默认为True.分析 17
newTouchTarget = null声明后续会使用到的变量
当事件已经做出分发时, 记录分发对应的 View 控件
- 分析 18
alreadyDispatchedToNewTouchTarget = false
记录事件是否已经做出分发, 后面用于过滤已经分发的事件, 避免事件重复分发.
继续向下看.
// 19
if (!canceled && !intercepted) {
// 20
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null;
// 21
if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 22
final int actionIndex = ev.getActionIndex();
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS;
// 23
removePointersFromTouchTargets(idBitsToAssign);
// 24
final int childrenCount = mChildrenCount;
// 25
if (newTouchTarget == null && childrenCount != 0) {
// 26
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// 27
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
分析 19
当前if判断没有else分支. 对于被拦截和取消的事件,不会执行if中的所有方法.分析 20
ev.isTargetAccessibilityFocus(): 检查TouchEvent是否可以触发View获取焦点.
可以则查找当前ViewGroup中有没有获得焦点的子View, 有就获取, 没有就为 null.分析 21
这个if也没有else分支. 对事件的类型进行判断, 主要处理DOWN事件.
[ 当前的触摸事件类型是不是DOWN], [ 支持多点触控且是ACTION_POINTER_DOWN], [ 需要鼠标等外设支持 ]
也就是说一个事件流只有一开始的DOWN事件才会去遍历分发事件, 后面的事件将不会再通过遍历分发, 而是直接分发到触摸目标队列的VIew中去分析 22
获得事件的actionIndex与 位分配 ID .
位分配 ID, 通过触控点的PointerId计算. 其逻辑为:
1 << ev.getPointerId(actionIndex), 即对 0000 0001 左移, 移动的位数为PointerId的值. 一般情况下PointerId从 0 开始, 每次 + 1. 即把PointerId记录通过位进行保存, 0对应 0000 0001, 2对应 0000 00100, 5对应, 0010 0000.-
分析 23
清除之前触摸事件中的目标.
方法为://检查是否有记录的 PointId, 并清除 private void removePointersFromTouchTargets(int pointerIdBits) { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; // mFirstTouchTarget 不为 null while (target != null) { //获取对应的 TouchTarget 链表的下一个对象 final TouchTarget next = target.next; //判断是否存在记录了 mFirstTouchTarget 中触控点的 TouchTarget if ((target.pointerIdBits & pointerIdBits) != 0) { target.pointerIdBits &= ~pointerIdBits; if (target.pointerIdBits == 0) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } //如果存在就移除 target.recycle(); //并指向链表中的下一个 TouchTarget 对象. target = next; continue; } } predecessor = target; target = next; } } 分析 24
获取子View的数量.分析 25
newTouchTarget == null && childrenCount != 0: 如果有子View并且在上面分析 17 的地方声明的newTouchTarget = null才会进入if中. 当前if也没有else逻辑. 第一次DOWN事件发生的时候,newTouchTarget肯定为null, 如果条件不成立则代码会直接跳转到下面的分析 36 处执行.分析 26
final float x = ev.getX(actionIndex);: 获得触摸事件的坐标.-
分析 27
ArrayList<View> preorderedList = buildTouchDispatchChildList();: 调用buildTouchDispatchChildList()方法创建待遍历的 View 列表.
boolean customOrder = preorderedList == null && isChildrenDrawingOrderEnabled();: 是否采用自定义View顺序. 这个顺序将决定哪个View会先接收到事件.
初始化了preorderedList和mChildren两个子View集合, 为什么需要两个呢?.通过
buildTouchDispatchChildList()方法构建待遍历的View集合会有如下特点- 如果
ViewGroup的子View数量不大于 1, 为 null. - 如果
ViewGroup的所有子View的 z 轴都为 0 , 为 null. - 子
View的排序和mChildren一样是按照View添加顺序从前往后排的, 但是还是会受到子Viewz 轴的影响. z 轴大的会往后排.
所以这两个集合之间的最大区别就是,
preorderedList中 z 轴大的子View会往后排. 而mChildren不会. - 如果
现在来到了事件分发中最关键最核心的地方. 朋友们, 你们还好吗? 都还健在吗
// 分析 28
for (int i = childrenCount - 1; i >= 0; i--) {
// 分析 29
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// 分析 30
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 分析 31
if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 分析 32
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 分析 33
resetCancelNextUpFlag(child);
// 分析 34
if (dispatchTransformedTouchEvent(ev, false, child,idBitsToAssign)) {
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 分析 35
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
// 分析 36
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
为了防止有点蒙圈的朋友, 这里再发一下, 整个方法的大致流程
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//要不要分发本次触摸事件
if (onFilterTouchEventForSecurity(ev)) {
...
//是否取消事件或者拦截事件
if (!canceled && !intercepted) {
...
//只处理 DOWN 事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
//===========================================现在要分析的地方是这里开始 ===========================================
//循环子 View 分发 Down 事件
for (int i = childrenCount - 1; i >= 0; i--) {
...
}
...
}
//没有找到新的可以消费事件的子View,那就找最近消费事件的子View来接受事件
if (newTouchTarget == null && mFirstTouchTarget != null) {
...
}
}
}
//==================================================== 结束线 ====================================================
if (mFirstTouchTarget == null) {
// 父 View 自己处理事件
} else {
//判断是否已经分发过 DOWN.
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
}else {
//判断是否需要分发 CANCEL 事件
}
}
}
return handler;
}
分析 28
以"从尾到头"的方式遍历View列表, (就是从后往前遍历.)
这就是为什么覆盖在上层的View总是能够优先获取到事件的原因.分析 29
根据childIndex获取子View对象.
如果preorderedList不为空, 则从preorderedList获取子View.
如果为空, 则从mChildren中获取子View分析 30
childWithAccessibilityFocus在上面 分析 20 处声明并赋值.
如果这个具有焦点的子View不为null
如果不为null那么接着判断这个具有焦点的子View与从
分析 29 处取出的子View对象是否是同一个. 如果不是同一个View那么直接跳到 分析 28 处, 进行下一次遍历. 如果是同一个, 则把childWithAccessibilityFocus赋值为null并且把循环的i指向childrenCount - 1的位置. 意思大概是下次遍历的时候跳过这个子View关于这点还未细看, 网上看到有的帖子说是 如果当前子View具有可访问的焦点时,会让该子View优先获得这次事件. 有知道的朋友可以帮忙斧正.-
分析 31
canViewReceivePointerEvents: 判断子View是否正常显示(VISIBLE)或者子View是否在播放动画.
isTransformedTouchPointInView: 检测触摸事件是否在该子View的范围内.- 如果在子
View原有的位置上没有看到子View,同时子View也不是因为动画而离开原来的位置, 那么肯定是隐藏了, 因此不符合事件消费的条件, 所以执行continue跳过. - 如果用户触摸的位置不在子
View的范围内,肯定也不符合事件消费的条件,同样执行continue跳过
那么为什么要这样判断呢?其目的在于确保了子
View即使是因为动画(例如位移动画)的原因离开了原来的位置, 子View也可以正常分发触摸了原范围内的事件, 这也正是子View执行位移动画后点击位置为什么没有跟随子View来到新位置的原因 - 如果在子
分析 32
在getTouchTarget方法中, 其逻辑就是如果mFirstTouchTarget表示的链表中的某一个节点就是当前的child, 则返回它赋值给newTouchTarget, 若找不到则返回null.
下面的判断主要用于多点触控的情况, 例如手指触摸某个子View触发了ACTION_DOWN, 这时另一根手指也放在这个视图上触发了ACTION_POINTER_DOWN, 此时就需要通过在链表中查找当前子View的结果来判断两根手指触摸的是否为同一个View,newTouchTarget != null表示触摸了同一个子View那么就将触摸点Id复制给新的newTouchTarget对象,并执行break跳出遍历, (因为这个View之前已经收到了DOWN事件.)分析 33
resetCancelNextUpFlag(child);重置子View的PFLAG_CANCEL_NEXT_UP_EVENT标志位.分析 34
在这里才会真正的将事件分发给子View去处理.
dispatchTransformedTouchEvent()方法中调用了child.dispatchTouchEvent. 方法返回True表示子View消费了, 继续执行if中的逻辑, 并结束遍历, 也就是说剩下的View都不会接收到这个事件了. 返回False, 则继续遍历寻找下一个满足条件的子 View.dispatchTouchEvent方法后面会有说到. 注意这里传入的cancel是false,child是当前的子View.先记住.-
分析 35
-
newTouchTarget = addTouchTarget(child, idBitsToAssign);将消费事件的View添加到mFirstTouchTarget触摸链中, 并赋值给newTouchTarget
addTouchTarget()方法内部逻辑. 根据传入的View生成一个新的TouchTarget, 并将新生成TouchTarget的next指向mFirstTouchTarget, 再将新生成TouchTarget赋值给mFirstTouchTarget, 最后返回这个TouchTarget
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }-
alreadyDispatchedToNewTouchTarget = true;
设置标志位,证明当前接收到的动作事件已经分发过了,这个标志后续的判断中会用到, 这个标记位 只会在这里设置为 True`.
-
分析 36
if (newTouchTarget == null && mFirstTouchTarget != null)
这里有可能是从分析 25 处跳转来的, 也有可能是执行完DOWN事件的分发来的.newTouchTarget在不是DOWN事件或者没有找到处理事件的View时为null, 但是这个判断是在DOWN事件逻辑内, 那这里意思就是没有找到处理事件的View.
mFirstTouchTarget在DOWN事件时, 如果找到了处理事件的View就不为null
分析 36 处这里的意思就是如果上面没有找到可以处理事件的子View, 那么就找最近处理过事件的子View来接收事件.并且给newTouchTarget赋值.
到这里, DOWN 事件已经算是分发完成了. 现在接着看后续的其他事件.
剩下的就是
public boolean dispatchTouchEvent(MotionEvent ev) {
...
//要不要分发本次触摸事件
if (onFilterTouchEventForSecurity(ev)) {
...
//是否取消事件或者拦截事件
if (!canceled && !intercepted) {
...
//只处理 DOWN 事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
//循环子 View 分发 Down 事件
for (int i = childrenCount - 1; i >= 0; i--) {
...
}
...
}
//没有找到新的可以消费事件的子View,那就找最近消费事件的子View来接受事件
if (newTouchTarget == null && mFirstTouchTarget != null) {
...
}
}
}
//===========================================现在要分析的地方是这里开始 ===========================================
if (mFirstTouchTarget == null) {
// 父 View 自己处理事件
} else {
//判断是否已经分发过 DOWN.
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget){
}else {
//判断是否需要分发 CANCEL 事件
}
}
//==================================================== 结束线 ====================================================
}
return handler;
}
剩下的就剩这一块了. 代码不是很长, 就一次性贴出来了.
// 分析 37
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
// 分析 38
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 分析 39
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 分析 40
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
//分析 41
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
分析 37
if (mFirstTouchTarget == null)都走到这一步了,mFirstTouchTarget还为空是什么意思? 说明没有到现在还没找到处理事件的View呀, 那怎么办呢. 看到if内又调用了dispatchTransformedTouchEvent(), 在 分析 34 的地方处理DOWN事件的时候调用了一次. 这里和上面传入的参数不同了, 第三个参数child传入的是null, 表示没有View处理事件. 需要ViewGroup自己处理事件.分析 38
遍历mFirstTouchTarget进行分发事件.分析 39
alreadyDispatchedToNewTouchTarget: 是在分析 35 处被赋值的. 有子View处理了DOWN事件, 该变量才会为True.
这个判断意思是: 只有DOWN时, 并且有子View处理了事件才会走if中的逻辑, 目的是为了避免重复分发事件, 因为在上面分析 34 的时候已经分发并处理过了. 所以这里直接把最终返回值handled = true.-
分析 40
走到这里, 就是真正开始分发除DOWN事件外的事件了. 包括CANCEL事件. 因为走到这一步, 首先确定了mFirstTouchTarget不为空, 代表有子View处理了事件, 接着又进到else, 表示当前事件不是DOWN事件.
cancelChild表示处理DOWN类型事件的目标控件的后续事件分发被拦截的情况. 父View拦截,或者 子View原本不可接收TouchEvent的状态,cancelChild为True. 如果子View处理了剩下的事件,handled = true.
接着又调用了dispatchTransformedTouchEvent方法, 第二个参数这里传入了cancelChild, 第三个参数传入了之前处理DOWN事件的子View.这里回答了问题 3, 什么情况下会发送 ACTION_CANCEL 事件 ?
这里的cancelChild为true, 给它发送CANCEL事件 (CANCEL是在这里进行发送, 并且子View接收过一次前驱事件.)
cancelChild为false, 就分发除DOWN外的剩余事件
- 分析 41
如果cancelChild为True, 表示ViewGroup拦截了事件, 然后需要清空事件队列. 这样就会使后续的触摸事件直接被ViewGroup默认拦截.
这里就很好理解了, 清空了事件队列后, 在下次事件来的时候, 执行到上面的分析 12, 那么就会直接进入到else, 给intercepted赋值为true. 然后在分析 19 处也不会进入if逻辑, 直接就到分析 37 处ViewGroup直接就自己处理了.
那么问题 4 的后半段也能解释了,
ACTION_DOWN 事件被子 View 消费了,那 ViewGroup 能拦截剩下的事件吗?首先这点是已经确定可以拦截的.参考上面的分析 12, 分析 13 处.
如果拦截了剩下事件,当前这个事件 ViewGroup 能消费吗?子 View 还会收到事件吗? 答案是:ViewGroup可以消费, 子View会收到一次CANCEL取消事件,然后不会再收到别的事件了. 为什么这样说咧.
- 首先第一次子
View处理了DOWN事件, 这个时候,ViewGroup没有拦截DOWN事件, 那么在分析 41 处, 就不会清空事件队列, 这时候mFirstTouchTarget有值,目标View就是处理DOWN事件的那个.- 接着
MOVE事件来了, 在走到分析 12 处的时候mFirstTouchTarget != null条件成立, 在分析 13 处, 如果子View允许ViewGroup拦截, 那么就会调用分析 13 中的onInterceptTouchEvent(), 这时ViewGroup拦截MOVE事件.intercepted = true. 分析 19 处不成立, 直接进入分析 37 处. 这时候mFirstTouchTarget还不等于null接着进入到else, 在分析 39 处alreadyDispatchedToNewTouchTarget肯定为false, 因为当前事件不是DOWN事件. 那么就走到了分析 40 处.cancelChild = true, 接着调用dispatchTransformedTouchEvent()方法, 传入的cancelChild = true在这个方法内就会发送CANCEL事件给子View. 接着就会执行分析 41 处逻辑, 清空事件队列.- 再下个
MOVE来的时候, 走到分析 12 处, 不成立, 分析 19 处不成立, 直接到分析 37处条件成立,ViewGroup自己处理. 第三个参数传入null表示自己处理.
关于问题 2 [ 如果子 View 消费掉 DOWN 事件后, 为什么后续的事件都会直接传给它? 是怎么实现的 ? ] 在这里也能得到解答了.
首先DOWN事件分发完后,mFirstTouchTarget有值了, 那么在后续事件到来后, 分析 19 成立 , 但是分析 21 不成立, 分析 37 处也不成立, 分析 39 处也不成立, 直接就执行了分析 40 处的逻辑, 直接就把mFirstTouchTarget中的目标View传入了dispatchTransformedTouchEvent()方法. 所以只要消费掉DOWN事件后, 后续事件都会直接分发给目标View, 而不会再去遍历查找目标View.
到这里, ViewGroup 的 dispatchTouchEvent 讲解完了, 接下来看 dispatchTransformedTouchEvent 这个方法到底做了什么
5. dispatchTransformedTouchEvent
ViewGroup.java 2988行.
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
//分析 42
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
//分析 43
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
if (newPointerIdBits == 0) {
return false;
}
//分析 44
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 分析 45
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
transformedEvent.recycle();
return handled;
}
-
分析 42
这段逻辑主要是检测是否需要发送CANCEL事件.
如果传入的参数cancel为true, 或者action是ACTION_CANCEL. 则设置消息类型为ACTION_CANCEL, 并将ACTION_CANCEL分发给对应的对象.- 如果没有子
View, 也就是传入的child为null的情况下, 会将消息分发给当前的ViewGroup, 只不调用的是View的dispatchTouchEvent - 如果有子
View,将这个取消事件传递给子View(在上面的分析 40 处, ViewGroup 拦截的情况下会进入到此处),并且调用child的dispatchTouchEvent.
- 如果没有子
分析 43
先获取触摸事件的触摸点ID, 接着与 期望的触摸点ID进行位运算, 并把结果赋值给newPointerIdBits,newPointerIdBits == 0,则不消费此次事件直接返回false.分析 44
若newPointerIdBits = oldPointerIdBits表示是相同的触摸点, 再判断传入的child是否为空, 或者传入的child的变换矩阵还是不是单位矩阵. 如果满足再次判断传入的child是否为null, 为null说明需要ViewGrou去处理事件. 不为null就将事件分发给child处理.
如果是分发给child处理, 会计算事件的偏移量. 因为child在ViewGroup中可能会发生位置变化. 需要除去这些移动距离, 以保证事件到达child的onTouchEvent()中时, 能够正确表示它在child中的相对坐标. 就相当于事件也要跟着child的偏移而偏移.分析 45
如果不是相同的触摸点, 则意味着需要先转化MotionEvent事件, 然后再对转换后的对象进行事件分发.
dispatchTransformedTouchEvent方法会对触摸事件进行重新打包后再分发, 如果它的第三个参数child是null, 则会将事件分发给ViewGroup自己, 只不过此时需要将ViewGroup看做是一个View.
到这里 ViewGroup.dispatchTouchEvent 就分析的差不多了, 如果你能坚持的看到这里, 相信你对 Android 中的事件分发机制学习的差不多了, 剩下的还剩两个 View.dispatchTouchEvent 以及 View.onTouchEvent 这两个方法的分析了. 会在后面讲解.
ViewGroup.dispatchTouchEvent 流程图如下
