Android 事件分发机制 - 事件流向详解

121 阅读6分钟

一、事件来源与入口

用户触摸 → InputManager → Binder → 主线程 MessageQueue → Looper → Activity.dispatchTouchEvent()
    → Window → DecorView → 根 ViewGroup → 逐层分发

二、核心方法一览

类型方法作用返回值
ViewGroup / ViewdispatchTouchEvent()分发true=消费 / false=向上 / super=走默认流程
仅 ViewGrouponInterceptTouchEvent()是否拦截(View 无此方法)true=拦截 / false 或 super=不拦截,传子 View
ViewGroup / ViewonTouchEvent()处理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向上不再传当前
superViewGroup 默认 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向上不触发
superclickable 决定:Button 等 true→消费;TextView/ImageView/自定义默认 false→向上同 clickable

可点击: setClickable(true)setOnClickListener(...)


六、完整场景(Activity → A → B → C)

#关键设置最终处理者后续 MOVE/UP
1A.dispatchTouchEvent = trueA传 A
2A.dispatchTouchEvent = falseActivity不传 A/B/C
3A.dispatch=super,A 拦截,A.onTouchEvent=trueA传 A
4A.dispatch=super,A 拦截,A.onTouchEvent=falseActivity不传 A
5A、B 不拦截,C.onTouchEvent=trueC传 C
6A、B 不拦截,C 不处理,B.onTouchEvent=trueB传 B
7A、B、C 均不处理Activity不传任何 View

七、记忆要点 + 常见注意

三条结论:

  1. dispatchTouchEvent

    • true → 在此消费,后面逻辑都不走。
    • false → 不处理,直接向上。
    • super → 才走「拦截 → 子 View → onTouchEvent」(ViewGroup)或「Listener → onTouchEvent」(View)。
  2. onInterceptTouchEvent(仅 ViewGroup)

    • true → 拦截,不传子 View,可给已收 DOWN 的子 View 发 ACTION_CANCEL
    • false / super → 不拦截,传子 View。
  3. 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
dispatchTouchEventViewGroup、View当前消费,不再传递不处理,向上传递走默认逻辑(拦截/子 View/onTouchEvent)
onInterceptTouchEvent仅 ViewGroup拦截,不传子 View不拦截,传子 View同 false
onTouchEventViewGroup、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/横向列表),同一方向滑动时需要决定由谁消费。
  • 解决思路
    1. 外部拦截:在父 ViewGroup 的 onInterceptTouchEvent() 里根据手势方向(如 MOVE 时判断水平/垂直位移)决定是否拦截;需要让子处理时 DOWN 不拦截,在 MOVE 时再决定。
    2. 内部拦截:子 View 在收到 ACTION_DOWN 时调用 getParent().requestDisallowInterceptTouchEvent(true),请求父在本轮序列中不拦截;父可根据业务再调 requestDisallowInterceptTouchEvent(false) 收回权限。

Q6:为什么点击/触摸子 View 没有反应?可能原因有哪些?

答: 可从事件分发链逐层排查:

  1. 父 ViewGroup 在 dispatchTouchEvent 里直接 return true/false:不会走 onIntercept 和子 View 分发,事件到父就结束或直接上交。
  2. 父 ViewGroup onInterceptTouchEvent 返回 true:事件被拦截,不会下发给子 View。
  3. 子 View 不在可触区域:被遮挡、坐标不在范围内、或子 View 被移出父布局。
  4. 子 View 被禁用或不可见enabled=falsevisibility=GONE/INVISIBLE 等会导致不接收或不可点击。
  5. 子 View 的 onTouchEvent 返回 false:表示不消费,事件会回传给父。
  6. 子 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(点击)可能不会执行。