事件分发机制
本质是 MotionEvent 在 View 树中“从外到内下发,再从内到外回传”的责任链过程。核心是三个方法:
dispatchTouchEvent():负责分发事件,所有 View 都有。onInterceptTouchEvent():只有 ViewGroup 有,用来决定是否拦截事件。onTouchEvent():真正处理事件,所有 View 都有。
返回值含义:
true:消费事件,事件不再向上回传。false:不消费,事件继续交给上层处理。
一、传递顺序
大致顺序是:
InputManager -> ViewRootImpl -> DecorView
-> Activity.dispatchTouchEvent()
-> PhoneWindow.superDispatchTouchEvent()
-> DecorView
-> ViewGroup.dispatchTouchEvent()
-> ... -> 子 View.dispatchTouchEvent()
-> View.onTouchEvent()
简化理解就是:
Activity -> Window -> DecorView -> ViewGroup -> View
事件先由外层向内层分发;如果内层不消费,再向外层回传,最终可能交给 Activity 的 onTouchEvent()。
二、ViewGroup 的分发逻辑
ViewGroup 的 dispatchTouchEvent() 大致流程:
- 如果是
ACTION_DOWN,重置状态,清除之前的触摸目标。 - 判断是否拦截:
- 调用
onInterceptTouchEvent()。 - 如果返回
true,表示拦截,不再分发给子 View。 - 如果返回
false,继续遍历子 View。
- 调用
- 遍历子 View,通常是倒序遍历,即后添加的 View 先收到事件。
- 如果某个子 View 的
dispatchTouchEvent()返回true,说明它消费了事件,ViewGroup 记录这个子 View 为mFirstTouchTarget。 - 如果没有任何子 View 消费,或者被拦截,就调用自己的
onTouchEvent()。 - 如果自己也不消费,返回
false,事件向上回传。
关键点:
一旦 ACTION_DOWN 被某个子 View 消费,后续的 ACTION_MOVE、ACTION_UP 会优先继续传给这个子 View,直到事件序列结束。
三、View 的处理逻辑
View 的 dispatchTouchEvent() 大致流程:
- 如果设置了
OnTouchListener,先调用onTouch()。 - 如果
onTouch()返回true,事件被消费,不再调用onTouchEvent()。 - 否则调用
onTouchEvent()。 - 如果 View 是
clickable、longClickable等可点击状态,onTouchEvent()通常返回true,并在ACTION_UP时触发performClick(),进而调用OnClickListener。
所以点击事件的优先级大致是:
OnTouchListener > onTouchEvent > OnClickListener
如果 View 不可点击,默认 onTouchEvent() 可能返回 false,事件会回传给父容器。
四、事件序列与 ACTION_CANCEL
一个完整触摸事件序列通常是:
ACTION_DOWN -> ACTION_MOVE -> ... -> ACTION_UP
如果中途被父 ViewGroup 拦截,子 View 会收到:
ACTION_CANCEL
ACTION_CANCEL 表示这个事件序列不再交给子 View 处理,子 View 应该清理按压状态、取消点击等。
因此:
ACTION_DOWN 是事件序列的起点,决定后续 MOVE/UP 归谁处理。
五、onInterceptTouchEvent 的特点
- 只有 ViewGroup 有。
- 默认返回
false,即不拦截。 - 一旦返回
true,后续同一事件序列不再分发给子 View。 - 如果已经分发给子 View,后来父 ViewGroup 又拦截,子 View 会收到
ACTION_CANCEL。 - 子 View 可以调用
requestDisallowInterceptTouchEvent(true),请求父 ViewGroup 不要拦截后续事件。但ACTION_DOWN时会重置这个标志,所以通常需要配合父 View 在 DOWN 时不拦截。
六、滑动冲突解决思路
常见两种方式:
1. 外部拦截法
父 ViewGroup 重写 onInterceptTouchEvent():
ACTION_DOWN返回false,先让子 View 有机会处理。ACTION_MOVE根据滑动方向判断是否拦截。- 如果父容器需要滑动,就返回
true拦截。
2. 内部拦截法
子 View 调用:
parent.requestDisallowInterceptTouchEvent(true);
让父容器不拦截事件,子 View 自己处理滑动。当子 View 需要父容器处理时,再允许父容器拦截。
实现原理: here
复杂场景可以使用 NestedScrolling 机制。
七、总结
Android 事件分发就是:
逐层调用 dispatchTouchEvent();ViewGroup 通过 onInterceptTouchEvent() 决定是否拦截;View 通过 onTouchEvent() 决定是否消费。 谁在 ACTION_DOWN 时返回 true,谁就拥有这一整段事件序列的处理权。