目录
  1. 1. 简介
  2. 2. 触摸事件的完整传递链路:从内核到应用
    1. 2.1. 1. 内核驱动层:触摸屏驱动
    2. 2.2. 2. EventHub:读取原始事件
    3. 2.3. 3. InputReader:事件解析与加工
    4. 2.4. 4. InputDispatcher:事件分发到目标窗口
    5. 2.5. 5. InputChannel:跨进程传递
    6. 2.6. 6. ViewRootImpl.WindowInputEventReceiver:应用端接收器
    7. 2.7. 完整链路图
  3. 3. 思路分析
  4. 4. 分发对象 —— MotionEvent 结构详解
    1. 4.1. MotionEvent 的核心字段
      1. 4.1.1. 事件坐标
      2. 4.1.2. 事件时间
      3. 4.1.3. 多点触控相关
      4. 4.1.4. 事件动作(Action)编码
      5. 4.1.5. 历史数据(批量移动事件)
  5. 5. 分发事件的组件
    1. 5.1. ViewGroup
    2. 5.2. View
  6. 6. 分发的核心方法
  7. 7. OnTouchListener 与 OnClickListener 的执行顺序与优先级
    1. 7.1. View.dispatchTouchEvent 源码分析
    2. 7.2. OnLongClickListener 与 OnTouchListener 的关系
  8. 8. 事件分发过程
    1. 8.1. Activity 的 dispatchTouchEvent 方法
    2. 8.2. ViewGroup 的 dispatchTouchEvent 方法
  9. 9. 滑动冲突解决方案
    1. 9.1. 外部拦截法(推荐)
    2. 9.2. 内部拦截法
    3. 9.3. requestDisallowInterceptTouchEvent 的实现
  10. 10. NestedScrolling 机制
    1. 10.1. 为什么需要 NestedScrolling
    2. 10.2. 核心接口
    3. 10.3. 嵌套滚动流程
    4. 10.4. NestedScrollingParent2 / NestedScrollingChild2(API 21+)
    5. 10.5. 典型应用:CoordinatorLayout + AppBarLayout + RecyclerView
  11. 11. 小结
    1. 11.1. 关键源码文件
UI进阶之触摸事件分发机制

简介

本篇主要对事件分发中的基本概念做深入介绍,同时也介绍参与事件分发的主要方法。从这些方法的核心逻辑中结合源码,总结事件分发的原理。除了应用层的事件分发(从 DecorView 到子 View),本文还会追溯触摸事件从内核驱动到应用层的完整传递链路:InputReader → InputDispatcher → InputChannel → ViewRootImpl。

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

触摸事件的完整传递链路:从内核到应用

在分析应用层的事件分发之前,有必要先理解触摸事件是如何从硬件最终到达 Activity 的。这个链路涉及多个系统服务。

1. 内核驱动层:触摸屏驱动

当用户手指触摸屏幕时,触摸屏硬件产生中断。内核中的触摸屏驱动读取触摸坐标和压力数据,通过 Linux 输入子系统(input subsystem)将事件写入 /dev/input/eventX 设备文件。

事件以 input_event 结构体形式输出:

struct input_event {
struct timeval time;
__u16 type; // EV_KEY, EV_ABS, EV_SYN...
__u16 code; // ABS_MT_POSITION_X, ABS_MT_POSITION_Y, BTN_TOUCH...
__s32 value; // 坐标值或状态
};

2. EventHub:读取原始事件

源码位置:frameworks/native/services/inputflinger/reader/EventHub.cpp

EventHub 通过 epoll 机制监听所有输入设备文件(/dev/input/event*),将原始 input_event 转换为 Android 内部表示。它负责:

  • 打开和管理所有输入设备(包括触摸屏、键盘、传感器等)。
  • 通过 epoll_wait 监听设备事件。
  • 将原始事件包装为 RawEvent 并传递给 InputReader。

3. InputReader:事件解析与加工

源码位置:frameworks/native/services/inputflinger/reader/InputReader.cpp

