文章内容
一、为什么需要理解事件分发
Android 提供的原生控件——比如 Button、ScrollView、RecyclerView——内部已经把事件处理逻辑写好了。用它们的时候,你几乎不需要操心事件冲突的问题。
但一旦开始写自定义 View,情况就变了。比如你做了一个自定义的滑动面板,里面嵌套了一个列表,手指一滑,要么外面的面板不动,要么里面的列表不响应。这就是事件冲突。
要解决冲突,就必须搞清楚两件事:事件是怎么分发的,以及事件最终由谁来处理。核心逻辑集中在两个源码文件里——View.java 和 ViewGroup.java。
二、MotionEvent:触摸事件的四种类型
Android 里所有触摸交互的基础是 MotionEvent。手指在屏幕上的每一个动作,系统都会封装成一个 MotionEvent 对象。它一共有四种类型:
ACTION_DOWN —— 手指按下。这是整个触摸序列的起点。每一次完整的触摸都必须从 DOWN 开始。
ACTION_MOVE —— 手指在屏幕上移动。从按下到抬起之间如果发生了滑动,MOVE 事件会多次触发,而不是只触发一次。移动越快、采样频率越高,触发次数越多。
ACTION_UP —— 手指抬起。触摸序列的终点。
ACTION_CANCEL —— 取消事件。当一个事件被父容器拦截的时候,子 View 会收到一个 CANCEL 事件,表示后续事件不会再来。
从 DOWN 到 UP(或 CANCEL),整个序列构成了一次完整的触摸事件。中间可能夹着很多个 MOVE,但它们都属于同一次事件。
系统在做事件分发时,以"一次事件"为单位——从 DOWN 开始,到 UP 结束,中间的所有 MOVE 都归同一个 View 处理。
三、一次事件只能由一个 View 处理
这是事件分发的核心原则:一个触摸事件,最终只能交给一个 View 来处理。
想象两个 View 叠在一起的情况:上面一个左右滑动的面板,下面一个上下滑动的列表。手指点击屏幕上的某个位置,这个点击事件要么给上面的面板,要么给下面的列表,不可能同时给两个。
如果事件给了上面的面板,那下面的列表就收不到点击;反过来也一样。这就是事件冲突的本质——两个 View 都想处理同一个事件,但事件只有一个。
用原生控件的时候,谷歌工程师已经帮你处理好了这类冲突。但自定义 View 时,你得自己想办法。
四、View 和 ViewGroup:两类角色
Android 的 UI 层级里有两种基本角色:
View —— 单个视图控件,比如 ImageView、TextView、Button。它们负责处理事件,也就是在收到事件后执行具体的响应逻辑(点击、长按、滑动等)。
ViewGroup —— 视图容器,比如 LinearLayout、RelativeLayout、FrameLayout。它们继承自 View,但多了一项职责:分发事件。ViewGroup 里可以嵌套子 View,所以它需要决定把事件分发给哪个子 View,或者在子 View 都不处理时自己处理。

每个 ViewGroup 既可以是容器(分发事件给子 View),也可以是 View(自己处理事件)。普通的 View 只处理事件,不做分发。
具体到源码层面,一共就是三个方法(签名取自 AOSP 的 View.java 和 ViewGroup.java):
// View.java —— 事件入口,View 和 ViewGroup 都有public boolean dispatchTouchEvent(MotionEvent event) // View.java —— 事件处理,View 和 ViewGroup 都有public boolean onTouchEvent(MotionEvent event) // ViewGroup.java —— 拦截判断,只有 ViewGroup 有public boolean onInterceptTouchEvent(MotionEvent ev)| 方法 | 谁有 | 职责 |
|---|---|---|
dispatchTouchEvent |
View / ViewGroup | 事件入口,决定事件往下怎么走 |
onInterceptTouchEvent |
仅 ViewGroup | 决定是否把事件拦下来自己处理 |
onTouchEvent |
View / ViewGroup | 真正处理事件,返回是否消费 |
注意 dispatchTouchEvent() 定义在 View.java 里,所以普通 View 也有这个方法——普通 View 没有子 View,dispatchTouchEvent() 基本就是交给自己的 onTouchEvent()。ViewGroup 重写了它,加入了"先分发给子 View"的逻辑。真正 ViewGroup 独有的方法是 onInterceptTouchEvent()。
写自定义 View 的时候,普通 View 主要重写 onTouchEvent();自定义 ViewGroup 还需要考虑 onInterceptTouchEvent() 的拦截逻辑。
五、事件分发的完整链路
一个触摸事件从手指接触屏幕开始,经过一条完整的链路最终到达处理它的 View:

