Android面试冲击附答案(三)————事件分发机制

6 阅读5分钟

Android面试冲击附答案(三)————事件分发机制


一、面试题与答案

1. 说说MotionEvent事件

事件类型:DOWNMOVEUPCANCEL

一个完整的事件序列由一个 DOWN 开始,中间可以有多个 MOVE,最终以 UPCANCEL 结束。

2. 说说Android的事件分发机制

MotionEvent 事件从外到内分发:Activity → ViewGroup → View

核心方法:dispatchTouchEventonInterceptTouchEventonTouchEvent

伪代码:

fun dispatchTouchEvent(ev: MotionEvent): Boolean {
    return if (isViewGroup) {
        if (onInterceptTouchEvent(ev)) onTouchEvent(ev) else child.dispatchTouchEvent(ev)
    } else {
        onTouchEvent(ev)
    }
}
3. 说说mFirstTouchTarget的作用?它是什么数据结构?

TouchTargetViewGroup 的静态内部类,单链表结构。

核心作用:减少遍历、控制事件分发、发送取消事件,只在 DOWN 事件里设置,标记事件序列由哪个 View 处理。

  1. 提升事件分发效率DOWN 事件来临时,ViewGroup 遍历子 View 找到目标并挂到 mFirstTouchTarget。后续事件来临时,mFirstTouchTarget != null 则直接分发,不再遍历子 View。
  2. 控制事件分发流程null 代表 ViewGroup 自己处理,不会触发 onInterceptTouchEvent;不为 null 代表分发给子 View。
  3. 管理取消事件:父 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.onTouchEventonTouchEvent 内部会判断 DOWNUP 是否都在 View 里,只有都在时才触发点击事件。
  • 事件序列结束,父 ViewGroup 自动重置:mFirstTouchTarget = null
5. View的enable和clickable有什么区别?
  • enable:优先级更高,是全局事件开关。enable = false 后,触摸和点击事件都不会触发。大部分 View 默认 trueTextView 默认 false
  • clickable:仅控制点击事件。clickable = false 时,onTouchEvent 对点击事件无响应,但能接收触摸事件。

enable = false 会让后续 clickable 直接失效。

6. onTouch、onTouchEvent、onClick的执行顺序?

优先级:onTouch → onTouchEvent → onClick

  • MotionEvent 事件先经过 onTouch 处理,如果 onTouch 消费了事件,onTouchEventonClick 都不会执行。
  • 如果 onTouchListener 未设置或未消费事件,进入 onTouchEvent
  • onTouchEvent 会判断 View 是否 clickablelongClickable,不可点击直接返回 false
  • onClickonTouchEvent 的副产品,优先级最低,onTouchEvent 判定为有效点击且设置了 onClickListener 时才回调。
7. ACTION_CANCEL什么时候触发?

子 View 正在处理触摸事件序列,但被父 View 或系统强制中断时触发。典型场景:

  1. 父 View 拦截了正在进行的事件序列
  2. 子 View 正在处理触摸事件时,被动态移除或隐藏
  3. 系统层面的事件中断,如 Dialog、Toast、系统通知栏等遮挡了 View

注意:手指滑到 View 之外后抬起,不会触发 CANCEL 事件,除非期间父 View 布局改变导致子 View 位置偏移。

8. ViewGroup什么时候会触发onInterceptTouchEvent?

先说不触发的情况:

  1. mFirstTouchTarget == null(没有子 View 处理事件),MOVE/UP 直接交给本层消费,不触发拦截
  2. 子 View 调用了 requestDisallowInterceptTouchEvent(true) 且不是 DOWN 事件
  3. ViewGroup 设置了 onTouchListener 且返回 true

必定触发ACTION_DOWN 事件必定会触发 onInterceptTouchEvent

9. 如何处理滑动冲突?

外部拦截法ViewGroup 重写 onInterceptTouchEventDOWN 事件不拦截,在 MOVE 事件中按需拦截。

内部拦截法

  1. 父 View 不拦截 DOWN 事件,其他事件允许拦截
  2. 重写子 View 分发逻辑,在 MOVE 里按需通过 requestDisallowInterceptTouchEvent 决定父 View 是否拦截
10. 点击事件被拦截,但是想传到下面的View,如何操作?

被拦截后交给本层 onTouchEvent,可以重写 onTouchEvent,调用子 View 的 dispatchTouchEvent 手动转发(需要转换坐标、判断 View 位置等)。

追问:requestDisallowInterceptTouchEvent 有作用吗?

  1. DOWN 事件不生效DOWN 事件会进入父 View 的拦截方法
  2. MOVEUP 事件有效,但需要在 DOWN 事件里提前设置,且前提是父 View 不拦截 DOWN 事件
11. 在ViewGroup中的onTouchEvent中消费ACTION_DOWN事件,ACTION_UP事件是怎么传递的?

DOWNViewGroup 消费,说明没有子 View 消费 DOWN,即 mFirstTouchTarget == null,后续所有事件都会直接交给 ViewGrouponTouchEvent 处理,不再分发给子 View。

12. Activity、ViewGroup和View都不消费ACTION_DOWN,那么ACTION_UP事件是怎么传递的?

事件序列无效,UP 事件直接交给 Activity 处理,不再走完整的分发流程。

13. 如果父View拦截了DOWN事件,子View还能接受到事件吗?

不能。父 View 拦截 DOWN 后,mFirstTouchTarget == null,后续事件直接交给 ViewGrouponTouchEvent,不会传递到子 View。

14. 如果父View不拦截DOWN事件,拦截MOVE/UP事件,子View中设置了requestDisallowInterceptTouchEvent(true)后,子View能收到MOVE/UP事件吗?

能收到。子 View 请求父 View 不拦截,事件会交给子 View 处理。

但必须在 ACTION_DOWN 里就设置,否则如果第一个 MOVE 事件被拦截了,可能导致后续事件分发不到子 View。


二、事件分发机制原理

核心角色

角色核心方法说明
ActivitydispatchTouchEvent事件入口,最终兜底处理
ViewGroupdispatchTouchEvent / onInterceptTouchEvent / onTouchEvent负责分发和拦截
ViewdispatchTouchEvent / 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 重写 onInterceptTouchEventDOWN 不拦截,MOVE 按需拦截父 View 主导滑动方向判断
内部拦截子 View 调用 requestDisallowInterceptTouchEventDOWN 里设置标志,MOVE 里动态控制子 View 主导滑动方向判断