1、概述
Android 中用 MotionEvent 表示屏幕上发生的事件,常用的事件类型如下:
这些事件是从 Activity 逐层传递到 View/ViewGroup 上的:
上图左侧是 Activity 的布局层级结构图,从 Activity 开始向内依次为 PhoneWindow、DecorView,DecorView 继承自 FrameLayout, 通过 addView() 将 root 添加进来,系统会根据不同的 Window 属性为 root 选择合适的布局,默认就是使用 screen_simple.xml,该布局所包含的 R.layout.xxx 正是在 Activity 的 setContentView(R.layout.xxx) 中设置的布局。
上图右侧的事件分发流程图与左侧的层级图对应,可以看出事件是如何从 Activity 传递到 View/ViewGroup 上的。需要注意的是,ViewGroup 主要是负责把事件分发给子 View/ViewGroup 的,只有在 ViewGroup 拦截了某个事件,或者所有子控件都不能处理这个事件时,才会自己处理(也称消费)这个事件;而 View 就仅是处理事件,因为它没有子控件可以分发,因此虽然 ViewGroup 和 View 中都有 dispatchTouchEvent(),但前者是用来分发,后者是用来处理。
下面将结合源码(基于 API 30)从 3 个角度分析事件分发流程:
- Activity 分发至 ViewGroup
- ViewGroup 的事件分发
- View 的事件处理
2、Activity 分发至 ViewGroup
我们从 Activity 开始,事件分发的入口方法是 dispatchTouchEvent():
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 空方法,有特殊需求的子类可以重写
onUserInteraction();
}
// getWindow() 得到 Window 的唯一实现类 PhoneWindow,如果
// 该事件向下分发得到了处理,那么就返回 true 表示该事件已被处理
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果事件分发出去没有被处理,就调用自己处理事件的方法
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
// 检查是否满足点击时应该关闭的条件(比如事件发生坐标在 Activity 之外),
// 满足的话就关闭当前 Activity 并返回 true 表示事件已经被处理。
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
// 如不满足条件就返回 false 表示事件未被处理,交由上一层处理
return false;
}
接着来到 PhoneWindow:
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
再进入到 DecorView:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView 继承自 FrameLayout,由于 FrameLayout 没重写 dispatchTouchEvent(),所以就要去看 ViewGroup 的 dispatchTouchEvent()。到这里,就完成了事件由 Activity 到 ViewGroup 的分发。
3、ViewGroup 的事件分发
ViewGroup 接到父容器分发来的事件,有两种选择,一是将该事件拦截由自己处理,二是将事件分发给子控件,如果子控件是 ViewGroup,就递归执行分发过程(当然也可能会拦截),直到子控件是 View 为止。
虽然事件最终会分发到某一个子控件上,但是子控件需要将事件的处理结果,即能否处理(消费)该事件返回给父容器。如果一个 ViewGroup 的所有子控件都不能处理某个事件,那么就只能由该 ViewGroup 自己处理该事件,并将处理结果继续向上回传:
在上图中:
- 蓝色线表示的是分发过程,如果 ViewGroup 不拦截就向下分发直到一个 View,如果拦截就调用自己处理事件的方法 onTouchEvent()。该方法返回的 boolean 类型结果表示该控件是否消费了该事件
- 红色线表示的是结果返回过程,如果 ViewGroup 在调用 dispatchTouchEvent() 分发事件时,收到的结果是所有子控件都不能处理该事件,那么它会调用自己的 onTouchEvent() 查看自己是否能处理事件,并将结果返回给 dispatchTouchEvent() 再返回给上一层分发者
以上是事件分发模型的思路,在开始看源码之前还有一件事需要了解:是所有类型的事件都要走以上流程吗?
答案是否定的。一个事件流其实是从 ACTION_DOWN 开始,中间混杂这若干个 ACTION_MOVE,最终以 ACTION_UP 或者 ACTION_CANCEL 结束。实际上只有 ACTION_DOWN 事件会走以上递归分发的流程,一旦找到了可以处理 ACTION_DOWN 的控件,随后的事件就都交给该控件处理。
由于 dispatchTouchEvent() 内容较多,因此我们将该方法内容分为 4 段详解。此外,还需说明,我们只着重分析单点触控,涉及到多点触控的内容,只有在影响理解时会稍作解释。
对 ACTION_DOWN 的预处理
ACTION_DOWN 作为一串事件流的第一个事件,会有一些特殊处理,比如初始化或者一些变量的重置:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// handled 是返回结果,表示事件是否被消费
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/**
* 1.如果事件是 ACTION_DOWN,证明是一串新的事件流,要进行
* 预处理,重置上一段事件流使用的状态变量等等
*/
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 置空链表 mFirstTouchTarget,清除所有 TouchTargets
cancelAndClearTouchTargets(ev);
// 清除 mGroupFlags 中的 FLAG_DISALLOW_INTERCEPT 标记位
resetTouchState();
}
...
}
}
cancelAndClearTouchTargets() 负责重置 mFirstTouchTarget 这个链表:
private void cancelAndClearTouchTargets(MotionEvent event) {
if (mFirstTouchTarget != null) {
boolean syntheticEvent = false;
if (event == null) {
final long now = SystemClock.uptimeMillis();
event = MotionEvent.obtain(now, now,
MotionEvent.ACTION_CANCEL, 0.0f, 0.0f, 0);
event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
syntheticEvent = true;
}
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
resetCancelNextUpFlag(target.child);
// 第一次调用该方法,第三个参数传的是分发事件的子 View,而第二个参数传 true 会给
// target.child 分发一个 ACTION_CANCEL 事件
dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
}
clearTouchTargets(); // 清空 mFirstTouchTarget 链表
if (syntheticEvent) {
event.recycle();
}
}
}
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
mFirstTouchTarget 是 TouchTarget 类型的链表头,TouchTarget 是能处理 ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE 事件的 View/ViewGroup 的一个包装类,记录了 View/ViewGroup 的相关信息。
resetTouchState() 会做一些其他变量的重置工作,包括表示“不允许拦截”的标记位 FLAG_DISALLOW_INTERCEPT,该标记位在后续判断 ViewGroup 是否拦截事件时会起到关键作用:
// 表示“不允许拦截”的标记位
protected static final int FLAG_DISALLOW_INTERCEPT = 0x80000;
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
// 初始化时需要重置这个标记位为 0,表示允许拦截
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
事件拦截
接下来要判断当前的 ViewGroup 是否要拦截事件:
- 如果不拦截,就向子控件进行事件分发
- 如果拦截,就不会向下分发,而是自己作为分发过程的最后一个节点,自己在 onTouchEvent() 中来处理该事件,如果消费了事件就返回 true
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// handled 是返回结果,表示事件是否被消费
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
/**
* 2.判断是否对事件进行拦截
* 当事件是 ACTION_DOWN 或者 mFirstTouchTarget 链表(存放的是能处理
* ACTION_DOWN 的控件的节点)不为空时,先判断是否拦截事件,如果
* onInterceptTouchEvent() 返回 true 表示拦截,那么该事件会交给
* 当前 ViewGroup 处理而不分发。mFirstTouchTarget != null 其实是此前
* 已经找到了处理 ACTION_DOWN 的控件,然后要判断其它事件是否被拦截
*/
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 上面对 ACTION_DOWN 预处理时, resetTouchState() 清除了 FLAG_DISALLOW_INTERCEPT,所
// 以对于 ACTION_DOWN 而言,下面的 disallowIntercept 算出来一定是 false,也就是允许拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果允许拦截的话,就要看 onInterceptTouchEvent() 的返回值
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
// 如果不允许拦截的话,intercepted 直接赋值为 false 表示不拦截
intercepted = false;
}
} else {
// 如果不是 ACTION_DOWN 事件且此前没有找到没有能消费 ACTION_DOWN
// 事件的子控件,那么就不用分发该事件,直接做拦截处理
intercepted = true;
}
...
}
}
这段代码的作用其实就是为表示是否进行拦截的 intercepted 进行赋值:
- if 条件说明了,只有在事件为 ACTION_DOWN 或者 mFirstTouchTarget 不为空(即此前已经找到了处理 ACTION_DOWN 事件的子控件)才会进行拦截判断
- else 条件指明,若不满足以上条件之一,即使进行事件分发,也不会找到可以消费事件的子控件,于是直接进行拦截令 intercepted = true
再进入 if 条件看,首先看是否允许拦截,默认情况下,mGroupFlags 是允许进行拦截的,除非子控件调用了父容器的 requestDisallowInterceptTouchEvent(),将 mGroupFlags 中的 FLAG_DISALLOW_INTERCEPT 置位:
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
// disallowIntercept 是子控件传递给我们的,如果当前 mGroupFlags 中关于
// FLAG_DISALLOW_INTERCEPT 的状态已经跟 disallowIntercept 一样,就直接返回
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
// 而如果 mGroupFlags 与 disallowIntercept 状态不同,则通过位运算使其相同
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
// 如果有父控件还要将这个拦截状态传递给父控件
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
虽然 requestDisallowInterceptTouchEvent() 可以将 disallowIntercept 一直向上传递到祖先,但是需要注意,前面介绍 ACTION_DOWN 预处理时提到过,resetTouchState() 中将 mGroupFlags 中的 FLAG_DISALLOW_INTERCEPT 重置为 0 了。因此,requestDisallowInterceptTouchEvent() 无论怎样设置,对于 ACTION_DOWN 事件来说,父容器都是可以拦截的,除非重写 ViewGroup.onInterceptTouchEvent() 令其返回 false。
假设子控件没有调用该方法禁止父容器拦截事件,那么 disallowIntercept 为 false,可以进入下一个 if 判断,intercepted 的值就由 onInterceptTouchEvent() 决定:
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
该方法默认返回 false 表示不拦截,只有在同时满足 if 中的四个条件时才会拦截。
分发 ACTION_DOWN 事件
事件分发有一个原则,就是先找到能处理 ACTION_DOWN 事件的控件,然后将事件流的后续事件优先分发到该控件上。
ViewGroup 在分发事件时,如果不拦截并且不取消事件的话,就要从子控件中寻找谁可以处理 ACTION_DOWN 事件了:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// handled 是返回结果,表示事件是否被消费
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
// 检查是否需要取消
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
// 如果有子 View 能处理事件,将其包装为 TouchTarget 赋值给 newTouchTarget
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 3.如果不取消且不拦截,就开始递归分发流程
if (!canceled && !intercepted) {
// ACTION_POINTER_DOWN 是多点触控中非主要(非首个)手指按下事件,
// ACTION_HOVER_MOVE 是鼠标移动事件,我们主要关注 ACTION_DOWN
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int childrenCount = mChildrenCount;
// newTouchTarget 刚刚在上面初始化为 null,childrenCount 是直接子控件的数量
if (newTouchTarget == null && childrenCount != 0) {
final float x =
isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
final float y =
isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
// 寻找可以接收事件的子 View,preorderedList 是按照 View 的 Z 值
// 由小到大排列的 List,从中取出 child 时则是倒序遍历的,也就是
// 从视图层级顶部的 child 开始筛选分发对象
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
final View[] children = mChildren;
// 倒序遍历 preorderedList
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// child 可见或有动画效果,并且事件坐标在 child 的范围内才满足分发的筛选条件
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 对于单点触控来说,此时还在找能处理 ACTION_DOWN 的节点,所以
// mFirstTouchTarget 为 null,getTouchTarget() 就为 null
newTouchTarget = getTouchTarget(child);
// 如果子控件已经接收了在其范围内的触摸事件,此时需要为其,在已经
// 处理的手指 ID 之外,再给其一个新的手指 ID,这是多点触控的情况
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// 在 dispatchTransformedTouchEvent() 中将事件分发给 child,
// 如果 child 为 null 就假设事件是分发给当前 ViewGroup 的
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 能进入 if 条件就表示分发出去的事件被child处理了
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// 将处理了事件的 child 包装成 TouchTarget 添加到链表的头部并返回给
// newTouchTarget,而 mFirstTouchTarget 指向链表头,此时 newTouchTarget = mFirstTouchTarget
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 找到一个能处理事件的子 View 就不用找其他的了,所以跳出循环
break;
}
}
// for 循环遍历结束,清空 preorderedList
if (preorderedList != null) preorderedList.clear();
}
/**
* 如果没有找到处理事件的子控件,并且 mFirstTouchTarget 链表不为空,
* 就让 newTouchTarget 指向最早加入链表的那个 TouchTarget。
* 这是多点触控时才会出现的条件,暂不考虑
*/
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
...
}
return handled;
}
对子控件排序
在遍历子控件之前,先用 buildTouchDispatchChildList() 创建了一个 ArrayList<View> 类型的 List,目的是把所有子控件按照其 Z 值由小到大进行排序:
public ArrayList<View> buildTouchDispatchChildList() {
return buildOrderedChildList();
}
private ArrayList<View> mPreSortedChildren;
// 构造一个 Z 值由小到大的 List
ArrayList<View> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
if (childrenCount <= 1 || !hasChildWithZ()) return null;
// 初始化 List
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
// callers should clear, so clear shouldn't be necessary, but for safety...
mPreSortedChildren.clear();
mPreSortedChildren.ensureCapacity(childrenCount);
}
// 判断 mGroupFlags 中的 FLAG_USE_CHILD_DRAWING_ORDER 标记位,表示
// 绘制子控件的顺序是否由 getChildDrawingOrder() 指定,多数情况为 false
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
// 获取待插入的子 View 对象及其 Z 值
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// 默认的插入位置在列表尾部
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
// 如果当前子控件的 Z 值比前面的小,就把插入位置前移
insertIndex--;
}
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
然后在遍历 preorderedList 时,for 循环从 childrenCount - 1 开始,也就是倒序遍历,先取 Z 值较大的 View,即在 View 的视图层级中较上层的 View,所以才会有上层的 View 先接收事件的说法。如果是同一层级的,写在前面的 View 会先被遍历到。
寻找分发目标
倒序遍历子控件的目的是找到一个能消费 ACTION_DOWN 的控件,该控件作为目标控件还需接收同一事件流中, ACTION_DOWN 后续的一系列事件。
在正式开始寻找分发目标前,要通过 View.canReceivePointerEvents() 和 ViewGroup.isTransformedTouchPointInView() 对子控件进行一个筛选:
// View 可见或者有动画才可以接收事件
protected boolean canReceivePointerEvents() {
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
// 事件坐标在 child 内才可以接收事件
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
transformPointToViewLocal(point, child);
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
同时满足上述两个条件才有作为目标控件的资格。注意,这个动画是一个 Animation,比如一个平移动画,事件坐标应该在动画的起始位置才算满足条件,在动画终点不算。
筛选后,才由 dispatchTransformedTouchEvent() 寻找目标控件:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 取消等特殊情况暂不考虑,只看一般流程……
// Perform any necessary transformations and dispatch.
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());
}
// 调用 child 的 dispatchTouchEvent() 分发事件
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
如果子控件 child 为 null,则由当前 ViewGroup 自己处理事件,由于 ViewGroup 中没有重写处理事件的 onTouchEvent(),它要使用它的父亲,即 View 的处理事件的方法 dispatchTouchEvent()。前面说过,ViewGroup 的 dispatchTouchEvent() 用来分发事件,View 的 dispatchTouchEvent() 用来处理事件,最终会调用到 onTouchEvent()。
如果 child 不为 null,就调用 child 的 dispatchTouchEvent()。由于 child 可能是 ViewGroup 或者 View,所以后续的流程也就会有差别:
- 如果 child 是一个 ViewGroup,调用它的 dispatchTouchEvent() 会继续向下分发这个事件,形成一个递归,直到分发到一个 View
- 如果 child 是一个 View,调用它的 dispatchTouchEvent() 会检查能否对事件进行处理,能处理就返回 true
假如 dispatchTransformedTouchEvent() 找到了能处理事件的子控件,返回 true 就会进入 if 条件,通过 addTouchTarget() 将该子控件对应的 TouchTarget 对象添加到 mFirstTouchTarget 的头部:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
// 新假如的节点的 next 指向原来的链表头,相当于是在头部添加链表新节点
target.next = mFirstTouchTarget;
// mFirstTouchTarget 指向链表头
mFirstTouchTarget = target;
return target;
}
对于单点触控,其实 mFirstTouchTarget 链表中就只有一个元素,就是处理 ACTION_DOWN 那个子控件对应的 TouchTarget。但如果是多点触控的话,除了事件流起始的 ACTION_DOWN 之外,还会有多个 ACTION_POINTER_DOWN 事件,如果也能成功分发,也会调用 addTouchTarget() 将处理 ACTION_POINTER_DOWN 的 TouchTarget 添加到链表中。所以 mFirstTouchTarget 里边放的就是能处理 ACTION_DOWN、ACTION_POINTER_DOWN 事件(实际上也有 ACTION_HOVER_MOVE)的 TouchTarget。
结果处理
遍历之后如果没有找到 newTouchTarget 但是 mFirstTouchTarget 链表不为空,其实就是在多点触控的情况下(单点触控不会满足这种条件),新的 ACTION_POINTER_DOWN 事件没有分发出去,那么就让 newTouchTarget 指向链表中末尾的元素,也就是最早进入链表的那个 TouchTarget:
/**
* 如果没有找到处理事件的子 View,并且 mFirstTouchTarget 链表不为空,
* 就让 newTouchTarget 指向最早加入链表的那个 TouchTarget。
*/
if (newTouchTarget == null && mFirstTouchTarget != null) {
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
到这里,针对 ACTION_DOWN 事件的处理就结束了,无非两种结果:
- 分发成功,mFirstTouchTarget 链表一定不为 null(单点触控有一个 TouchTarget,多点触控可能有多个),事件流中后续事件的分发都以链表中的节点为分发目标;
- 分发失败,mFirstTouchTarget 为 null,没有子控件能处理 ACTION_DOWN 事件,那么就只能像拦截了 ACTION_DOWN 事件那样,自己处理了。
另外需要注意一点,只有 ACTION_DOWN 走了这个递归分发的流程,其它事件是根据 ACTION_DOWN 的分发结果去 mFirstTouchTarget 链表中找分发目标的,不会递归。
其它事件处理
其他事件,主要是 ACTION_MOVE 和 ACTION_CANCEL,会走以下流程,需要注意的是,ACTION_MOVE 在一串流中会有多个,如下代码也会走多次:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
...
/**
* 4.其他事件处理。此前的代码都是针对 ACTION_DOWN 进行处理的,
* 从这里开始,是所有事件都会执行的代码
*/
if (mFirstTouchTarget == null) {
/**
* mFirstTouchTarget 为 null 时,说明没有调用到 addTouchTarget() 将 TouchTarget
* 添加到 mFirstTouchTarget 链表中,可能是 intercepted 为 true 进行了拦截,也可能
* 是在前面遍历分发事件时,没能找到处理该事件的子控件,就只能交由当前的 ViewGroup
* 进行处理。也就是说,分发下去没有被处理,和直接拦截是一样的……
*/
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
// mFirstTouchTarget 在单点触控且有子控件处理 ACTION_DOWN 时
// 也只有一个元素,只有多点触控才有可能有多个元素,所以对于单点
// 而言,while 循环就只执行一次
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// ACTION_DOWN 满足 if 条件
handled = true;
} else {
// 其它事件的分发走这,如果被拦截了,cancelChild 为 true,要取消事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 通过 dispatchTransformedTouchEvent() 分发,target 是 mFirstTouchTarget
// 中的节点,也就是处理 ACTION_DOWN 的控件对应的 TouchTarget
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 取消处理
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
}
能清晰的看出,分两种情况:
- if (mFirstTouchTarget == null) 表示之前没有找到能处理 ACTION_DOWN 的 TouchTarget,所以就只能由当前 ViewGroup 对象自己进行处理。在调用 dispatchTransformedTouchEvent() 时给表示子控件的第三个参数传 null,可以调用到 View.dispatchTouchEvent() 最终调用 onTouchEvent()
- else 条件表示此前找到了能处理 ACTION_DOWN 的 TouchTarget,那么就遍历 mFirstTouchTarget,通过 dispatchTransformedTouchEvent() 找到能消费事件的控件,第三个参数传了 TouchTarget.child,就是以 TouchTarget 为根向下递归去找,如果没找到,就继续遍历 mFirstTouchTarget 中的下一个 TouchTarget。当然,对于单点触控而言,mFirstTouchTarget 只有一个节点,遍历时只循环一次
因此我们看到,对 ACTION_DOWN 以外其他事件的处理,是先分发给之前处理了 ACTION_DOWN 的 TouchTarget,如果其表示的控件处理不了,就递归给它的子控件。如果所有控件都处理不了就由当前 ViewGroup 处理这个事件。
此外在 else 中我们需要关注一下 cancelChild,该变量贯穿了 else 条件。
这里我们举一个实际例子,比如 ViewPager 内嵌套了一个上下滑动的 RecyclerView。我们先上下滑动,由于 ViewPager 在进行上下滑动时,不会拦截事件:
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
...
switch (action) {
case MotionEvent.ACTION_MOVE: {
...
if (action != MotionEvent.ACTION_DOWN) {
if (mIsBeingDragged) {
return true;
}
if (mIsUnableToDrag) {
return false;
}
}
...
// 横向偏移量大于 mTouchSlop 并且大于纵向偏移量的 2 倍,则认为
// 需要左右滑动,将返回值 mIsBeingDragged 置为 true 进行拦截
if (xDiff > mTouchSlop && xDiff * 0.5f > yDiff) {
mIsBeingDragged = true;
requestParentDisallowInterceptTouchEvent(true);
setScrollState(SCROLL_STATE_DRAGGING);
mLastMotionX = dx > 0
? mInitialMotionX + mTouchSlop : mInitialMotionX - mTouchSlop;
mLastMotionY = y;
setScrollingCacheEnabled(true);
} else if (yDiff > mTouchSlop) {
// 不满足左右滑动,但是纵向偏移大于 mTouchSlop 时认为是上下滑动,
// 对于 ViewPager 而言是不能拖动的情况,因此不进行拦截
mIsUnableToDrag = true;
}
...
}
}
这样 RecyclerView 拿到事件处理权进行上下滑动。接下来改为左右滑动,由于 RecyclerView 并不会处理左右滑动,它允许 ViewPager 对事件进行拦截:
/**
* RecyclerView 的 ACTION_MOVE 事件处理,只有在自己可以进行滑动时才会调用
* requestDisallowInterceptTouchEvent() 请求父容器不要拦截事件,默认情况下可以拦截
*/
@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
case MotionEvent.ACTION_MOVE: {
...
final int x = (int) (e.getX(index) + 0.5f);
final int y = (int) (e.getY(index) + 0.5f);
int dx = mLastTouchX - x;
int dy = mLastTouchY - y;
...
if (mScrollState == SCROLL_STATE_DRAGGING) {
mReusableIntPair[0] = 0;
mReusableIntPair[1] = 0;
if (dispatchNestedPreScroll(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
mReusableIntPair, mScrollOffset, TYPE_TOUCH
)) {
dx -= mReusableIntPair[0];
dy -= mReusableIntPair[1];
// Updated the nested offsets
mNestedOffsets[0] += mScrollOffset[0];
mNestedOffsets[1] += mScrollOffset[1];
// Scroll has initiated, prevent parents from intercepting
getParent().requestDisallowInterceptTouchEvent(true);
}
mLastTouchX = x - mScrollOffset[0];
mLastTouchY = y - mScrollOffset[1];
if (scrollByInternal(
canScrollHorizontally ? dx : 0,
canScrollVertically ? dy : 0,
e)) {
getParent().requestDisallowInterceptTouchEvent(true);
}
if (mGapWorker != null && (dx != 0 || dy != 0)) {
mGapWorker.postFromTraversal(this, dx, dy);
}
}
} break;
}
...
}
当进行左右滑动时,RecyclerView 允许父容器拦截,满足 ViewPager 的 onTouchEvent() 在 ACTION_MOVE 时进行拦截的条件,因此 ViewPager 进行了拦截。这样,在 ViewGroup 的 dispatchTouchEvent() 中,判断拦截的变量 intercepted = true:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// RecyclerView 允许 ViewPager 拦截,并且 ViewPager 真的拦截了
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
}
拦截之后,代码继续向下走到我们本节开头提到的 else 条件:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// intercepted = true,因此 cancelChild = true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 由于 cancelChild = true,因此这里实际是给 target.child 发了
// 一个 ACTION_CANCEL,取消 RecyclerView 的事件处理
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 取消了 target 的事件处理权,需将其从 mFirstTouchTarget 中移除
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
}
当 dispatchTransformedTouchEvent() 的第二个参数为 true 时,会向指定的 target.child 发送一个 ACTION_CANCEL:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 备份原始的 Action
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 将 event 的 Action 设置为 ACTION_CANCEL,这里只需设置 Action
// 并发送出去即可
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
// 还原 event 原本的 Action
event.setAction(oldAction);
return handled;
}
...
}
这样 RecyclerView 接收到 ACTION_CANCEL 就会取消滚动操作:
@Override
public boolean onTouchEvent(MotionEvent e) {
...
switch (action) {
...
case MotionEvent.ACTION_CANCEL: {
cancelScroll();
} break;
}
}
以上是由上下滑动转为左右滑动后,第一个 ACTION_MOVE 事件的过程。由于 ACTION_MOVE 事件有多个,第二个 ACTION_MOVE 到来,走 ViewPager(ViewPager 没重写实际上还是走 ViewGroup)的 dispatchTouchEvent() 时,在判断是否拦截时,对于单点触控而言,满足拦截条件:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
...
final boolean intercepted;
// 第一个左右滑动的 ACTION_MOVE 在最后移除了 mFirstTouchTarget 中的 target,
// 对于单点触控只有一个节点的 mFirstTouchTarget 而言,此时其已经为 null,因
// 此这里会走 else 条件直接给 intercepted = true
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// RecyclerView 允许 ViewPager 拦截,并且 ViewPager 真的拦截了
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
}
然后代码继续执行,这次在处理其他事件的代码部分,就要走 if 条件了:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
...
if (onFilterTouchEventForSecurity(ev)) {
// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
...
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// 这里 canceled 应该为 false
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
}
再次进入 dispatchTransformedTouchEvent(),本次条件是不取消,但是子控件为空:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
...
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);
}
// Done.
transformedEvent.recycle();
return handled;
}
调用 super.dispatchTouchEvent() 就是调用 View 的同名方法,最终会调用 onTouchEvent(),也就是说,由 ViewPager 自己处理了。
由以上流程,也可以得出一些结论:
- 如果子控件没有要求父控件不拦截,父控件是可以抢夺子控件的事件的,但是反之不可
- 子控件一旦拿到了 ACTION_DOWN 事件,事件流由谁处理就是子控件说了算了,它可以自己处理完整个事件流,也可以在自身不处理后续事件且没有要求父控件不拦截的情况下被父控件拦截,交由父控件处理
4、View 的事件处理
ViewGroup 通过调用子控件的 dispatchTouchEvent() 将事件分发给子控件,如果子控件是一个 View,那么 dispatchTouchEvent() 就用来处理该事件:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
// 如果 View 是 ENABLED 的并且处理了 ScrollBar 的拖拽,则认为处理了事件。
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
// 检查监听器
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// result 为 false 的情况下,检查 onTouchEven() 返回值。
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
优先检查 View 上是否设置了监听器,如果一个处于 ENABLED 的 View 设置了监听器,并且设置的是 OnTouchListener,且 OnTouchListener 的 onTouch() 返回了 true,那么 result 就赋值为 true 表示处理了事件。
如果上一个条件不满足使得 result 为 false 了,就再看 onTouchEvent() 是否返回 true,是 true 也表示事件被处理。
onTouchEvent() 中就分别对 ACTION_UP、ACTION_DOWN、ACTION_CANCEL、ACTION_MOVE 等事件进行处理。其中在处理 ACTION_UP 事件时,如果判断出需要执行点击事件,就会执行 performClickInternal():
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
// 检查 OnClickListener 是否为空,不为空就直接执行 onClick() 并返回 true。
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
综合以上代码可以看出,如果 View 设置了 OnTouchListener 并且 onTouch() 返回了 true,那么 View 上设置的 OnClickListener 就不会被执行。