Android 事件分发机制(一)—— 全流程源码解析

1. 点击事件传递链路

事件并非凭空产生直接到达 View,而是经过了一个跨越内核、系统服务与应用进程的漫长旅程。

1.1 源头溯源:系统层

事件在触达应用层 Java 代码前,需在系统底层完成"硬件-内核-服务-应用"的跨进程接力:

触摸信号由内核驱动捕获并写入文件节点,经由 SystemServer 中的 InputReader 读取与 InputDispatcher 分发,最终通过 Socket 跨进程传输至应用进程的 ViewRootImpl,从而正式激活 Android 应用层的事件分发体系。

1.2 核心组件的角色定位

进入应用层后,Android UI 系统的事件传递物理载体由三层核心组件协同构成:

  1. Activity:作为业务入口持有 Window。
  2. Window:作为系统桥梁管理 DecorView。
  3. 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 责任链的构建与分工

在标准的责任链模式中,多个对象有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。

在事件分发机制中,这一模式被适配为"分发-拦截-消费"的三级体系:

  1. 链的节点 :每个 ViewViewGroup 都是链上的一个节点。
  2. 传递机制:事件在视图树中传递,直到被某个节点消费或者回传至根节点。
  3. 双向流动:不同于标准责任链的单向传递,Android 提供了下沉(分发)与上浮(回溯)的双向通道,确保事件能被精准捕获或兜底处理。

Android 事件分发机制是一个由 dispatchTouchEvent 驱动的责任链,通过 onInterceptTouchEvent 作为控制阀决定是否截断向下传递,并由 onTouchEvent 作为处理者最终决定是消耗事件终止链条,还是向上级回溯任务。

核心方法:

先明确这三个方法在 ActivityViewGroupView 中的存在情况:

方法 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) 的分发效率 :对于 MOVEUP,代码直接进入 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 是在 onTouchEventACTION_UP 中被触发的。

  1. 优先级OnTouchListener.onTouch > onTouchEvent > OnClickListener.onClick
  2. PerformClick :在 ACTION_UP 中,如果是非长按且在 View 区域内,会调用 performClick(),进而回调 onClick
  3. Clickable 的影响 :只要 View 是 CLICKABLELONG_CLICKABLE 的(Button 默认为 true,ImageView 默认为 false),onTouchEvent 就会返回 true,表示消费事件。

Android 将触控回调拆分为 OnTouchListener / onTouchEvent / OnClickListener ,本质是对 "事件拦截" / "事件处理" / "语义回调" 的分层解耦设计:

  • onTouchListener:外部可插拔的拦截层
  • onTouchEvent:View 内部的触控状态机
  • onClick:高层语义结果回调
相关推荐
峥嵘life3 小时前
2026 Android EDLA 认证相关资源网址汇总(持续更新)
android·java·学习
kkk_皮蛋3 小时前
在移动端使用 WebRTC (Android/iOS)
android·ios·webrtc
淘源码d3 小时前
上门家政源码,基于Java/SpringBoot和Uniapp的全栈家政预约平台,支持多端适配(小程序/H5/APP)
java·vue.js·源码·家政·家政小程序源码·上门家政·家政平台
aqi004 小时前
FFmpeg开发笔记(九十六)采用Kotlin+Compose的视频编辑器OpenVideoEditor
android·ffmpeg·kotlin·音视频·流媒体
Dream it possible!4 小时前
LeetCode 面试经典 150_分治_将有序数组转换为二叉搜索树(105_108_C++_简单)(递归)
c++·leetcode·面试
鱼鱼块4 小时前
告别重复传参!用柯里化提升代码优雅度
前端·javascript·面试
风止何安啊4 小时前
那些让你 debug 到凌晨的陷阱,我帮你踩平了:React Hooks 避坑指南
前端·react.js·面试
a程序小傲4 小时前
得物Java面试被问:Fork/Join框架的使用场景
java·开发语言·面试
阿蒙Amon4 小时前
C#每日面试题-简述可空类型
microsoft·面试·c#