一、事件来源与入口
用户触摸 → InputManager → Binder → 主线程 MessageQueue → Looper → Activity.dispatchTouchEvent()
→ Window → DecorView → 根 ViewGroup → 逐层分发
二、核心方法一览
| 类型 | 方法 | 作用 | 返回值 |
|---|---|---|---|
| ViewGroup / View | dispatchTouchEvent() | 分发 | true=消费 / false=向上 / super=走默认流程 |
| 仅 ViewGroup | onInterceptTouchEvent() | 是否拦截(View 无此方法) | true=拦截 / false 或 super=不拦截,传子 View |
| ViewGroup / View | onTouchEvent() | 处理 | true=消费 / false=向上 / super=由 clickable 决定 |
说明:
dispatchTouchEvent()和onTouchEvent()定义在 View 中,ViewGroup 继承 View 并重写dispatchTouchEvent()加入了「拦截 + 遍历子 View」的逻辑;View 没有onInterceptTouchEvent()。
三、总流程图(建议先看)
Activity.dispatchTouchEvent()
│
▼
ViewGroup.dispatchTouchEvent()
│
┌────┴────┐
│ true? │──yes──→ 当前 ViewGroup 消费,结束(不调 onIntercept/onTouchEvent)
│ false? │──yes──→ 向上交给父/Activity,结束
│ super │──yes──→ 继续 ↓
└────┬────┘
▼
onInterceptTouchEvent()
│
┌────┴────┐
│ true? │──yes──→ 不传子 View,调当前 ViewGroup.onTouchEvent()
│ false │──yes──→ 遍历子 View,子.dispatchTouchEvent()
└────┬────┘
│ 子不处理 / 无子 View
▼
当前 ViewGroup.onTouchEvent() → true=消费 / false=向上
若当前是 View(叶子节点): 无 onInterceptTouchEvent;dispatchTouchEvent(super) 内先执行 OnTouchListener,未消费再执行 onTouchEvent()。
四、ViewGroup 事件流向(表格速查)
4.1 dispatchTouchEvent()
| 返回值 | onIntercept 被调? | 遍历子 View? | onTouchEvent 被调? | 事件去向 | 后续 MOVE/UP |
|---|---|---|---|---|---|
| true | 否 | 否 | 否 | 当前 ViewGroup 消费 | 继续传当前 |
| false | 否 | 否 | 否 | 向上 | 不再传当前 |
| super | 是 | 由拦截结果定 | 拦截或子未处理时 | 见 4.2 | 由最终消费者定 |
结论: 只有 super 才会走「拦截 → 子 View → 自身 onTouchEvent」;true/false 直接返回。
4.2 dispatchTouchEvent = super 时
| onInterceptTouchEvent | 行为 | 当前 onTouchEvent 被调? |
|---|---|---|
| true | 不传子 View;若子曾收 DOWN → 会收 ACTION_CANCEL | 是 |
| false / super | 遍历子 View,调子.dispatchTouchEvent() | 仅当无子处理时 |
| 当前 onTouchEvent() | 事件去向 | 后续 MOVE/UP |
|---|---|---|
| true | 当前 ViewGroup 消费 | 继续传当前 |
| false | 向上 | 不再传当前 |
| super | ViewGroup 默认 clickable=false → 通常向上 | 由返回值定 |
4.3 代码示例(MOVE 时拦截)
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return false; // 让子 View 可点击
case MotionEvent.ACTION_MOVE:
if (isScrollGesture(event)) return true; // 拦截,子 View 会收 CANCEL
return false;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
handleScroll(event);
return true;
}
五、View 事件流向(表格速查)
View 也有 dispatchTouchEvent() 和 onTouchEvent()(继承自 View 类),只是没有 onInterceptTouchEvent()。
5.1 dispatchTouchEvent()
| 返回值 | onTouchEvent 被调? | 事件去向 | 后续 MOVE/UP |
|---|---|---|---|
| true | 否 | 当前 View 消费 | 继续传当前 |
| false | 否 | 向上 | 不再传当前 |
| super | 先 OnTouchListener;若未消费再 onTouchEvent | 由二者决定 | 由是否消费决定 |
顺序: OnTouchListener.onTouch() == true → 不调 onTouchEvent;否则调 onTouchEvent()。
5.2 onTouchEvent()
| 返回值 | 事件去向 | 触发 performClick? |
|---|---|---|
| true | 当前 View 消费 | 可触发(clickable + UP) |
| false | 向上 | 不触发 |
| super | clickable 决定:Button 等 true→消费;TextView/ImageView/自定义默认 false→向上 | 同 clickable |
可点击: setClickable(true) 或 setOnClickListener(...)。
六、完整场景(Activity → A → B → C)
| # | 关键设置 | 最终处理者 | 后续 MOVE/UP |
|---|---|---|---|
| 1 | A.dispatchTouchEvent = true | A | 传 A |
| 2 | A.dispatchTouchEvent = false | Activity | 不传 A/B/C |
| 3 | A.dispatch=super,A 拦截,A.onTouchEvent=true | A | 传 A |
| 4 | A.dispatch=super,A 拦截,A.onTouchEvent=false | Activity | 不传 A |
| 5 | A、B 不拦截,C.onTouchEvent=true | C | 传 C |
| 6 | A、B 不拦截,C 不处理,B.onTouchEvent=true | B | 传 B |
| 7 | A、B、C 均不处理 | Activity | 不传任何 View |
七、记忆要点 + 常见注意
三条结论:
-
dispatchTouchEvent
- true → 在此消费,后面逻辑都不走。
- false → 不处理,直接向上。
- super → 才走「拦截 → 子 View → onTouchEvent」(ViewGroup)或「Listener → onTouchEvent」(View)。
-
onInterceptTouchEvent(仅 ViewGroup)
- true → 拦截,不传子 View,可给已收 DOWN 的子 View 发 ACTION_CANCEL。
- false / super → 不拦截,传子 View。
-
onTouchEvent
- true → 消费;false → 向上;super → 看 clickable(ViewGroup 默认 false,Button 默认 true)。
mFirstTouchTarget: 记录消费了 ACTION_DOWN 的 View,后续 MOVE/UP 直接发给该 View,保证同一序列一致。
常见注意:
- 事件序列:一次触摸为 DOWN → 若干 MOVE → UP/CANCEL;谁消费了 DOWN,后续事件就发给谁。
- requestDisallowInterceptTouchEvent(true):子 View 可请求父 ViewGroup 在本次序列中不拦截,常用于滑动冲突(如子水平、父垂直)。
- 父在 MOVE 时拦截:若子已收到 DOWN,子会收到 ACTION_CANCEL,应重置状态。
- clickable 默认值:ViewGroup / TextView / ImageView / 自定义 View 为 false;Button 为 true。
八、面试 Q&A
Q1:请简述 Android 触摸事件分发的整体流程?
答: 触摸事件从底层经 InputManager、Binder 到应用主线程,由 Activity.dispatchTouchEvent() 接收,再经 Window、DecorView 传到根 ViewGroup,按视图树自上而下分发。每一层先 dispatchTouchEvent(),ViewGroup 会调 onInterceptTouchEvent() 决定是否拦截,不拦截则传给子 View,子不处理再交给当前 View 的 onTouchEvent()。谁在 ACTION_DOWN 时消费了事件,后续 MOVE/UP 会直接发给谁(由 mFirstTouchTarget 记录)。
Q2:事件分发涉及哪几个核心方法?返回值含义是什么?
答: 三个方法:
| 方法 | 谁有 | 返回值 true | 返回值 false | 返回值 super |
|---|---|---|---|---|
dispatchTouchEvent | ViewGroup、View | 当前消费,不再传递 | 不处理,向上传递 | 走默认逻辑(拦截/子 View/onTouchEvent) |
onInterceptTouchEvent | 仅 ViewGroup | 拦截,不传子 View | 不拦截,传子 View | 同 false |
onTouchEvent | ViewGroup、View | 消费事件 | 不消费,向上 | 由 clickable 等决定 |
Q3:View 和 ViewGroup 在事件分发上有什么区别?
答:
- View:有
dispatchTouchEvent()、onTouchEvent(),没有onInterceptTouchEvent()。dispatchTouchEvent(super)时先走 OnTouchListener,再走 onTouchEvent()。 - ViewGroup:继承 View,重写
dispatchTouchEvent(),在其中增加「先 onInterceptTouchEvent 判断是否拦截 → 不拦截则遍历子 View 分发 → 子不处理再自己 onTouchEvent」的逻辑。
Q4:什么是 ACTION_CANCEL?什么时候子 View 会收到?
答: ACTION_CANCEL 表示「这次触摸序列对你来说被取消了」。典型场景:子 View 已经收到了 ACTION_DOWN,后续 ACTION_MOVE 时父 ViewGroup 在 onInterceptTouchEvent() 里返回 true 拦截了事件,系统就会给该子 View 发一次 ACTION_CANCEL,通知它不要再期待后续的 MOVE/UP。子 View 应在收到 CANCEL 时重置触摸相关状态。
Q5:滑动冲突怎么解决?requestDisallowInterceptTouchEvent 是干什么的?
答:
- 冲突场景:例如外层垂直滑动(ScrollView)、内层水平滑动(ViewPager/横向列表),同一方向滑动时需要决定由谁消费。
- 解决思路:
- 外部拦截:在父 ViewGroup 的
onInterceptTouchEvent()里根据手势方向(如 MOVE 时判断水平/垂直位移)决定是否拦截;需要让子处理时 DOWN 不拦截,在 MOVE 时再决定。 - 内部拦截:子 View 在收到 ACTION_DOWN 时调用
getParent().requestDisallowInterceptTouchEvent(true),请求父在本轮序列中不拦截;父可根据业务再调requestDisallowInterceptTouchEvent(false)收回权限。
- 外部拦截:在父 ViewGroup 的
Q6:为什么点击/触摸子 View 没有反应?可能原因有哪些?
答: 可从事件分发链逐层排查:
- 父 ViewGroup 在 dispatchTouchEvent 里直接 return true/false:不会走 onIntercept 和子 View 分发,事件到父就结束或直接上交。
- 父 ViewGroup onInterceptTouchEvent 返回 true:事件被拦截,不会下发给子 View。
- 子 View 不在可触区域:被遮挡、坐标不在范围内、或子 View 被移出父布局。
- 子 View 被禁用或不可见:
enabled=false、visibility=GONE/INVISIBLE等会导致不接收或不可点击。 - 子 View 的 onTouchEvent 返回 false:表示不消费,事件会回传给父。
- 子 View 在 DOWN 时没消费:若 DOWN 没被消费,系统不会把该子设为 mFirstTouchTarget,后续 MOVE/UP 不会发给它。
Q7:onTouch 和 onTouchEvent 有什么区别?执行顺序如何?
答:
- OnTouchListener.onTouch():外部设置的监听器,在
View.dispatchTouchEvent()里先于 onTouchEvent() 被调用。若 onTouch 返回 true,表示消费事件,不会再调用 onTouchEvent()。 - onTouchEvent():View 自身处理触摸的逻辑,在未设置 OnTouchListener 或 onTouch 返回 false 时才会执行。
因此优先级是:OnTouchListener.onTouch() > onTouchEvent()。若在 setOnTouchListener 里 return true,则 onTouchEvent 以及其内的 performClick(点击)可能不会执行。