1. 点击事件传递链路
事件并非凭空产生直接到达 View,而是经过了一个跨越内核、系统服务与应用进程的漫长旅程。
1.1 源头溯源:系统层
事件在触达应用层 Java 代码前,需在系统底层完成"硬件-内核-服务-应用"的跨进程接力:
触摸信号由内核驱动捕获并写入文件节点,经由 SystemServer 中的 InputReader 读取与 InputDispatcher 分发,最终通过 Socket 跨进程传输至应用进程的 ViewRootImpl,从而正式激活 Android 应用层的事件分发体系。
1.2 核心组件的角色定位
进入应用层后,Android UI 系统的事件传递物理载体由三层核心组件协同构成:
- Activity:作为业务入口持有 Window。
- Window:作为系统桥梁管理 DecorView。
- DecorView:作为视图根节点承载具体的交互执行。
点击阅读:Android 窗口显示(一)------ Activity、Window 和 View 之间的联系
1.3 点击事件传递链路:U 型流转的闭环
点击事件的传递并非单向流动,而是一个基于递归调用的完整闭环,形成了的U型结构,这是 Android 事件分发机制的核心骨架。
1.3.1 下沉阶段(分发与拦截)
Activity <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ PhoneWindow <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ DecorView (根 ViewGroup) <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ 子 ViewGroup <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ 目标 View
当手指触摸屏幕,硬件信号被封装为 Event 后,首先传递给 Activity。随后,事件经由 PhoneWindow 传至 DecorView。DecorView 作为根 ViewGroup,开始自顶向下地分发事件,寻找能处理该事件的目标 View。
1.3.2 上浮阶段(消费与回溯)
目标 View <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ 子 ViewGroup <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ DecorView <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ PhoneWindow <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ Activity
当事件到达底层目标 View 后,或者在分发过程中没有 View 消费事件,事件会像气泡一样自底向上回传给父容器,直至回到 Activity 的 onTouchEvent 进行兜底处理。
2. 责任链模式下的 U 型流转
Android 事件分发本质上是责任链模式。它不仅仅是线性的传递,而是基于视图树结构的递归调用与回溯。
2.1 责任链的构建与分工
在标准的责任链模式中,多个对象有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。
在事件分发机制中,这一模式被适配为"分发-拦截-消费"的三级体系:
- 链的节点 :每个
View和ViewGroup都是链上的一个节点。 - 传递机制:事件在视图树中传递,直到被某个节点消费或者回传至根节点。
- 双向流动:不同于标准责任链的单向传递,Android 提供了下沉(分发)与上浮(回溯)的双向通道,确保事件能被精准捕获或兜底处理。
Android 事件分发机制是一个由 dispatchTouchEvent 驱动的责任链,通过 onInterceptTouchEvent 作为控制阀决定是否截断向下传递,并由 onTouchEvent 作为处理者最终决定是消耗事件终止链条,还是向上级回溯任务。
核心方法:
先明确这三个方法在 Activity、ViewGroup 和 View 中的存在情况:
| 方法 | Activity | ViewGroup | View | 作用 |
|---|---|---|---|---|
| dispatchTouchEvent | ✅ | ✅ | ✅ | 分发事件。只要事件传到了该组件,此方法必被调用。 |
| onInterceptTouchEvent | ❌ | ✅ | ❌ | 拦截事件。决定是否把事件拦下自己处理。 |
| onTouchEvent | ✅ | ✅ | ✅ | 处理事件。消耗(Consume)点击或滑动动作。 |
为什么只有viewgroup有onInterceptTouchEvent方法,为什么这么设计?
Android 事件分发机制本质上是一次基于责任链模式的 U 型完整流转:
当触摸发生时,事件由 Activity 始发,经 PhoneWindow 传至 DecorView,随后开启 自顶向下 的分发链路,期间 ViewGroup 通过 dispatchTouchEvent 分发并利用 onInterceptTouchEvent 判定是否拦截;一旦到达底层 View 或被拦截,链路即刻反转为 自底向上 的回溯,通过 onTouchEvent 逐级尝试消费,若最终无 View 响应,事件将回流至 Activity 兜底,形成从系统层到视图树再回归逻辑层的完整闭环。
3. 源码解析
3.1 Activity 的分发
硬件驱动 (Kernel) <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ SystemServer (InputReader/InputDispatcher) <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ ViewRootImpl <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ DecorView <math xmlns="http://www.w3.org/1998/Math/MathML"> → \rightarrow </math>→ Activity
InputEventReceiver 类的 dispatchInputEvent 方法:
java
private void dispatchInputEvent(int seq, InputEvent event) {
mSeqMap.put(event.getSequenceNumber(), seq);
// 调用子类的实现
onInputEvent(event);
}
它接收 Native 层传递过来的 InputEvent 对象,并将其分发给子类(如 ViewRootImpl.WindowInputEventReceiver)。
所以最终执行到了:
java
// ViewRootImpl.java
final class WindowInputEventReceiver extends InputEventReceiver {
@Override
public void onInputEvent(InputEvent event) {
// ...
enqueueInputEvent(event, this, 0, true); // 将事件加入队列
}
}
enqueueInputEvent 会调用 doProcessInputEvents ,开始处理事件队列:
java
// ViewRootImpl.java
void doProcessInputEvents() {
while (mPendingInputEventHead != null) {
// ...
deliverInputEvent(q); // 开始分发事件
}
}
deliverInputEvent 会将事件交给责任链的第一个节点 mFirstInputStage 。这个责任链在 setView 方法中初始化:
java
// ViewRootImpl.java
private void deliverInputEvent(QueuedInputEvent q) {
stage.deliver(q);
}
java
// ViewRootImpl.java -> setView()
// 初始化 InputStage 责任链
// 1. NativePreImeInputStage
// 2. ViewPreImeInputStage
// 3. ImeInputStage (输入法)
// 4. EarlyPostImeInputStage
// 5. NativePostImeInputStage
// 6. ViewPostImeInputStage (关键节点)
// 7. SyntheticInputStage
mFirstInputStage = nativePreImeStage;
事件会依次经过这些 Stage。如果前面的 Stage 没有消费事件(例如 IME 没有拦截),事件最终会到达 ViewPostImeInputStage 。
ViewPostImeInputStage 的 onProcess 方法会根据事件类型调用不同的处理逻辑。对于触摸事件(MotionEvent),它会调用 processPointerEvent :
java
// ViewRootImpl.java
final class ViewPostImeInputStage extends InputStage {
@Override
protected int onProcess(QueuedInputEvent q) {
if (q.mEvent instanceof KeyEvent) {
return processKeyEvent(q);
} else {
// ...
if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
return processPointerEvent(q);
}
// ...
}
}
private int processPointerEvent(QueuedInputEvent q) {
final MotionEvent event = (MotionEvent)q.mEvent;
// ...
// mView 是 DecorView
// 这里调用 DecorView.dispatchPointerEvent
boolean handled = mView.dispatchPointerEvent(event);
// ...
return handled ? FINISH_HANDLED : FORWARD;
}
}
mView 在 ViewRootImpl 中就是 DecorView 。 DecorView 继承自 View , dispatchPointerEvent 是 View 类的方法:
java
// View.java
public final boolean dispatchPointerEvent(MotionEvent event) {
if (event.isTouchEvent()) {
return dispatchTouchEvent(event); // 这里的 dispatchTouchEvent 是 DecorView 重写的版本
} else {
return dispatchGenericMotionEvent(event);
}
}
java
public boolean dispatchTouchEvent(MotionEvent ev) {
// 获取 Window 的回调接口,通常是 Activity 实例
final Window.Callback cb = mWindow.getCallback();
// 优先将事件分发给 Activity 处理
// 条件:1) 回调存在 2) 窗口未销毁 3) 不是子窗口(mFeatureId < 0 表示主窗口)
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
通过 mWindow.getCallback() 获取到 Window 的控制者,它的老大,Activity。最终调用至 Activity 的 dispatchTouchEvent 方法。
DecorView 将事件上报给 Activity ,让 Activity 有机会在 View 处理之前拦截或处理。
Activity 的 dispatchTouchEvent 非常简单:
java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 屏保或系统交互回调
}
// 委托给 Window 处理 -> PhoneWindow -> DecorView
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 如果所有 View 都不消费,最终由 Activity 兜底处理
return onTouchEvent(ev);
}
如果 Window 的 superDispatchTouchEvent 返回 true,表示事件被消费,循环结束。
重写 Activity 的 dispatchTouchEvent 方法的一些场景:点击空白处隐藏软键盘、全局防抖、应用长时间无操作检测、全局埋点与手势分析等。
3.2 ViewGroup 的拦截与分发 (机制核心)
这是整个机制中最复杂的部分。ViewGroup 既要决定是否拦截,又要遍历子 View 寻找消费者。
Activity → PhoneWindow → DecorView (根 ViewGroup) → 子 ViewGroup → 目标 View
Activity 收到事件后,委托给 PhoneWindow 处理:
java
public boolean superDispatchTouchEvent(MotionEvent event) {
// DecorView 是 Window 对应的 RootView,继承自 FrameLayout
return mDecor.superDispatchTouchEvent(event);
}
事件回到 DecorView ,但这次调用的是 super 方法,进入标准的 ViewGroup 分发流程:
java
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
// 调用父类 FrameLayout 的 dispatchTouchEvent,启动标准的 View 事件分发流程
return super.dispatchTouchEvent(event);
}
A. 拦截逻辑 (Intercept)
在 ViewGroup.dispatchTouchEvent 中,若是新事件(DOWN)或已有目标(mFirstTouchTarget),且未被禁止(disallowIntercept),则调 onInterceptTouchEvent;否则(非DOWN且无目标)默认拦截:
java
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
//...
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 新的触摸序列开始 (ACTION_DOWN)
// 1. 清除之前的 TouchTargets (mFirstTouchTarget置空)
// 2. 重置触摸状态 (包括 FLAG_DISALLOW_INTERCEPT)
cancelAndClearTouchTargets(ev);
resetTouchState();
}
final boolean intercepted;
// 拦截判断的核心条件:
// 1. 一个新的事件序列开始
// 2. 如果 mFirstTouchTarget != null,说明已经有子 View 消费了该序列的 ACTION_DOWN,
// 后续的 MOVE/UP 事件也需要再次判断是否拦截
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 子 View 可以通过 requestDisallowInterceptTouchEvent(true) 设置 FLAG_DISALLOW_INTERCEPT
// 从而强制父容器跳过 onInterceptTouchEvent 的调用,直接不拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 调用 onInterceptTouchEvent 询问是否拦截
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
// 如果不是 DOWN 事件,且 mFirstTouchTarget 为空
// 说明 DOWN 事件时 ViewGroup 拦截了,或者没有子 View 消费
// 此时直接默认拦截后续事件,不再调用 onInterceptTouchEvent
intercepted = true;
}
// ...
// [流程解释] 如果事件未取消且未拦截,则开始分发给子 View
if (!canceled && !intercepted) {
}
}
}
mFirstTouchTarget 是 ViewGroup 中用于记录当前触摸事件序列(如 ACTION_DOWN 之后)中成功消费了事件的子 View 链表 ,它的存在使得后续事件(如 ACTION_MOVE、ACTION_UP)可以直接分发给目标 View 而无需重新遍历查找,从而提升分发效率。
场景 A:ACTION_DOWN :事件序列的开始,一定会进入拦截判断逻辑,默认调用 onInterceptTouchEvent 方法,如果返回 true,则 ViewGroup 进行拦截,不会将事件序列分发至子 View。子 View 可调用 requestDisallowInterceptTouchEvent(true) 设置 FLAG_DISALLOW_INTERCEPT,这样会强制禁止 ViewGroup 进行拦截。
场景 B:ACTION_MOVE 且已有子 View 捕获 :此时 mFirstTouchTarget!= null,表示该事件序列的 ACTION_DOWN 事件已经分发,有子 View 成功消费了之前的事件 (DOWN),那么后续事件一定会进入拦截判断逻辑。父 View 依然有机会在后续的 MOVE 过程中"反悔",通过 onInterceptTouchEvent 返回 true 来切断子 View 的事件流(此时子 View 会收到 CANCEL)。
场景 C:ACTION_MOVE 但无子 View 捕获 :即 mFirstTouchTarget == null,且不是 DOWN。代码进入 else 分支,intercepted = true。这是一种优化:既然没有子 View 在处理这个手势(意味着之前所有的子 View 在 DOWN 时都返回了 false,或者父 View 已经在 DOWN 时拦截了),那么后续事件毫无疑问应该由父 View 自己处理,无需再询问是否拦截 。
onInterceptTouchEvent 中不应进行复杂的业务逻辑,实际上,该方法应仅用于状态检测 (如:检测滑动距离是否超过 TouchSlop)。真正的业务处理(如滚动视图)应在 onTouchEvent 中进行.
B. 寻找目标子 View (Targeting)
如果事件没有被取消,也没有被拦截,且是 DOWN 事件,ViewGroup 就开始"招募"处理者,准备向下分发:
java
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
//...
if (onFilterTouchEventForSecurity(ev)) {
// ...
// [流程解释] 如果事件未取消且未拦截,则开始分发给子 View
if (!canceled && !intercepted) {
// ...
if (actionMasked == MotionEvent.ACTION_DOWN || ...) {
// 1. 获取子 View 列表(按照 Z-order 排序,最后绘制的在最上面)
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
// 2. 倒序遍历(从最上层 View 开始问)
for (int i = childrenCount - 1; i >= 0; i--) {
// ... 获取 child ...
// 3. 门槛检查:View 可见?动画没播放完?点击坐标在 View 内部?
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue; // 不满足条件,找下一个
}
// 4. 尝试分发!(dispatchTransformedTouchEvent 内部调用 child.dispatchTouchEvent)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 5. 找到了!子 View 返回了 true
// 6. 【关键】记录这个子 View 到 mFirstTouchTarget 链表中
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break; // 找到了就停止遍历!优化性能
}
}
}
}
}
}
为了确保点击的最上层 View 能优先接收到事件,需要通过 buildTouchDispatchChildList 获取一个按 Z 轴排序的子 View 列表:
java
// ViewGroup.java
public ArrayList<View> buildTouchDispatchChildList() {
// 实际调用构建有序列表的方法
return buildOrderedChildList();
}
ArrayList<View> buildOrderedChildList() {
final int childrenCount = mChildrenCount;
// [优化] 如果没有子 View 设置 Z 值,不需要排序,直接返回 null
if (childrenCount <= 1 || !hasChildWithZ()) return null;
// [优化] 复用列表,避免内存抖动
if (mPreSortedChildren == null) {
mPreSortedChildren = new ArrayList<>(childrenCount);
} else {
mPreSortedChildren.clear();
}
// ViewGroup 的子 View 绘制顺序是否是自定义的
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// 获取子 View 索引(支持自定义绘制顺序 getChildDrawingOrder)
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
// 获取到子 View 对象
final View nextChild = mChildren[childIndex];
// 获取当前子 View 的 Z 值(Z = elevation + translationZ)
final float currentZ = nextChild.getZ();
// [步骤2] 插入排序 (Insertion Sort)
// 寻找插入位置:保持列表按 Z 值从小到大排序
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
// 将当前 View 插入到计算出的最终位置
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
继续看 dispatchTouchEvent 方法:
java
// ViewGroup.java
for (int i = childrenCount - 1; i >= 0; i--) {
// [获取 View] 如果有排序列表,从列表中取;否则直接取 mChildren 数组
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
// [命中测试] 1. View 可见 2. 触摸点在 View 范围内
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// [分发] 尝试将事件交给子 View 处理
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ... 子 View 消费了事件,记录 Target 并跳出循环
newTouchTarget = addTouchTarget(child, idBitsToAssign); // [关键] 添加到 TouchTarget 链表头
alreadyDispatchedToNewTouchTarget = true; // [标记] 标记已分发
break;
}
}
C. 后续分发
java
// Case A: 没人处理 (mFirstTouchTarget 为空)
if (mFirstTouchTarget == null) {
// 可能是之前拦截了,也可能是所有子 View 都不在点击范围内,或者是子 View 的 onTouchEvent 都返回了 false。
// 没有子 View 消费事件 (mFirstTouchTarget 为 null)
// 此时 ViewGroup 自己处理事件,调用 dispatchTransformedTouchEvent 时 child 传 null
// 内部会调用 super.dispatchTouchEvent(ev),即 View.dispatchTouchEvent (调用 onTouch/onTouchEvent)
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {// Case B: 有人处理 (mFirstTouchTarget 不为空)
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// 如果是刚才在"第二阶段"刚找到的那个 child,且已经发过 DOWN 了,就别发了,直接标记 handled = true
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 这是一个 MOVE 或 UP 事件,或者是多点触控的其他手指
// 直接把事件分发给 target.child,【不需要】再遍历子 View 树了!
final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) {
handled = true;
}
// 如果父容器此时突然拦截 (intercepted = true),cancelChild 会变为 true
// 下一次循环,链表就会被清理,mFirstTouchTarget 变回 null
}
target = next;
}
}
- O(1) 的分发效率 :对于
MOVE和UP,代码直接进入else块,直接拿到mFirstTouchTarget进行分发,跳过了宽大的for循环遍历。这就是为什么 Android 事件分发效率高的原因。
D. 收尾
java
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState(); // 清除 mFirstTouchTarget,重置标记位
}
当手指抬起或事件取消时,整个链路状态清零,为下一次点击做准备。
3.3 View 的消费逻辑
java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 如果是取消事件 (cancel=true 或 ACTION_CANCEL),直接分发取消操作,不需要坐标转换
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
// [关键调用] 如果没有 child (即 ViewGroup 自己处理),调用父类的 dispatchTouchEvent (View.dispatchTouchEvent)
handled = super.dispatchTouchEvent(event);
} else {
// [关键调用] 调用子 View 的 dispatchTouchEvent
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction); // 恢复 Action
return handled;
}
// Calculate the number of pointers to deliver.
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// If for some reason we ended up in an inconsistent state where it looks like we
// might produce a motion event with no pointers in it, then drop the event.
if (newPointerIdBits == 0) {
return false;
}
// If the number of pointers is the same and we don't need to perform any fancy
// irreversible transformations, then we can reuse the motion event for this
// dispatch as long as we are careful to revert any changes we make.
// Otherwise we need to make a copy.
// [源码细节] 优化:如果 Pointer 数量没变,尽量重用 MotionEvent 对象,避免创建新对象
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
if (child == null) {
handled = super.dispatchTouchEvent(event); // [关键] ViewGroup 自己处理
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY); // [坐标转换] 转换到子 View 坐标系
handled = child.dispatchTouchEvent(event); // [关键] 分发给子 View
event.offsetLocation(-offsetX, -offsetY); // [恢复] 恢复坐标
}
return handled;
}
transformedEvent = MotionEvent.obtain(event); // 需要变换矩阵,复制一份事件
} else {
transformedEvent = event.split(newPointerIdBits); // 需要拆分 Pointer,复制一份事件
}
// 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()); // 应用矩阵变换
}
handled = child.dispatchTouchEvent(transformedEvent); // [关键] 分发给子 View
}
// Done.
transformedEvent.recycle(); // 回收临时事件
return handled;
}
java
public boolean dispatchTouchEvent(MotionEvent event) {
// If the event should be handled by accessibility focus first.
if (event.isTargetAccessibilityFocus()) {
// We don't have focus or no virtual descendant has it, do not handle the event.
if (!isAccessibilityFocusedViewOrHost()) {
return false;
}
// We have focus and got the event, then use normal event dispatch.
event.setTargetAccessibilityFocus(false);
}
boolean result = false;
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
// Defensive cleanup for new gesture
stopNestedScroll();
}
// <p>
// [流程解释] View 的事件分发逻辑
// 1. 优先调用 OnTouchListener (如果设置了且 View 是 Enabled 的)
// 2. 如果 OnTouchListener 返回 false,再调用 onTouchEvent
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (!result && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
}
// Clean up after nested scrolls if this is the end of a gesture;
// also cancel it if we tried an ACTION_DOWN but we didn't want the rest
// of the gesture.
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
先判断 OnTouchListener.onTouch(),若返回 true 则消费事件,结束分发;若返回 false,则调用 onTouchEvent()。
onTouchEvent: 核心事件处理 (点击、长按等):
java
public boolean onTouchEvent(MotionEvent event) {
// [面试高频考点] CLICKABLE 属性对事件消费的影响
// 只要 View 是 CLICKABLE、LONG_CLICKABLE 或 CONTEXT_CLICKABLE 的,
// onTouchEvent 就会返回 true,表示消费了事件,无论是否实际执行了点击逻辑
// [源码细节] 哪怕 View 是 DISABLED (不可用) 状态,只要它是 CLICKABLE 的,它依然会消费事件(返回 true),只是不响应动作。
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE ...)
// ...
// ACTION_UP: 处理点击事件
case MotionEvent.ACTION_UP:
// ...
// [源码细节] 执行点击回调
// performClick() 会调用 OnClickListener.onClick()
// 这是一个 Post 操作,保证了 UI 状态更新优先于点击回调
// [面试高频考点] onClick 是在哪里调用的?
// 答:在 onTouchEvent 的 ACTION_UP 中,且没有触发长按,且处于按下状态时调用。
performClickInternal();
// ACTION_DOWN: 处理按下反馈和长按检测
case MotionEvent.ACTION_DOWN:
// [源码细节] 延迟反馈 (Scrolling Container)
// 如果 View 在滚动容器中 (如 ListView/ScrollView),ACTION_DOWN 不会立即显示按下状态 (Pressed),
// 而是会延迟一段时间 (TapTimeout)。这是为了区分用户是想点击还是想滑动。
// 如果在 TapTimeout 内发生了滑动 (ACTION_MOVE),则取消点击判定。
if (isInScrollingContainer) {
// ...
} else {
setPressed(true, x, y);
// [源码细节] 检查长按
// 发送一个延迟消息,如果在 LongPressTimeout 时间内没有 UP 或 CANCEL,则触发 onLongClick
checkForLongClick(...)
}
// ACTION_MOVE: 处理移出 View
case MotionEvent.ACTION_MOVE:
if (!pointInView(x, y, touchSlop)) {
// [源码细节] 如果手指移出了 View 的范围 (加上 touchSlop 容差)
// 则取消点击和长按检测,并将 Pressed 状态置为 false
removeTapCallback();
removeLongPressCallback();
// ...
}
onClick 是在 onTouchEvent 的 ACTION_UP 中被触发的。
- 优先级 :
OnTouchListener.onTouch>onTouchEvent>OnClickListener.onClick。 - PerformClick :在
ACTION_UP中,如果是非长按且在 View 区域内,会调用performClick(),进而回调onClick。 - Clickable 的影响 :只要 View 是
CLICKABLE或LONG_CLICKABLE的(Button 默认为 true,ImageView 默认为 false),onTouchEvent就会返回true,表示消费事件。
Android 将触控回调拆分为 OnTouchListener / onTouchEvent / OnClickListener ,本质是对 "事件拦截" / "事件处理" / "语义回调" 的分层解耦设计:
- onTouchListener:外部可插拔的拦截层
- onTouchEvent:View 内部的触控状态机
- onClick:高层语义结果回调