目录
  1. 1. 简介
  2. 2. 思路分析
  3. 3. 分发对象
  4. 4. 分发事件的组件
    1. 4.1. ViewGroup
    2. 4.2. View
  5. 5. 分发的核心方法
  6. 6. 事件分发过程
  7. 7. 小结
重拾Android-UI进阶之触摸事件分发机制

简介

本篇主要对事件分发中的基本概念做一下简单介绍,同时也介绍一下参与事件分发的主要方法。从这些方法的核心逻辑中结合源码,总结事件分发的原理。

注意:不同版本间的代码会有区别,本文是基于Android-28的源码进行分析

思路分析

  • touch 事件是如何从驱动层传递给 Framework 层的 InputManagerService;
  • WMS 是如何通过 ViewRootImpl 将事件传递到目标窗口;
  • touch 事件到达 DecorView 后, 又是如何一步步传递到内部的子View中的。

接下来针对上述思路进行知识整理。

分发对象

首先了解被分发的对象到底是哪些?这些被分发的对象就是用户触摸屏幕而产生的的点击事件,事件主要包括:按下、滑动、抬起、取消。这些事件被封装成MotionEvent对象。该对象的主要事件如下表所示:

事件 触发场景 单次事件流中触发的次数
MotionEvent.ACTION_DOWN 在屏幕按下时 1次
MotionEvent.ACTION_MOVE 在屏幕滑动时 0次或多次
MotionEvent.ACTION_UP 在屏幕抬起时 0次或1次
MotionEvent.ACTION_CANCEL 滑动超出控件边界时 0次或1次

按下、滑动、抬起、取消这几种事件组成了一个事件流。事件流以按下为开始,中间可能有若干次滑动,以抬起或者取消作为结束。

在Android对事件分发的处理过程中,主要是对按下事件做分发,进而找到能够处理按下事件的控件。对于事件流中后续的事件(如滑动、抬起等),则直接分发给能够处理按下事件的组件。

分发事件的组件

分发事件的组件,也称为分发事件者,这些包括ActivityViewViewGroup,三者的一般结构为:

分发事件组件关系图.png

从图上可看出,Activity包括了ViewGroupViewGroup又可以包含多个View

组件 特点 示例
Activity Android视图类 如MainActivity
ViewGroup View的容器,可以包含若干个View 各种布局类
View UI类组件的基类 如按钮、文本框

ViewGroup

ViewGroup 是一组View的组合,在其内部有可能包含多个子View,当手指触摸屏幕时,手指所在区域既能在ViewGroup显示范围内,也可能在其内部View控件上。

因此该组件内部的事件分发是处理当前 Group 和子 View 之间的逻辑关系:

1. 当前 Group 是否需要拦截 touch 事件

2. 是否需要将 touch 事件继续分发给子View

3. 如何将 touch 事件 分发给 子View

View

View 是页面上能呈现的最小组件单元,不可再细分,内部也不会存在子 View,所以该组件的事件分发重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一系列的效果展示(比如滑动、放大、点击、长按等)。

所以这里面需要处理一些逻辑关系是:

1. 是否存在 TouchListener;

2. 是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。

分发的核心方法

负责对事件进行分发的方法主要有三个,分别是:


dispatchTouchEvent()

onTouchEvent()

onInterceptTouchEvent()

它们并不是全都存在于所负责分发的组件中,具体情况分析如下表中:

组件 dispatchTouchEvent onTouchEvent onInterceptTouchEvent
Activity 存在 存在 不存在
ViewGroup 存在 存在 存在
View 存在 存在 不存在

从表中可看出,dispatchTouchEventonTouchEvent 方法存在于上述三个组件中,而 onInterceptTouchEvent 单独为ViewGroup所拥有。

ViewGroup类中,实际上是没有 onTouchEvent 方法,但是由于其继承自View,而后者是拥有onTouchEvent方法,因此ViewGroup也是可以调用onTouchEvent 方法的。

事件分发过程

这一章节是本文的核心,具体流程介绍如下。

Android分发事件U型图.png

分发方法:dispatchTouchEvent
从方法名可看出该方法的主功能就是负责分发,这是Android事件分发过程中的核心。事件是如何传递的,主要是看该方法,理解此方法,也就吃透了Android事件分发机制。

