简介
本篇主要对事件分发中的基本概念做深入介绍,同时也介绍参与事件分发的主要方法。从这些方法的核心逻辑中结合源码,总结事件分发的原理。除了应用层的事件分发(从 DecorView 到子 View),本文还会追溯触摸事件从内核驱动到应用层的完整传递链路:InputReader → InputDispatcher → InputChannel → ViewRootImpl。
注意:不同版本间的代码会有区别,本文是基于 Android-30(Android 11)的源码进行分析。
触摸事件的完整传递链路:从内核到应用
在分析应用层的事件分发之前,有必要先理解触摸事件是如何从硬件最终到达 Activity 的。这个链路涉及多个系统服务。
1. 内核驱动层:触摸屏驱动
当用户手指触摸屏幕时,触摸屏硬件产生中断。内核中的触摸屏驱动读取触摸坐标和压力数据,通过 Linux 输入子系统(input subsystem)将事件写入 /dev/input/eventX 设备文件。
事件以 input_event 结构体形式输出:
struct input_event { |
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() { |
4. InputDispatcher:事件分发到目标窗口
源码位置:frameworks/native/services/inputflinger/dispatcher/InputDispatcher.cpp
InputDispatcher 运行在 InputDispatcherThread 中。它通过 InputChannel 将事件发送给目标应用窗口:
- 从 InputReader 接收
NotifyMotionArgs。 - 查找触摸坐标对应的目标窗口(通过 WindowManagerService 维护的窗口 Z-order 列表)。
- 将 MotionEvent 通过 InputChannel 的 socket 发送给目标窗口的 InputConsumer。
int32_t InputDispatcher::findTouchedWindowAtLocked(int32_t 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 { |
enqueueInputEvent → doProcessInputEvents → deliverInputEvent → 最终根据事件类型调用 processPointerEvent(MotionEvent)或 processKeyEvent(KeyEvent)。
private int processPointerEvent(QueuedInputEvent q) { |
完整链路图
[触摸屏硬件] |
思路分析
理解了完整传递链路后,应用层的分析聚焦在以下问题:
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 左上角的坐标 |
事件时间
// 事件发生的时间戳(uptimeMillis) |
多点触控相关
// 获取触控点数量 |
事件动作(Action)编码
MotionEvent 的 action 是一个复合整数值,低 8 位表示动作类型,高 8 位表示触控点索引:
int action = event.getAction(); |
动作类型常量:
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(); |
这对于手写、绘图等场景非常重要,可以获取更平滑的轨迹。
分发事件的组件
分发事件的组件,也称为分发事件者,这些包括 Activity、View、ViewGroup,三者的一般结构为:
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 之间的逻辑关系:
- 当前 Group 是否需要拦截 touch 事件
- 是否需要将 touch 事件继续分发给子 View
- 如何将 touch 事件分发给子 View
View
View 是页面上能呈现的最小组件单元,不可再细分,内部也不会存在子 View,所以该组件的事件分发重点在于当前 View 如何去处理 touch 事件,并根据相应的手势逻辑进行一系列的效果展示(比如滑动、放大、点击、长按等)。
所以这里面需要处理一些逻辑关系是:
- 是否存在 TouchListener;
- 是否自己接收处理 touch 事件(主要逻辑在 onTouchEvent 方法中)。
分发的核心方法
负责对事件进行分发的方法主要有三个,分别是:
dispatchTouchEvent() |
它们并不是全都存在于所负责分发的组件中,具体情况分析如下表中:
| 组件 | dispatchTouchEvent | onTouchEvent | onInterceptTouchEvent |
|---|---|---|---|
| Activity | 存在 | 存在 | 不存在 |
| ViewGroup | 存在 | 存在 | 存在 |
| View | 存在 | 存在 | 不存在 |
从表中可看出,dispatchTouchEvent 和 onTouchEvent 方法存在于上述三个组件中,而 onInterceptTouchEvent 单独为 ViewGroup 所拥有。
ViewGroup 类中,实际上是没有 onTouchEvent 方法,但是由于其继承自 View,而后者是拥有 onTouchEvent 方法的,因此 ViewGroup 也是可以调用 onTouchEvent 方法的。
OnTouchListener 与 OnClickListener 的执行顺序与优先级
这是一个经典的面试题,其答案藏在 View.dispatchTouchEvent 的源码中。
View.dispatchTouchEvent 源码分析
public boolean dispatchTouchEvent(MotionEvent event) { |
关键结论:
- OnTouchListener 的优先级高于 onTouchEvent(包括 OnClickListener 和 OnLongClickListener)。
- 如果 OnTouchListener.onTouch 返回 true,则 onTouchEvent 不会被调用。这意味着 OnClickListener 也不会被触发。
- OnClickListener 在
onTouchEvent的 ACTION_UP 处理中被触发,条件是 View 是可点击的且在按下时获取了焦点。
OnLongClickListener 与 OnTouchListener 的关系
在 View.onTouchEvent 中处理 ACTION_DOWN 时,会 post 一个延迟 Runnable 用于触发长按:
case MotionEvent.ACTION_DOWN: |
如果用户手指在长按超时前抬起(ACTION_UP),长按回调被取消,触发 OnClickListener。如果手指保持不动超过长按超时,触发 OnLongClickListener。如果 OnLongClickListener.onLongClick 返回 true,则在后续的 ACTION_UP 中不再触发 OnClickListener。
事件分发过程
这一章节是本文的核心,具体流程介绍如下。
DOWN 事件进入 |
分发方法:dispatchTouchEvent
从方法名可看出该方法的主功能就是负责分发,这是 Android 事件分发过程中的核心。事件是如何传递的,主要是看该方法,理解此方法,也就吃透了 Android 事件分发机制。
在了解该方法的核心机制之前,需要知道这样一个结论:
如果某个组件的该方法返回
TRUE,则表示该组件已对该事件进行了处理,即不会继续调用其他组件的事件分发方法,也就停止了分发。如果某个组件的该方法返回
FALSE,则表示该组件不能对该事件进行处理,需要按照规则继续分发事件,在不覆写该方法的情况下,除了一些特殊组件(系统已做过处理组件),其余组件默认都是返回false。
结合源码分析核心逻辑:
Activity 的 dispatchTouchEvent 方法
public boolean dispatchTouchEvent(MotionEvent ev) { |
这里 Activity 并没有通过自身来判断是否有子 View 消费分发事件,而是交给它依附的 Window 对象来处理,但是在 Window 对象里,该方法是一个抽象方法,实际上它交给了它的具体对象 PhoneWindow 处理的;
PhoneWindow 核心代码:
|
DecorView 核心代码:
public boolean superDispatchTouchEvent(MotionEvent 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 方法的核心逻辑代码(去掉非重点代码):
|
dispatch 主要分为 3 大步骤:
步骤1:判断当前 ViewGroup 是否需要拦截此 touch 事件,如果拦截则此次 touch 事件不再传递给子 View(或者以 CANCEL 的方式通知子 View)。
步骤2:如果没有拦截,则将事件分发给子 View 继续处理,如果子 View 将此次事件捕获,则将
mFirstTouchTarget赋值给捕获 touch 事件的 View。步骤3:根据
mFirstTouchTarget重新分发事件。
如果步骤1中,当前 ViewGroup 并没有对事件进行拦截,则执行步骤2。
事件分发流程 demo 演示
上图 DOWN 事件中,DownInterceptGroup 的 onInterceptTouchEvent 被触发一次;
然后在子 View CaptureTouchView 的 dispatchTouchEvent 中返回 true,代表它消费了这个 DOWN 事件。
这样 CaptureTouchView 会被添加到父视图(DownInterceptGroup)中的 mFirstTouchTarget 中。
因此后续的 MOVE 和 UP 事件都会经过 DownInterceptGroup 的 onInterceptTouchEvent 进行拦截判断。
为什么 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 { |
可以看出 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 中根据业务逻辑决定是否拦截事件:
|
关键原则:ACTION_DOWN 绝对不能拦截,否则子 View 无法收到任何后续事件。
内部拦截法
子 View 通过 requestDisallowInterceptTouchEvent(true) 阻止父容器拦截:
// 子 View 的 onTouchEvent 或 dispatchTouchEvent 中 |
requestDisallowInterceptTouchEvent 的实现
public void requestDisallowInterceptTouchEvent(boolean 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 实现:产生嵌套滚动 |
嵌套滚动流程
子 View (NestedScrollingChild) 开始滑动 |
NestedScrollingParent2 / NestedScrollingChild2(API 21+)
Android Support Library 26+ 引入了 NestedScrollingParent2/Child2,增加了 type 参数区分触摸滑动(TYPE_TOUCH)和非触摸滑动(TYPE_NON_TOUCH,如惯性滚动),使嵌套滚动控制更精细。
典型应用:CoordinatorLayout + AppBarLayout + RecyclerView
CoordinatorLayout 实现了 NestedScrollingParent,RecyclerView 实现了 NestedScrollingChild。当用户滑动 RecyclerView 时:
- RecyclerView 通知 CoordinatorLayout 要开始垂直滚动。
- CoordinatorLayout 在
onNestedPreScroll中优先消费滚动量(驱动 AppBarLayout 收缩/展开)。 - 剩余的滚动量交给 RecyclerView 自身处理。
- 松手后的 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 |

