3. 事件分发机制:触摸事件的分发流程

触摸偏移的问题,追根溯源,往往出在事件分发这个环节。说白了,就是系统把触摸信号一层层往下传,传到哪个 View 手里,哪个 View 来处理。如果这个流程出了问题,偏移就来了。

我个人习惯把事件分发比作一个「三层漏斗」:Activity → ViewGroup → View。每一层都有机会拦截或者消费事件。今天我们就把这个漏斗拆开来看。

核心结论:事件分发的本质是「责任链」模式。每一层都有两次机会处理事件——一次在分发前,一次在分发后。搞懂这个,你就能精准定位触摸偏移的根因。

3.1 三层架构:事件从哪来,到哪去?

先看整体流程。我画了一张图,帮你建立全局认知。

触摸事件三层分发流程 Activity dispatchTouchEvent() ViewGroup dispatchTouchEvent() onInterceptTouchEvent() onTouchEvent() View dispatchTouchEvent() onTouchEvent() 事件未被消费时向上回传 💡 关键点: 每一层都先调用 dispatchTouchEvent(),再决定是否拦截或消费。 事件从 Activity 自上而下传递,如果没人消费,则自下而上回传。

嗯,这张图你得记住。后面所有调试技巧都围绕这个流程展开。

3.2 dispatchTouchEvent:分发的总入口

每一层都有 dispatchTouchEvent()。它是事件分发的「总调度员」。你想想看,系统把 MotionEvent 扔过来,它来决定:

  • 是自己处理?
  • 还是传给子 View?
  • 或者干脆不处理?

返回值是个 boolean:true 表示事件被消费了,false 表示没人要。

我的调试习惯:遇到触摸偏移,我第一件事就是在 dispatchTouchEvent() 里打日志。看事件到底走到了哪一层,有没有被提前拦截。这一步能筛掉 60% 的问题。

3.3 onInterceptTouchEvent:ViewGroup 的「安检门」

这个方法是 ViewGroup 独有的。View 没有它。

它的作用很简单——决定要不要拦截当前事件。返回 true,事件就扣在自己手里,不再往下传;返回 false,放行给子 View。

我曾经在一个嵌套滑动项目里踩过坑:外层 ViewGroup 的 onInterceptTouchEvent() 在某些边界条件下返回了 true,导致内层 RecyclerView 完全收不到滑动事件。用户一滑,整个页面跟着动,偏移得一塌糊涂。

⚠️ 注意:一旦 ViewGroup 拦截了事件(返回 true),后续的 MOVE、UP 事件都不会再调用 onInterceptTouchEvent(),直接交给 ViewGroup 自己的 onTouchEvent()。这是很多「滑动冲突」的根源。

3.4 onTouchEvent:事件的最终消费者

事件传到这一层,要么消费,要么放弃。返回 true 表示「我处理了」,返回 false 表示「我不处理,往上抛」。

这里有个细节:如果 View 的 onTouchEvent() 返回 false,事件会回传给父 ViewGroup 的 onTouchEvent()。这就是所谓的「事件回传机制」。

我见过不少新手在自定义 View 里,onTouchEvent() 返回了 super.onTouchEvent(),但父类默认返回 false,结果事件被吞了,点击没反应。嗯,这种问题排查起来最费时间。

3.5 三个方法的协作关系

我把它们的调用顺序和返回值影响整理成了一张表,你调试时可以直接对照:

方法 所属类 返回 true 的含义 返回 false 的含义
dispatchTouchEvent() Activity / ViewGroup / View 事件被消费,分发结束 事件未被消费,向上回传
onInterceptTouchEvent() ViewGroup 独有 拦截事件,交给自己的 onTouchEvent 放行事件,传给子 View
onTouchEvent() ViewGroup / View 消费事件,不再回传 不消费,事件回传给父 ViewGroup

3.6 一个典型场景:为什么点击位置偏了?

假设你有一个自定义 ViewGroup,里面放了一个 Button。你点击 Button,但响应位置偏了 20 像素。怎么回事?

我遇到过类似的情况。排查后发现:ViewGroup 的 onInterceptTouchEvent() 在 DOWN 事件时返回了 false,但在 MOVE 事件时返回了 true。结果 Button 收到了 DOWN,却没收到 MOVE 和 UP。Button 的点击区域被「截胡」了,视觉上就感觉点偏了。

解决方案?要么在 onInterceptTouchEvent() 里统一返回 false,要么在 onTouchEvent() 里做坐标补偿。

调试口诀:

  • 事件没到目标 View → 检查父 ViewGroup 的 onInterceptTouchEvent()
  • 事件到了但没响应 → 检查目标 View 的 onTouchEvent() 返回值
  • 事件响应但位置不对 → 检查坐标转换,或者父 ViewGroup 是否在 MOVE 时拦截了

3.7 代码示例:打印事件分发日志

这是我最常用的调试手段。在自定义 ViewGroup 里重写这三个方法,打印日志:

public class DebugViewGroup extends LinearLayout {

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        Log.d("TouchDebug", "dispatchTouchEvent: " + ev.getAction());
        return super.dispatchTouchEvent(ev);
    }

    @Override
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercept = super.onInterceptTouchEvent(ev);
        Log.d("TouchDebug", "onInterceptTouchEvent: " + ev.getAction() + " -> " + intercept);
        return intercept;
    }

    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean consumed = super.onTouchEvent(event);
        Log.d("TouchDebug", "onTouchEvent: " + event.getAction() + " -> " + consumed);
        return consumed;
    }
}

跑一下,看 Logcat 输出。你就能清晰地看到事件在每一层的流转。偏移问题往往就在某一次拦截或消费的返回值上。

3.8 避坑指南:我曾经踩过的三个坑

  • 坑一:在 onInterceptTouchEvent() 里对 DOWN 事件返回 true。这会导致子 View 永远收不到任何事件。我建议只在 MOVE 事件里做拦截判断。
  • 坑二:忘记调用 super 方法。如果你完全重写了 dispatchTouchEvent() 但没有调用 super,事件分发链就断了。我曾经因为这个排查了整整一下午。
  • 坑三:在 onTouchEvent() 里返回 false 但期望 View 能响应点击。记住,返回 false 意味着「我不处理」,系统会认为这个 View 不可点击。