在了解该方法的核心机制之前,需要知道这样一个结论:

如果某个组件的该方法返回 TRUE,则表示该组件已对该事件进行了处理,即不会继续调用其他组件的事件分发方法,也就停止了分发。

如果某个组件的该方法返回 FALSE,则表示该组件不能对该事件进行处理,需要按照规则继续分发事件,在不覆写该方法的情况下,除了一些特殊组件(系统已做过处理组件),其余组件默认都是返回false

结合源码分析核心逻辑

// Activity 的 dispatchTouchEvent 方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 如果子 View 消费了该事件,则返回 TRUE,让调用者知道该事件已被消费
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果子 View 没有消费该事件,则调用自身的 onTouchEvent 尝试处理。
return onTouchEvent(ev);
}

这里 Activity 并没有通过自身来判断是否有子View消费分发事件,而是交给它依附的Window对象来处理,但是在Window对象里,该方法是一个抽象方法,实际上它交给了它的具体对象 PhoneWindow处理的;

PhoneWindow核心代码:

@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// 交给DecorView分发处理
return mDecor.superDispatchTouchEvent(event);
}

DecorView核心代码

public boolean superDispatchTouchEvent(MotionEvent event) {
//最终还是调用了ViewGroup里的分发逻辑代码
return super.dispatchTouchEvent(event);
}

这也很好理解,DecorView就是一个ViewGroup,因此,整个流程串通起来即可看出,当事件传递给Activity后,它先将事件分发给子View处理。

如果经过子View层层传递或者处理后,该事件被消费了(即返回 TRUE),则Activity的分发方法也返回 TRUE,同样也表示该事件已经被消费了。

如果经过子View层层传递或者处理后,该事件没有被消费(即返回 FALSE),则Activity的分发方法就会返回 FALSE,接着调用自身的 onTouchEvent 去处理。

如果 onTouchEvent 消费了该事件,那依然能返回 TRUE (表示已消费该事件),这个 TRUE 作为 dispatchTouchEvent 的返回值,让调用它的对象知道该 Activity 已经消费了事件。

如果 onTouchEvent 没有消费该事件,则会返回 FALSE (表示未消费该事件),这个 FALSE 作为 dispatchTouchEvent 的返回值,让调用它的对象知道该 Activity 并没有消费事件,需要继续处理。

接下来看ViewGroup 关于 dispatchTouchEvent 方法的核心逻辑代码(去掉非重点代码)

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {

... ...

boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {

... ...

/*
* 步骤1:检查当前的ViewGroup是否需要拦截事件
*/
final boolean intercepted;
// 如果事件为 DOWN 事件,则调用 onInterceptTouchEvent 进行拦截判断
// 或者
// mFirstTouchTarget 不为空,代表已经有子 View 捕获了这个事件,子 View 的dispatchTouchEvent 返回true 就是代表捕获 touch 事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判断是否不允许拦截 这个是由子View传递过来的控制标记
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果子View告诉父View:你可以拦截 则交给父View拦截处理
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // 恢复操作,以防更改
} else { // 否则不允许父View拦截
intercepted = false;
}
} else {
// 没有触控目标,此动作不是初始的按下操作
// 拥有此标记,则这个View Group 可以继续拦截触摸事件。
intercepted = true;
}

... ...

/*
* 步骤2:将事件分发给子View
*/
if (!canceled && !intercepted) {
// 表明事件主动分发的前提条件是事件为 DOWN 事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {

... ...

final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {

... ...

// 遍历所有子 View
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);

... ...

// 判断事件坐标是否在子 View 坐标范围内,并且子 View 并没有处在动画状态
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}

... ...

// 将事件分发给子 View,如果子 View 捕获事件成功,则将 mFirstTouchTarget 赋值给子 View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {

... ...

newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}

... ...
}
}

... ...
}
}

