1. 事件分发 是什么?
事件分发是指 触摸事件(MotionEvent) 从屏幕产生后,系统如何将其传递给具体的 View 并决定由谁处理的过程。
本质上是 View 系统对触摸事件的一套传递与消费机制。
Android 的事件传递顺序:
Activity → ViewGroup → View
Activity → PhoneWindow → DecorView → ViewGroup → View
- PhoneWindow 内部持有 DecorView(整个界面的根 View,继承自 FrameLayout)。
- DecorView 的 superDispatchTouchEvent 会直接调父类 ViewGroup 的 dispatchTouchEvent,正式进入 ViewGroup 分发。
2. 三个核心方法
| 方法 | 所属类 | 作用 | 返回值含义 |
|---|---|---|---|
dispatchTouchEvent() |
View / ViewGroup | 事件分发入口,决定事件流向 | true:事件被自己或子控件消耗,停止上级传递;false:自己及子控件均不处理,回传给上层 |
onInterceptTouchEvent() |
只 ViewGroup 有 | 拦截事件,将事件拉回给自己处理 | true:拦截,事件交给自己的 onTouchEvent;false(默认):不拦截,继续向子控件传递 |
onTouchEvent() |
View / ViewGroup | 实际处理触摸事件 | true:表示自己消耗了事件;false:未消耗,事件回传 |
3. 整体流程
因为 事件传递顺序 是以 Activity → ViewGroup → View,所以以这三个阶段来说明事件分发的具体流程
3.1 Activity 分发
java
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
可以看出,这儿
Activity.dispatchTouchEvent内部调用了window的dispatchTouchEvent方法,如果所有子View没有消费的话会调用Activity自身的onTouchEvent方法
text
Activity.dispatchTouchEvent()
└→ getWindow().superDispatchTouchEvent(ev) // PhoneWindow
└→ mDecor.superDispatchTouchEvent(ev) // DecorView
└→ super.dispatchTouchEvent(ev) // 实际调的是 ViewGroup.dispatchTouchEvent()
- 若 ViewGroup 最终消费事件 → 返回 true,事件终止。
- 若 ViewGroup 没消费(返回 false)→ 会执行 Activity.onTouchEvent(ev)。
3.2 ViewGroup 分发
简陋流程,伪代码:
java
boolean dispatchTouchEvent(MotionEvent ev) {
if (!disallowIntercept && onInterceptTouchEvent(ev)) {
// 拦截:自己处理,调用自己的 onTouchEvent()
return super.dispatchTouchEvent(ev); // 实际上是 View.dispatchTouchEvent
}
// 不拦截:遍历所有子 View,找触摸点落在哪个子 View 上
for (View child : children) {
if (child 被触摸) {
if (child.dispatchTouchEvent(ev)) { // 交给子 View 分发
return true; // 子 View 消费了,自己就不处理了
}
}
}
// 子 View 都不消费,自己处理
return super.dispatchTouchEvent(ev); // 最终走到自己的 onTouchEvent()
}
大致分为三步:
-
拦截:重写 onInterceptTouchEvent 返回 true,事件就停止下传,自己处理。
-
不拦截:找到具体子 View,调用子 View 的 dispatchTouchEvent。
-
消费与冒泡:子 View 消费(返回 true)则结束;都不消费则"冒泡"回自身。
3.3 View 分发与消费
3.3.1 View 分发
java
boolean dispatchTouchEvent(MotionEvent event) {
if (ENABLED && mOnTouchListener != null
&& mOnTouchListener.onTouch(this, event)) {
return true; // onTouch 直接消费,不再往下
}
return onTouchEvent(event); // 否则交给 onTouchEvent 处理
}
判断当前的View有没有注册TouchListener,如果注册了,那就不往下走了,说明事件已经被消费了,如果没注册,那就继续往下走,执行onTouchEvent()来消费这个事件。
- 若 onTouch 返回 true,事件立即被消费,不会触发 onTouchEvent,更不会触发 onClick。
- 若 onTouch 返回 false 或没有注册,才会进入下一步:View 的消费(即 onTouchEvent)。
3.3.2 View消费
- onTouchEvent 内部根据控件是否可点击来决定是否消费事件:
- 可点击(CLICKABLE 或 LONG_CLICKABLE 为 true):
- 在 ACTION_UP 时调用 performClick(),进而触发 onClick()。
- 返回 true,表示事件被当前 View 消费。
- 不可点击:
- 返回 false,事件向上层传递(冒泡给父 View)。
- 可点击(CLICKABLE 或 LONG_CLICKABLE 为 true):
完整执行顺序:onTouch → onTouchEvent → onClick
4. 相关结论&问题
结论:
- 事件序列的连续性
- 事件序列:在一次事件分发中,以每个 DOWN 事件为开始, UP / CANCEL 事件为停止,在 DOWN -> MOVE -> MOVE -> UP / CANCEL 整个过程看做是一个事件序列
- 一旦某个 View 决定不处理 DOWN 事件(onTouchEvent 返回 false),同一序列的后续事件(MOVE, UP)都不会再给它。
- 某个 View 处理了 DOWN 事件,那么同序列的后续事件(MOVE, UP) 都会交给它
- 如果 ViewGroup 拦截了非 DOWN 事件,子 View 会收到 CANCEL。
- ViewGroup 拦截注意事项
- onInterceptTouchEvent 只存在于 ViewGroup,View 没有这个方法。
- 子 View 可通过 requestDisallowInterceptTouchEvent(true) 阻止父 View 拦截。
- 返回值的含义
- dispatchTouchEvent 返回 true:事件被当前层级或子层级消费。
- dispatchTouchEvent 返回 false:自身及子层级都不处理,事件向上传递。
- onTouchEvent 返回 true:表示当前 View 消费了事件,终止传递。
- onTouch 和 onClick 的关系
- 注册了 OnTouchListener 且 onTouch() 返回 true → onClick 不会再调用。
- 只有 onTouch() 返回 false,才会走到 onTouchEvent,最终触发 onClick。
问题:
Q1:ViewGroup 为什么没有 onTouchEvent?
有。ViewGroup 继承自 View,天然有
onTouchEvent。当它拦截事件或没有子 View 消费时,就会执行自己的onTouchEvent。
Q2:如何解决滑动冲突?
两种方案:
- 外部拦截法 :在父 View 的
onInterceptTouchEvent中根据手势逻辑返回true/false。- 内部拦截法 :子 View 调用
requestDisallowInterceptTouchEvent(true),父 View 配合不要拦截DOWN事件。
Q3:为什么推荐默认不拦截 DOWN 事件?
因为一旦父 View 拦截了
DOWN,子 View 将收不到任何事件序列,后续 MOVE/UP 都不会传给它,容易出现事件丢失。
Q4:如果所有 View 都不消费事件,最终会怎样?
事件会回溯到 Activity,调用
Activity.onTouchEvent(),如果它也不消费,系统最终丢弃该事件。