View 事件分发:从 InputEventReceiver 到 View 回调
View 事件分发:从 InputEventReceiver 到 View 回调
上一章我们看到了目标窗口。这一章回答:事件进入应用进程后,ViewRootImpl 如何把它交给 View 体系,ViewGroup 与 View 又是怎么一层层分发到 onTouchEvent() 的?
InputEventReceiver 取到事件后,ViewRootImpl 先让它经过 InputStage 职责链,再从 DecorView 进入 View 树。系统并不知道最终业务 View 的回调地址,也不会跨进程直接调用 onTouchEvent()。
事件先到根 View,再进入子树
| 常见误解 | 正确理解 |
|---|---|
| 事件直接调用目标 View 的 onTouchEvent | 事件从根 View 自上而下分发,经过每一层 ViewGroup |
onInterceptTouchEvent() 是父 View 的“要不要” | 它决定父 View 是否截断向下分发,是分发链的关键开关 |
| 一个 View 的 dispatchTouchEvent 只处理自己 | ViewGroup 会先询问子树,再决定自己消费或回溯 |
ViewGroup 在向子节点分发前可以通过 onInterceptTouchEvent() 改变后续路径;目标节点未消费 时,结果沿调用栈返回父节点。一次手势序列还要维持目标一致性,不能只根据单个 MOVE 事件判断。
核心结论
ViewRootImpl把接收事件投入 InputStage 链,触摸走deliverPointerEvent()。- 触摸事件从根视图开始,
ViewGroup.dispatchTouchEvent()先问自己onInterceptTouchEvent(),再沿子树分发。 - 子 View 通过
dispatchTouchEvent()决定自己或子级消费;都不消费则回溯给父级。 View.dispatchTouchEvent()最终调用onTouchEvent(),返回 true 表示消费。- 分发是否命中目标 View,取决于坐标是否落在子 View 范围内,与窗口命中测试是两套逻辑。
应用内分发主链
逐步解释:
- 接收事件:
InputEventReceiver在窗口 Looper 上收到事件。 - 进入 InputStage:
ViewRootImpl按事件类型分流,触摸走deliverPointerEvent()。 - 根视图分发:从根视图开始调用
dispatchTouchEvent()。 - ViewGroup 拦截:父级先问
onInterceptTouchEvent(),返回 true 则自己处理。 - 子树分发:不拦截时按子视图顺序分发,命中坐标的子 View 继续处理。
- 消费与回溯:
onTouchEvent()返回 true 表示消费;都不消费则回溯给父级。
为什么 ViewGroup 要先拦截
触摸目标由坐标决定,但事件要从根一路问下来,父级需要机会在子 View 处理前“截胡” (例如滑动容器拦截子 View 的触摸)。onInterceptTouchEvent() 就是父级的截胡开关; 它返回 true 后,该序列后续事件(ACTION_MOVE/UP)默认继续由父级处理,直到序列结束。 这套规则解释了为什么“子 View 的手势”和“父容器的滑动”常常竞争。
源码证据
阅读目标:确认事件从输入通道进入 View 分发的入口。
正文指针:ViewRootImpl.java deliverInputEvent()/deliverPointerEvent(); ViewGroup.java dispatchTouchEvent()/onInterceptTouchEvent(); View.java dispatchTouchEvent()/onTouchEvent()。
// ViewGroup.java(伪代码,行号以 r75 为准)
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean intercepted = onInterceptTouchEvent(ev);
if (!intercepted) {
// 按坐标与顺序把事件分发给子 View
}
// 子级未消费则自己处理或返回 false
}这段代码证明: 分发是“先拦截判断、再子树分发、最后回溯”的三段结构; dispatchTouchEvent() 是每一层的统一入口,onTouchEvent() 只是最终消费点。
验证与排障
环境:开发设备或模拟器;以下命令不需要 root。
adb shell settings put system show_touches 1
adb logcat -v threadtime | grep -E "ViewRootImpl|InputEventReceiver"开发者选项开启“显示点按操作”可以直观看到触摸位置;logcat 与断点用于确认事件是否 到达应用、进入哪一层分发。View 层问题典型表现为:窗口收到了事件(dumpsys input 有投递)但业务 View 没反应——此时检查坐标是否命中、父级是否拦截、onTouchEvent 是否消费。
常见误区
- “onTouchEvent 是事件的入口”:入口是
dispatchTouchEvent(),onTouchEvent 是末端。 - “拦截只影响当前事件”:拦截后序列后续事件默认由父级接管。
- “View 返回 false 不影响父级”:返回 false 表示未消费,父级会接手或继续回溯。
- “窗口命中 = View 命中”:窗口命中决定事件给哪个窗口,View 命中决定窗口内给谁。
延伸问题
requestDisallowInterceptTouchEvent()如何阻止父级拦截?- ACTION_CANCEL 在什么情况下被发送,View 该如何处理?
- 多点触控下,
getActionMasked()与 pointer index 如何配合分发? - 嵌套滚动协调器(NestedScrolling)如何与拦截机制协作?
源码入口
| 文件 | 关键位置 | 作用 |
|---|---|---|
ViewRootImpl.java | deliverInputEvent() / deliverPointerEvent() | 进入 View 分发 |
ViewGroup.java | dispatchTouchEvent() / onInterceptTouchEvent() | 子树分发与拦截 |
View.java | dispatchTouchEvent() / onTouchEvent() | 单视图分发与消费 |
InputEventReceiver.java | onInputEvent() | 接收入口 |
公共路径:frameworks/base/core/java/android/view/。行号以 r75 检索为准。
需要按层定位输入故障时,见 输入问题排查。