开胃比喻:一个任务从老板传到实习生的故事
想象一下,你在一家“不大不小”的公司。大老板(Activity)接了个急活:客户用手指在屏幕上划了一下。老板懒得动,就把任务甩给了部门总监(顶层ViewGroup)。总监一看,这活我干不了啊,得往下派,于是又扔给小组长(内层ViewGroup)。小组长瞅了瞅,发现自己也搞不定,最后丢给了一线员工(View)。
员工要么自己把活干了(消费事件),要么干不了,再原路退回给老板。老板要是发现谁都不接,那就只能自己叹口气:“算了,这活没人干得动。”
小备注:实际上,Android 的事件是从 Activity 的
dispatchTouchEvent进入视图体系的,最顶层的 View 叫 DecorView。但为了好懂,咱们就简化成“老板开始派活”。
这就是安卓里臭名昭著的事件分发机制——说白了就是一场从上到下的“踢皮球”游戏。
1. 大老板的“派活单”:dispatchTouchEvent
大老板手里有张单子叫 dispatchTouchEvent。他拿到触摸事件后,第一反应不是自己干,而是问手下:“谁能处理?”然后挨个往下传。
- 如果中间有哪个领导说“我能处理”,事件就停在那,不再往下传。
- 如果传到最底层都没人接,事件原路返回,老板自己兜底。
技术点:dispatchTouchEvent 是事件分发的入口。Activity、ViewGroup 和 View 都有这个方法,但前两者主要负责向下派发,而 View 的 dispatchTouchEvent 主要是调用自己的 onTouchEvent。返回 true 表示事件被消费了,返回 false 表示没人要。
大白话:老板只管派活,不管细节。派出去没人干,那就自己认栽。
2. 中层干部的“截胡权”:onInterceptTouchEvent
小组长(ViewGroup)有个特殊技能:拦截。他可以在任务往下传的过程中,瞅一眼这活值不值得自己干。如果觉得“这客户是我老熟人,我来”,他就调用 onInterceptTouchEvent 返回 true,把后续任务截胡了。
但注意:当小组长决定拦截时,当前的 DOWN 事件其实已经传给子 View 了。只不过子 View 在收到后续 MOVE 事件之前,会先收到一个 ACTION_CANCEL——“退货通知”,告诉子 View:“这活你别干了,交给我吧。”
技术点:onInterceptTouchEvent 只存在于 ViewGroup。它返回 true 后,当前事件仍会发给子 View(子 View 会收到 CANCEL),但后续的 MOVE/UP 事件直接交给这个 ViewGroup 自己的 onTouchEvent 处理。
大白话:领导可以中途“抢单”,但抢的时候会给当前员工发个“你被解雇了”的通知,让他赶紧收手。这就是滑动冲突(比如 ListView 和内部横向滑动 View 打架)的根源。
3. 基层员工的“处理能力”:onTouchEvent
一线员工(View)是最后接盘的人。他的 onTouchEvent 返回 true 表示“这活我能干,我吃了”,返回 false 表示“我搞不定,退回去”。
别忘了:ViewGroup 同时也是“一线员工”——因为它继承自 View,也有 onTouchEvent。当小组长拦截了事件,或者所有子 View 都不消费时,小组长自己的 onTouchEvent 就会被调用。
技术点:onTouchEvent 处理具体触摸逻辑,比如按钮按下变色、滑动列表等。如果它返回 false,事件会回溯给父视图的 onTouchEvent。
大白话:员工能搞定就留下,搞不定就退给组长;组长自己也有手,可以亲自干。
4. 如果所有人都不接?最终归宿
假设一个事件从 Activity 传到顶层 ViewGroup,再传到内层 ViewGroup,最后传到某个 Button。Button 的 onTouchEvent 返回 false。那么事件开始回溯:Button 的父 ViewGroup 的 onTouchEvent 被调用,如果也返回 false,继续往上,直到 Activity 的 onTouchEvent。如果 Activity 也返回 false……那这个事件就被丢弃了。
大白话:整个公司没人愿意接这个活,老板只能当没发生过。
5. 补充:事件分发的“退货通知”——ACTION_CANCEL
这是面试里的高频坑点。当父 ViewGroup 中途拦截事件时,子 View 会收到 ACTION_CANCEL。比如你有个横向滑动的 ViewPager 里面套了个竖向滑动的 ListView,当你在 ListView 上横向滑动时,ViewPager 检测到横向偏移就会拦截事件,此时 ListView 会收到 CANCEL,需要及时重置状态(取消高亮、停止惯性滚动等),否则 UI 会卡在半路。
代码实践:在自定义 View 的 onTouchEvent 中处理 ACTION_CANCEL,把按下状态恢复成正常。
总结表格
| 角色 | 对应组件 | 关键方法 | 职责 | 返回 true 意味着 |
|---|---|---|---|---|
| 大老板 | Activity | dispatchTouchEvent | 启动分发 | 事件被消费,不再回传 |
| 中层干部 | ViewGroup | dispatchTouchEvent + onInterceptTouchEvent + onTouchEvent | 派发、拦截或自己处理 | 拦截后自己消费,子 View 收到 CANCEL |
| 基层员工 | View | dispatchTouchEvent + onTouchEvent | 处理或退回 | 我搞定了,后续事件也给我 |
面试官爱怎么问?
问: 说一下 Android 的事件分发机制,尤其是 onInterceptTouchEvent 是干嘛的?
答(白话版):
事件分发就像领导派活,从 Activity 一层层传到最里面的 View。onInterceptTouchEvent 是 ViewGroup 独有的“截胡”方法。如果某个中间领导觉得这活该自己干,就拦截下来。注意:拦截后当前 DOWN 事件还是会传给子 View,但子 View 会收到 CANCEL 通知,后续事件直接给领导。比如横向 ViewPager 套竖向 ListView,ViewPager 在检测到横向滑动时拦截,ListView 就会收到 CANCEL 并停止滑动。
问: 滑动冲突怎么解决?
答(白话版):
两种办法。一是外部拦截:父 ViewGroup 重写 onInterceptTouchEvent,根据滑动的横向/竖向差值决定是否抢走事件。二是内部拦截:子 View 调用 getParent().requestDisallowInterceptTouchEvent(true) 告诉父 View“你别抢,我自己来”。推荐用外部拦截,逻辑清晰,不破坏事件分发链条。
人话总结金句
事件分发就是一场从老板到员工的踢皮球游戏——从上往下传,谁有能力谁接球;中层领导可以中途喊停(拦截),让当前员工收个“退货通知”(CANCEL)后,自己把活接下来;如果传到最底层都没人接,就原路退回,老板自己兜底。
汇总导航