友情提示:
本章内容主要是从源码上分析 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
添加顺序从前往后排的, 但是还是会受到子View
z 轴的影响. 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 流程图如下
网友评论