Android面试冲击附答案(三)————事件分发机制
一、面试题与答案
1. 说说MotionEvent事件
事件类型:DOWN、MOVE、UP、CANCEL
一个完整的事件序列由一个 DOWN 开始,中间可以有多个 MOVE,最终以 UP 或 CANCEL 结束。
2. 说说Android的事件分发机制
MotionEvent 事件从外到内分发:Activity → ViewGroup → View
核心方法:dispatchTouchEvent → onInterceptTouchEvent → onTouchEvent
伪代码:
fun dispatchTouchEvent(ev: MotionEvent): Boolean {
return if (isViewGroup) {
if (onInterceptTouchEvent(ev)) onTouchEvent(ev) else child.dispatchTouchEvent(ev)
} else {
onTouchEvent(ev)
}
}
3. 说说mFirstTouchTarget的作用?它是什么数据结构?
TouchTarget 是 ViewGroup 的静态内部类,单链表结构。
核心作用:减少遍历、控制事件分发、发送取消事件,只在 DOWN 事件里设置,标记事件序列由哪个 View 处理。
- 提升事件分发效率:
DOWN事件来临时,ViewGroup遍历子 View 找到目标并挂到mFirstTouchTarget。后续事件来临时,mFirstTouchTarget != null则直接分发,不再遍历子 View。 - 控制事件分发流程:
null代表ViewGroup自己处理,不会触发onInterceptTouchEvent;不为null代表分发给子 View。 - 管理取消事件:父 View 拦截或子 View 被移除时,
ViewGroup会遍历mFirstTouchTarget链表给每个子 View 发送CANCEL事件。
4. 屏幕上有个Button,描述手指按到Button后再移动到Button外再抬起的事件分发过程
- DOWN 事件:Activity → ViewGroup(默认不拦截,遍历子 View,找到 Button,挂到
mFirstTouchTarget)→Button.onTouchEvent消费事件 - MOVE 事件:
ViewGroup判断mFirstTouchTarget != null,本层不处理,直接交给Button.onTouchEvent。即使移动到 View 之外,依然交给mFirstTouchTarget处理。 - UP 事件:分发逻辑同上,还是交给
Button.onTouchEvent。onTouchEvent内部会判断DOWN和UP是否都在 View 里,只有都在时才触发点击事件。 - 事件序列结束,父
ViewGroup自动重置:mFirstTouchTarget = null。
5. View的enable和clickable有什么区别?
enable:优先级更高,是全局事件开关。enable = false后,触摸和点击事件都不会触发。大部分 View 默认true,TextView默认false。clickable:仅控制点击事件。clickable = false时,onTouchEvent对点击事件无响应,但能接收触摸事件。
enable = false 会让后续 clickable 直接失效。
6. onTouch、onTouchEvent、onClick的执行顺序?
优先级:onTouch → onTouchEvent → onClick
MotionEvent事件先经过onTouch处理,如果onTouch消费了事件,onTouchEvent和onClick都不会执行。- 如果
onTouchListener未设置或未消费事件,进入onTouchEvent。 onTouchEvent会判断 View 是否clickable和longClickable,不可点击直接返回false。onClick是onTouchEvent的副产品,优先级最低,onTouchEvent判定为有效点击且设置了onClickListener时才回调。
7. ACTION_CANCEL什么时候触发?
子 View 正在处理触摸事件序列,但被父 View 或系统强制中断时触发。典型场景:
- 父 View 拦截了正在进行的事件序列
- 子 View 正在处理触摸事件时,被动态移除或隐藏
- 系统层面的事件中断,如 Dialog、Toast、系统通知栏等遮挡了 View
注意:手指滑到 View 之外后抬起,不会触发 CANCEL 事件,除非期间父 View 布局改变导致子 View 位置偏移。
8. ViewGroup什么时候会触发onInterceptTouchEvent?
先说不触发的情况:
mFirstTouchTarget == null(没有子 View 处理事件),MOVE/UP直接交给本层消费,不触发拦截- 子 View 调用了
requestDisallowInterceptTouchEvent(true)且不是DOWN事件 ViewGroup设置了onTouchListener且返回true
必定触发:ACTION_DOWN 事件必定会触发 onInterceptTouchEvent。
9. 如何处理滑动冲突?
外部拦截法:ViewGroup 重写 onInterceptTouchEvent,DOWN 事件不拦截,在 MOVE 事件中按需拦截。
内部拦截法:
- 父 View 不拦截
DOWN事件,其他事件允许拦截 - 重写子 View 分发逻辑,在
MOVE里按需通过requestDisallowInterceptTouchEvent决定父 View 是否拦截
10. 点击事件被拦截,但是想传到下面的View,如何操作?
被拦截后交给本层 onTouchEvent,可以重写 onTouchEvent,调用子 View 的 dispatchTouchEvent 手动转发(需要转换坐标、判断 View 位置等)。
追问:requestDisallowInterceptTouchEvent 有作用吗?
- 对
DOWN事件不生效,DOWN事件会进入父 View 的拦截方法 - 对
MOVE和UP事件有效,但需要在DOWN事件里提前设置,且前提是父 View 不拦截DOWN事件
11. 在ViewGroup中的onTouchEvent中消费ACTION_DOWN事件,ACTION_UP事件是怎么传递的?
DOWN 被 ViewGroup 消费,说明没有子 View 消费 DOWN,即 mFirstTouchTarget == null,后续所有事件都会直接交给 ViewGroup 的 onTouchEvent 处理,不再分发给子 View。
12. Activity、ViewGroup和View都不消费ACTION_DOWN,那么ACTION_UP事件是怎么传递的?
事件序列无效,UP 事件直接交给 Activity 处理,不再走完整的分发流程。
13. 如果父View拦截了DOWN事件,子View还能接受到事件吗?
不能。父 View 拦截 DOWN 后,mFirstTouchTarget == null,后续事件直接交给 ViewGroup 的 onTouchEvent,不会传递到子 View。
14. 如果父View不拦截DOWN事件,拦截MOVE/UP事件,子View中设置了requestDisallowInterceptTouchEvent(true)后,子View能收到MOVE/UP事件吗?
能收到。子 View 请求父 View 不拦截,事件会交给子 View 处理。
但必须在 ACTION_DOWN 里就设置,否则如果第一个 MOVE 事件被拦截了,可能导致后续事件分发不到子 View。
二、事件分发机制原理
核心角色
| 角色 | 核心方法 | 说明 |
|---|---|---|
Activity | dispatchTouchEvent | 事件入口,最终兜底处理 |
ViewGroup | dispatchTouchEvent / onInterceptTouchEvent / onTouchEvent | 负责分发和拦截 |
View | dispatchTouchEvent / onTouchEvent | 最终消费者 |
事件分发流程
手指触摸屏幕
│
▼
Activity.dispatchTouchEvent
│
▼
ViewGroup.dispatchTouchEvent
│
├─ DOWN事件 ──► 遍历子View,找到目标 ──► 设置 mFirstTouchTarget
│
├─ 调用 onInterceptTouchEvent
│ │
│ ├─ 返回 true(拦截)──► ViewGroup.onTouchEvent
│ │
│ └─ 返回 false(不拦截)──► child.dispatchTouchEvent
│
└─ MOVE/UP事件 ──► mFirstTouchTarget != null ──► 直接分发给目标子View
└─ mFirstTouchTarget == null ──► ViewGroup.onTouchEvent
mFirstTouchTarget 状态机
DOWN 事件到来
│
▼
遍历子View,找到能处理的View
│
├─ 找到 ──► mFirstTouchTarget = TouchTarget(child)
│
└─ 未找到 ──► mFirstTouchTarget = null(ViewGroup自己处理)
事件序列结束(UP/CANCEL)
│
▼
mFirstTouchTarget = null(重置)
onTouch / onTouchEvent / onClick 优先级
MotionEvent
│
▼
onTouchListener.onTouch()
│
├─ 返回 true(消费)──► 结束,onTouchEvent 和 onClick 不执行
│
└─ 返回 false ──► onTouchEvent()
│
├─ clickable/longClickable = false ──► 返回 false
│
└─ UP事件 + 有效点击 ──► onClick()
滑动冲突解决方案对比
| 方案 | 实现位置 | 核心逻辑 | 适用场景 |
|---|---|---|---|
| 外部拦截 | 父 View 重写 onInterceptTouchEvent | DOWN 不拦截,MOVE 按需拦截 | 父 View 主导滑动方向判断 |
| 内部拦截 | 子 View 调用 requestDisallowInterceptTouchEvent | DOWN 里设置标志,MOVE 里动态控制 | 子 View 主导滑动方向判断 |