5、事件分发机制(上):MotionEvent的生成与传递,dispatchTouchEvent与onInterceptTouchEvent的协作
各位同学,今天我们来聊聊Android事件分发机制的上半部分。说实话,这个知识点是面试高频区,也是很多开发者容易踩坑的地方。我个人习惯把事件分发比作一个「层层审批」的过程——从系统底层到你的手指触摸屏幕,再到ViewGroup和View的决策,每一步都有它的规则。
MotionEvent是怎么来的?
先说说MotionEvent的生成。你想想看,当你手指按在屏幕上,硬件层会先感知到电容变化,然后驱动层把原始信号转换成Linux内核的输入事件。接着,InputReader线程读取这些事件,InputDispatcher负责分发。
嗯,这里有个关键点:InputDispatcher会把事件发送到当前聚焦窗口的ViewRootImpl。ViewRootImpl内部通过Choreographer机制,在下一个VSync信号到来时,调用View的dispatchTouchEvent方法。
我在项目中遇到过一个问题:某些低端机型上触摸响应延迟明显。后来排查发现,是InputDispatcher和UI线程之间的消息队列积压导致的。说白了,就是事件从内核到应用层的链路太长,任何一个环节阻塞都会影响手感。
MotionEvent的传递路径:
硬件驱动 → Linux内核 → InputReader → InputDispatcher → ViewRootImpl → Activity → ViewGroup → View
dispatchTouchEvent:事件分发的总入口
每个View和ViewGroup都有dispatchTouchEvent方法。这个方法决定了事件该往哪走。我习惯把它理解成「事件路由」——它根据返回值决定是否继续传递。
- 返回true:表示当前View或ViewGroup消费了事件,不再往下传
- 返回false:表示不消费,事件会回传给父View的onTouchEvent
- 调用super.dispatchTouchEvent():走默认分发逻辑
这里有个容易混淆的点:dispatchTouchEvent的默认实现,在View和ViewGroup中是不一样的。View的默认实现会直接调用onTouchEvent;而ViewGroup的默认实现会先调用onInterceptTouchEvent判断是否拦截,再遍历子View分发。
避坑指南:我曾经在自定义ViewGroup中重写了dispatchTouchEvent,但没有调用super,导致子View完全收不到事件。后来花了半天才定位到问题——dispatchTouchEvent的返回值必须谨慎处理,不是简单的true/false就能解决的。
onInterceptTouchEvent:拦截决策的关键
onInterceptTouchEvent是ViewGroup特有的方法。它决定了当前ViewGroup是否要「截胡」事件,不让子View处理。
你想想看,为什么需要这个方法?举个例子:一个ScrollView里面嵌套了一个可以横向滑动的ViewPager。手指横向滑动时,ScrollView不应该拦截;手指纵向滑动时,ScrollView需要拦截。这个决策就是在onInterceptTouchEvent中完成的。
| 返回值 | 含义 | 后续行为 |
|---|---|---|
| false | 不拦截 | 事件继续传递给子View的dispatchTouchEvent |
| true | 拦截 | 事件交给当前ViewGroup的onTouchEvent处理 |
嗯,这里要注意:一旦onInterceptTouchEvent返回true,后续的MOVE和UP事件都不会再调用onInterceptTouchEvent,而是直接交给当前ViewGroup处理。除非你主动调用requestDisallowInterceptTouchEvent来改变这个状态。
dispatchTouchEvent与onInterceptTouchEvent的协作流程
我画了一张图来展示这个协作过程,你看完应该就明白了:
从图中你可以看到,整个流程是这样的:
- Activity收到事件,调用根ViewGroup的dispatchTouchEvent
- ViewGroup的dispatchTouchEvent先调用onInterceptTouchEvent做拦截判断
- 如果拦截:直接走自己的onTouchEvent,事件不再往下传
- 如果不拦截:遍历子View,找到触摸点所在的子View,调用它的dispatchTouchEvent
- 子View处理完后,把结果返回给父ViewGroup
注意:事件分发是「责任链模式」的典型应用。每个层级都有机会处理事件,但一旦某个层级消费了事件(dispatchTouchEvent返回true),上层就不会再收到后续事件。我曾经在嵌套滑动场景中踩过这个坑——内层View消费了DOWN事件,但MOVE事件却丢失了,就是因为没有正确处理事件序列的连续性。
事件序列的连续性
一个完整的手势由多个事件组成:DOWN、MOVE、UP。系统保证同一个手势序列的所有事件都发给同一个View。怎么保证的?
说白了,就是DOWN事件决定了目标。一旦某个View在DOWN事件中返回true,后续的MOVE和UP都会直接发给它,不会再走拦截流程。除非父View调用了requestDisallowInterceptTouchEvent来强制改变。
嗯,这里有个细节:如果子View在DOWN事件中返回false,父View的onTouchEvent会收到DOWN事件。但如果父View在DOWN事件中也返回false,事件就会继续向上传递,直到Activity。如果Activity也不处理,事件就消失了。
核心原则:
- DOWN事件决定事件序列的归属
- 一旦归属确定,后续事件不再走拦截判断
- 子View可以通过requestDisallowInterceptTouchEvent阻止父View拦截
实战中的常见场景
我在项目中遇到过这样一个需求:一个横向滑动的列表,每个item里面有一个可以纵向滑动的区域。如果不做处理,横向滑动和纵向滑动会冲突。
解决方案其实不复杂:在父View的onInterceptTouchEvent中,根据手指滑动的方向做判断。如果横向位移大于纵向位移,就拦截事件;否则放行给子View。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 记录起始点
lastX = ev.getX();
lastY = ev.getY();
break;
case MotionEvent.ACTION_MOVE:
float deltaX = Math.abs(ev.getX() - lastX);
float deltaY = Math.abs(ev.getY() - lastY);
// 横向滑动大于纵向,拦截
if (deltaX > deltaY && deltaX > touchSlop) {
return true;
}
break;
}
return super.onInterceptTouchEvent(ev);
}
这个代码看起来简单,但实际调试时你会发现很多坑。比如touchSlop的值在不同设备上不一样,需要从ViewConfiguration中获取。再比如,如果子View调用了requestDisallowInterceptTouchEvent,你的拦截逻辑就会失效。
个人经验:我建议在自定义ViewGroup时,先搞清楚你的拦截逻辑是否会被子View的requestDisallowInterceptTouchEvent影响。如果不需要子View干预,可以在onInterceptTouchEvent中忽略这个标志位。但大多数情况下,保持默认行为更安全。
总结一下
事件分发机制的上半部分,核心就是理解MotionEvent的生成路径,以及dispatchTouchEvent和onInterceptTouchEvent的协作关系。你想想看,整个流程其实就是一个「层层过滤」的过程——每个ViewGroup都有机会拦截事件,但一旦放行,子View就有机会处理。
嗯,这里要记住:事件分发的核心是DOWN事件。它决定了整个事件序列的走向。后续的MOVE和UP只是沿着DOWN事件确定的路径走而已。
我在做系统UI开发时,经常需要处理各种嵌套滑动场景。说实话,事件分发机制理解透了,这些场景其实都有固定的套路可循。关键是要搞清楚每个方法的调用时机和返回值含义。
公众号:蓝海资料掘金营,微信deep3321