InputReader 运行在 InputReaderThread 中。它从 EventHub 获取 RawEvent,交给对应的 InputMapper 处理:

  • TouchInputMapper:处理触摸事件。将驱动层的 ABS_MT_POSITION_X/Y 等原始坐标转换为屏幕坐标,识别多点触控,产生 NotifyMotionArgs
  • KeyboardInputMapper:处理按键事件。
  • SwitchInputMapper:处理开关事件。
void InputReader::loopOnce() {
// 从 EventHub 获取事件
size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

if (count) {
processEventsLocked(mEventBuffer, count);
}
// ...
// 将加工后的事件发送给 InputDispatcher
mQueuedListener->flush();
}

4. InputDispatcher:事件分发到目标窗口

源码位置:frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp

InputDispatcher 运行在 InputDispatcherThread 中。它通过 InputChannel 将事件发送给目标应用窗口:

  1. 从 InputReader 接收 NotifyMotionArgs
  2. 查找触摸坐标对应的目标窗口(通过 WindowManagerService 维护的窗口 Z-order 列表)。
  3. 将 MotionEvent 通过 InputChannel 的 socket 发送给目标窗口的 InputConsumer。
int32_t InputDispatcher::findTouchedWindowAtLocked(int32_t displayId,
int32_t x, int32_t y, ...) {
// 遍历可触摸窗口列表(按 Z-order 从上到下)
for (const sp<TouchedWindow>& touchedWindow : mTouchedWindows) {
if (touchedWindow->windowInfo->touchableRegionContainsPoint(x, y)) {
return touchedWindow->displayId; // 返回找到的窗口
}
}
// ...
}

5. InputChannel:跨进程传递

InputChannel 是对内核 pipe/socket 的封装,用于 InputDispatcher(system_server 进程)和 ViewRootImpl(应用进程)之间的跨进程通信。

每个窗口在通过 WMS 注册时,会创建一对 InputChannel——一端给 InputDispatcher,一端给应用进程的 ViewRootImpl。

6. ViewRootImpl.WindowInputEventReceiver:应用端接收器

源码位置:frameworks/base/core/java/android/view/ViewRootImpl.java

final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event) {
// 将事件入队
enqueueInputEvent(event, this, 0, true);
}
}

enqueueInputEventdoProcessInputEventsdeliverInputEvent → 最终根据事件类型调用 processPointerEvent(MotionEvent)或 processKeyEvent(KeyEvent)。

private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent) q.mEvent;
// 交给 DecorView 处理 → 进入应用层的事件分发链
boolean handled = mView.dispatchPointerEvent(event);
// ...
}

完整链路图

[触摸屏硬件]
↓ 中断 + I2C/SPI
[内核触摸驱动] → /dev/input/eventX
↓ epoll
[EventHub] → RawEvent

[InputReader] → NotifyMotionArgs

[InputDispatcher] → 通过 InputChannel (socket pair)

[ViewRootImpl.WindowInputEventReceiver]

[DecorView.dispatchTouchEvent] → 应用层分发开始

思路分析

理解了完整传递链路后,应用层的分析聚焦在以下问题:

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

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

分发对象 —— MotionEvent 结构详解

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

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

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

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

MotionEvent 的核心字段

源码位置:frameworks/base/core/java/android/view/MotionEvent.java

事件坐标

// 相对于当前 View 左上角的坐标
float getX();
float getY();

// 相对于屏幕左上角的原始坐标
float getRawX();
float getRawY();

事件时间

// 事件发生的时间戳(uptimeMillis)
long getEventTime();
// 从 boot 以来经过的时间
long getDownTime();

多点触控相关

// 获取触控点数量
int getPointerCount();
// 获取指定的触控点 ID
int getPointerId(int pointerIndex);
// 获取指定触控点的坐标
float getX(int pointerIndex);
float getY(int pointerIndex);
// 获取当前事件的触控点索引(相对于 ACTION_POINTER_DOWN/UP)
int getActionIndex();

事件动作(Action)编码

MotionEvent 的 action 是一个复合整数值,低 8 位表示动作类型,高 8 位表示触控点索引:

int action = event.getAction();
int actionMasked = event.getActionMasked(); // 提取动作类型
int pointerIndex = event.getActionIndex(); // 提取触控点索引

动作类型常量:

  • ACTION_DOWN(0):第一个手指按下。
  • ACTION_UP(1):最后一个手指抬起。
  • ACTION_MOVE(2):手指移动。
  • ACTION_CANCEL(3):事件被取消。
  • ACTION_OUTSIDE(4):触摸在 View 边界外。
  • ACTION_POINTER_DOWN(5):非首个手指按下(多点触控)。
  • ACTION_POINTER_UP(6):非最后一个手指抬起(多点触控)。

历史数据(批量移动事件)

Android 为了提高效率,会将多个连续的 MOVE 事件打包为一个 MotionEvent,存储在 history 中:

int historySize = event.getHistorySize();
for (int i = 0; i < historySize; i++) {
float historicalX = event.getHistoricalX(i);
float historicalY = event.getHistoricalY(i);
long historicalTime = event.getHistoricalEventTime(i);
// 处理历史轨迹点
}

这对于手写、绘图等场景非常重要,可以获取更平滑的轨迹。

分发事件的组件

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

Activity → PhoneWindow → DecorView → ViewGroup → View
↑ ↑
存储关系 事件传递入口

从图上可看出,Activity 包括了 ViewGroup,ViewGroup 又可以包含多个 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 方法的。

OnTouchListener 与 OnClickListener 的执行顺序与优先级

这是一个经典的面试题,其答案藏在 View.dispatchTouchEvent 的源码中。

View.dispatchTouchEvent 源码分析

public boolean dispatchTouchEvent(MotionEvent event) {
// ...
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 步骤 1:先检查 OnTouchListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true; // OnTouchListener 消费了事件
}

// 步骤 2:如果 OnTouchListener 没有消费,才调用 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
// ...
return result;
}

关键结论:

  1. OnTouchListener 的优先级高于 onTouchEvent(包括 OnClickListener 和 OnLongClickListener)。
  2. 如果 OnTouchListener.onTouch 返回 true,则 onTouchEvent 不会被调用。这意味着 OnClickListener 也不会被触发。
  3. OnClickListener 在 onTouchEvent 的 ACTION_UP 处理中被触发,条件是 View 是可点击的且在按下时获取了焦点。

OnLongClickListener 与 OnTouchListener 的关系

在 View.onTouchEvent 中处理 ACTION_DOWN 时,会 post 一个延迟 Runnable 用于触发长按:

case MotionEvent.ACTION_DOWN:
// ...
// post 长按检测
if (mPendingCheckForLongPress == null) {
mPendingCheckForLongPress = new CheckForLongPress();
}
mPendingCheckForLongPress.rememberWindowAttachCount();
postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout());
break;

如果用户手指在长按超时前抬起(ACTION_UP),长按回调被取消,触发 OnClickListener。如果手指保持不动超过长按超时,触发 OnLongClickListener。如果 OnLongClickListener.onLongClick 返回 true,则在后续的 ACTION_UP 中不再触发 OnClickListener。

事件分发过程

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

    DOWN 事件进入

┌─ Activity.dispatchTouchEvent ─┐
│ ↓ │
│ PhoneWindow.superDispatchTouchEvent │
│ ↓ │
│ DecorView.superDispatchTouchEvent │
│ ↓ │
│ ViewGroup.dispatchTouchEvent ──┐│
│ ├─ onInterceptTouchEvent? ││
│ │ ├─ true → 拦截,自己处理 ││
│ │ └─ false → 分发给子View ││
│ ├─ 遍历子 View ││
│ │ └─ child.dispatchTouchEvent││
│ │ └─ ... (递归) ││
│ └─ onTouchEvent (如果无子View消费)│
└────────────────────────────────┘

分发方法: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 方法

接下来看 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 演示

上图 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

多点触控与 TouchTarget 链表