/*
* 步骤3:根据mFirstTouchTarget,再次分发事件
*/
// 如果 mFirstTouchTarget 为null,说明在上述的事件分发中并没有子 View 对事件进行捕获操作。
// 这种条件下,直接调用 dispatchTransformedTouchEvent 方法,并传入 child 为null,最终会调用 super.dispatchTouchEvent 方法
// 实际上最终是调用自身的 onTouchEvent 方法,进行处理 touch 事件
// 也就是说:如果没有子 View 捕获处理 touch 事件,ViewGroup 会通过自身的 onTouchEvent 方法进行处理
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 {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// mFirstTouchTarget 不为null,说明步骤2中有子 View 对 touch 事件进行了捕获,
// 则直接将当前以及后续的事件交给 mFirstTouchTarget 指向的 View 进行处理。
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}

... ...
}
...
}
}
}
}

dispatch主要分为3大步骤:

  • 步骤1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再传递给子 View(或者以 CANCEL 的方式通知子 View)。

  • 步骤2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将 mFirstTouchTarget 赋值给捕获 touch 事件的 View。

  • 步骤3:根据 mFirstTouchTarget 重新分发事件。

如果步骤1中,当前 ViewGroup 并没有对事件进行拦截,则执行步骤2

事件分发流程demo演示

图片.png

上图 DOWN 事件中,DownInterceptGrouponInterceptTouchEvent 被触发一次;

然后在子 View CaptureTouchViewdispatchTouchEvent 中返回 true,代表它消费了这个 DOWN 事件。

这样 CaptureTouchView 会被添加到父视图(DownInterceptGroup)中的 mFirstTouchTarget 中。

因此后续的 MOVE 和 UP 事件都会经过 DownInterceptGrouponInterceptTouchEvent 进行拦截判断

为什么 DOWN 事件特殊

所有 touch 事件都是从 DOWN 事件开始的,这是 DOWN 事件比较特殊的原因之一。另一个原因是 DOWN 事件的处理结果会直接影响后续 MOVE、UP 事件的逻辑。

在步骤2中,只有 DOWN 事件会传递给子 View 进行捕获判断,一旦子 View 捕获成功,后续的 MOVE 和 UP 事件是通过遍历 mFirstTouchTarget 链表,查找之前接受 ACTION_DOWN 的子 View,并将触摸事件分配给这些子 View。也就是说后续的 MOVE、UP 等事件的分发交给谁,取决于他们的起始事件 DOWN 是由谁捕获的

mFirstTouchTarget的作用

mFirstTouchTarget 的部分源码如下:

private static final class TouchTarget {
private static final int MAX_RECYCLED = 32;
private static final Object sRecycleLock = new Object[0];
private static TouchTarget sRecycleBin;
private static int sRecycledCount;

public static final int ALL_POINTER_IDS = -1; // all ones

// The touched child view.
public View child;

// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;

// The next target in the target list.
public TouchTarget next;

private TouchTarget() {
}
}

可以看出 mFirstTouchTarget 是一个 链表 结构 TouchTarget 类型的对象。这个对象的作用就是用来记录捕获了 DOWN 事件的 View,具体是保存在它的成员变量 child 变量中。

至于为什么是链表类型结构,是因为Android设备支持多指操作,每一个手指的 DOWN 事件都可以当做一个 TouchTarget 保存起来。在步骤3中判断 mFirstTouchTarget 不为null,则再次将事件分发给响应的 TouchTarget

小结

本文主要分析 dispatchTouchEvent 的事件流程机制,这一过程主要分3部分:

  • 判断是否需要拦截 ——> 主要根据 onInterceptTouchEvent 方法的返回值来决定是否拦截;

  • 在 DOWN 事件中将 touch 事件分发给子 View ——> 这一过程如果有子 View 捕获消费了 touch 事件,则会对 mFirstTouchTarget 进行赋值

  • 最后,DOWN、MOVE、UP 事件都会根据 mFirstTouchTarget 是否为null,决定是自己处理 touch 事件,还是再次分发给子 View。

还有整个事件分发中一些特殊的注意点:

  • DOWN 事件的特殊:事件的起点;决定后续事件由谁来消费处理;

  • mFirstTouchTarget 作用:记录捕获消费 touch 事件的 View,是一个链表结构;

  • CANCEL 事件的触发场景:当父视图先不拦截,然后再 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件。

打赏
  • 微信
  • 支付宝

评论