- Activity 收到系统传来的触摸事件,调用
dispatchTouchEvent()。 - 事件传递给 PhoneWindow,再传给 DecorView(Activity 的根视图)。
- DecorView 把事件交给 ViewGroup 的
dispatchTouchEvent()。ViewGroup 在这里做两件事:先尝试分发给子 View,如果子 View 都不处理,就自己处理。 - 最终事件到达某个 View 的
onTouchEvent(),由它完成实际的响应。
不管中间经过多少层 ViewGroup 嵌套,分发逻辑是一样的:容器先分发,分发不出去就自己处理。
返回值:什么叫"消费"事件
上面这条链路能走通,靠的是每一层方法的 boolean 返回值。某个方法返回 true,表示"这个事件我要了,不用再往回传了",就是消费了这个事件。
三个方法的返回值含义并不相同:
| 方法 | 返回 true |
返回 false |
|---|---|---|
dispatchTouchEvent |
事件已在这条分支里被消费,不再交还上层 | 没人消费,交还给上层继续找处理者 |
onInterceptTouchEvent |
拦截:不再分发给子 View,转交自己的 onTouchEvent |
不拦截:继续向子 View 分发 |
onTouchEvent |
我消费了这个事件 | 我不处理,交还给上层的 onTouchEvent |
简单来说:true 是"到此为止,我要了",false 是"我不管,继续往下"。
消费与否直接决定后续事件的去向。如果一个 View 在 ACTION_DOWN 时返回了 false,后续的 MOVE 和 UP 事件就不会再发给它了——系统在按下那一刻就认定"这个 View 不要这次事件",后续动作走别的路径。写需要处理滑动的自定义 View 时,DOWN 必须返回 true,哪怕此时还没处理任何逻辑。
反过来,如果一路传到底都没人返回 true,事件会沿原路返回,最终交给 Activity#onTouchEvent() 兜底。
六、理解冲突的起点
回到开头的问题:两个 View 叠在一起,事件冲突怎么解决?
关键在 ViewGroup#dispatchTouchEvent()。它是分发链路的核心——决定事件给谁、什么时候拦截、拦截后怎么通知子 View。
View.java:事件处理。每个 View 子类都可以重写onTouchEvent()来自定义处理逻辑。ViewGroup.java:事件分发 + 事件处理。除了继承 View 的处理能力,还重写了dispatchTouchEvent()(加入"先分发给子 View"的逻辑),并新增了onInterceptTouchEvent()两个关键方法。
拦截:ViewGroup 的决策点
onInterceptTouchEvent() 是 ViewGroup 处理冲突的入口。它在 dispatchTouchEvent() 内部被调用,每次事件往下分发之前都会先检查是否需要拦截:
// 自定义 ViewGroup 示例public class TouchDemoLayout extends FrameLayout { public TouchDemoLayout(Context context, AttributeSet attrs) { super(context, attrs); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { // 返回 false:不拦截,事件继续分发给子 View(默认行为) // 返回 true: 拦截,事件转交给自己的 onTouchEvent(), // 正在处理事件的子 View 会收到一个 ACTION_CANCEL return false; }}ViewGroup 里这个方法的默认实现是返回 false——不拦截,事件照常往下走。不重写它的话,一个普通的 FrameLayout 不会跟子 View 抢事件。
这里也补上了第二章留下的那个坑:ACTION_CANCEL 是从哪来的。子 View 已经在处理一次事件了(比如手指按下后正在滑动),中途父容器在某个 MOVE 上返回了 true 把事件拦走,子 View 就会收到一个 ACTION_CANCEL,用来收尾自己的状态(停止滑动、清掉按下效果)。典型场景就是外层滑动容器判断出"这是一次纵向滑动,该我管",于是从子 View 手里把后续事件接过来。
如果你用的是谷歌提供的标准控件(比如 ImageView 嵌套在 LinearLayout 里)出现了事件冲突,需要去看这些控件内部的分发逻辑。自定义 View 的分发和处理逻辑完全由你控制——冲突也得你自己处理。
事件分发在实际开发中用得很多。UI 交互越复杂,越需要精确控制事件的流向。而控制事件流向的基础,就是理解 View 和 ViewGroup 各自在分发链路中扮演的角色。
本文由 AgentWorkFlow 生成