当两个手指同时触摸屏幕时,产生了两个 DOWN 事件(第一个手指产生 ACTION_DOWN,第二个手指产生 ACTION_POINTER_DOWN)。如果这两个手指分别落在两个不同的子 View 上,那么 mFirstTouchTarget 链表中就有两个节点,每个节点记录一个子 View。后续每个手指的 MOVE 和 UP 事件分别分发到对应的子 View。

滑动冲突解决方案

在嵌套滚动场景中(如 ScrollView 内嵌 RecyclerView,或 ViewPager 内嵌可横向滑动的 View),必然遇到滑动冲突。解决滑动冲突有两种核心方案。

外部拦截法(推荐)

父容器在 onInterceptTouchEvent 中根据业务逻辑决定是否拦截事件:

@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
float x = ev.getX();
float y = ev.getY();

switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 不能拦截 DOWN,否则子 View 收不到任何事件
intercepted = false;
mLastX = x;
mLastY = y;
break;

case MotionEvent.ACTION_MOVE:
float dx = Math.abs(x - mLastX);
float dy = Math.abs(y - mLastY);
// 当水平滑动距离大于垂直滑动距离时,父容器拦截事件
if (dx > dy) {
intercepted = true;
} else {
intercepted = false;
}
break;

case MotionEvent.ACTION_UP:
intercepted = false;
break;
}
return intercepted;
}

关键原则:ACTION_DOWN 绝对不能拦截,否则子 View 无法收到任何后续事件。

内部拦截法

子 View 通过 requestDisallowInterceptTouchEvent(true) 阻止父容器拦截:

// 子 View 的 onTouchEvent 或 dispatchTouchEvent 中
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 告诉父容器不要拦截
getParent().requestDisallowInterceptTouchEvent(true);
mLastX = ev.getX();
mLastY = ev.getY();
break;

case MotionEvent.ACTION_MOVE:
float dx = Math.abs(ev.getX() - mLastX);
float dy = Math.abs(ev.getY() - mLastY);
// 当垂直滑动距离较大时,允许父容器拦截
if (dy > dx) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.dispatchTouchEvent(ev);
}

requestDisallowInterceptTouchEvent 的实现

public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return; // 状态相同,不需要处理
}

if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}

// 递归通知上层父容器
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}

注意:requestDisallowInterceptTouchEvent 只对当前事件序列有效(从 DOWN 到 UP/CANCEL)。DOWN 事件到达时,FLAG_DISALLOW_INTERCEPT 会被自动重置。

NestedScrolling 机制

NestedScrolling 是 Android 5.0(API 21)引入的嵌套滚动机制,用于在嵌套的滑动容器之间协调滚动行为。

为什么需要 NestedScrolling

在 NestedScrolling 出现之前,嵌套滑动有严重的体验问题:当用户在子 View(如 RecyclerView)中向上滑动,到达子 View 的顶部后,父 View(如 AppBarLayout)不能自动开始滚动。用户必须抬起手指再重新滑动,体验割裂。

NestedScrolling 机制通过在父子之间建立双向通信,使滚动动量可以在两个容器之间无缝传递。

核心接口

// 子 View 实现:产生嵌套滚动
public interface NestedScrollingChild {
void setNestedScrollingEnabled(boolean enabled);
boolean isNestedScrollingEnabled();
boolean startNestedScroll(int axes);
void stopNestedScroll();
boolean dispatchNestedScroll(int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed, int[] offsetInWindow);
boolean dispatchNestedPreScroll(int dx, int dy, int[] consumed, int[] offsetInWindow);
boolean dispatchNestedFling(float velocityX, float velocityY, boolean consumed);
boolean dispatchNestedPreFling(float velocityX, float velocityY);
}

// 父 View 实现:消费嵌套滚动
public interface NestedScrollingParent {
boolean onStartNestedScroll(View child, View target, int nestedScrollAxes);
void onNestedScrollAccepted(View child, View target, int nestedScrollAxes);
void onStopNestedScroll(View target);
void onNestedScroll(View target, int dxConsumed, int dyConsumed,
int dxUnconsumed, int dyUnconsumed);
void onNestedPreScroll(View target, int dx, int dy, int[] consumed);
boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed);
boolean onNestedPreFling(View target, float velocityX, float velocityY);
int getNestedScrollAxes();
}

嵌套滚动流程

子 View (NestedScrollingChild) 开始滑动

startNestedScroll(SCROLL_AXIS_VERTICAL)

父 View (NestedScrollingParent)
↓ onStartNestedScroll → true (表示愿意参与嵌套滚动)
↓ onNestedScrollAccepted

子 View 每次滑动前:
↓ dispatchNestedPreScroll(dx, dy, consumed)
父 View 优先消费:
↓ onNestedPreScroll → 在 consumed[] 中返回消费量

子 View 消费剩余滚动量,然后:
↓ dispatchNestedScroll(consumedX, consumedY, unconsumedX, unconsumedY)
父 View 处理未消费部分:
↓ onNestedScroll

子 View 松手时的 fling:
↓ dispatchNestedPreFling(velocityX, velocityY)
父 View 优先消费 fling:
↓ onNestedPreFling → true (父 View 先 fling)
↓ dispatchNestedFling(velocityX, velocityY, consumedByParent)
父 View 消费剩余 fling:
↓ onNestedFling

滑动结束:
↓ stopNestedScroll()
父 View:
↓ onStopNestedScroll

NestedScrollingParent2 / NestedScrollingChild2(API 21+)

Android Support Library 26+ 引入了 NestedScrollingParent2/Child2,增加了 type 参数区分触摸滑动(TYPE_TOUCH)和非触摸滑动(TYPE_NON_TOUCH,如惯性滚动),使嵌套滚动控制更精细。

典型应用:CoordinatorLayout + AppBarLayout + RecyclerView

CoordinatorLayout 实现了 NestedScrollingParent,RecyclerView 实现了 NestedScrollingChild。当用户滑动 RecyclerView 时:

  1. RecyclerView 通知 CoordinatorLayout 要开始垂直滚动。
  2. CoordinatorLayout 在 onNestedPreScroll 中优先消费滚动量(驱动 AppBarLayout 收缩/展开)。
  3. 剩余的滚动量交给 RecyclerView 自身处理。
  4. 松手后的 fling 由 CoordinatorLayout 优先处理(先完成 AppBarLayout 的动画),再交给 RecyclerView。

小结

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

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

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

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

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

  • DOWN 事件的特殊:事件的起点;决定后续事件由谁来消费处理;
  • mFirstTouchTarget 作用:记录捕获消费 touch 事件的 View,是一个链表结构;
  • CANCEL 事件的触发场景:当父视图先不拦截,然后在 MOVE 事件中重新拦截,此时子 View 会接收到一个 CANCEL 事件;
  • OnTouchListener 优先级:高于 OnClickListener 和 OnLongClickListener;如果 OnTouchListener 返回 true,onTouchEvent(包括 OnClickListener)不会被调用。
  • 滑动冲突解决:外部拦截法(父 onInterceptTouchEvent 判断)和内部拦截法(子 requestDisallowInterceptTouchEvent 控制)。
  • NestedScrolling:通过 NestedScrollingChild 和 NestedScrollingParent 接口实现父子视图间的流畅嵌套滚动。

关键源码文件

文件 路径
View.java frameworks/base/core/java/android/view/View.java
ViewGroup.java frameworks/base/core/java/android/view/ViewGroup.java
ViewRootImpl.java frameworks/base/core/java/android/view/ViewRootImpl.java
DecorView.java frameworks/base/core/java/com/android/internal/policy/DecorView.java
PhoneWindow.java frameworks/base/core/java/com/android/internal/policy/PhoneWindow.java
MotionEvent.java frameworks/base/core/java/android/view/MotionEvent.java
InputReader.cpp frameworks/native/services/inputflinger/reader/InputReader.cpp
InputDispatcher.cpp frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
EventHub.cpp frameworks/native/services/inputflinger/reader/EventHub.cpp
打赏
  • 微信
  • 支付宝

评论