一文读懂Android触摸事件分发机制:从源码到实战,彻底搞定滑动冲突
前言
事件分发是Android开发中的"拦路虎",很多工作2-3年的开发者也未必能说清楚。网上文章虽多,但要么太理论化,要么太源码化,看完就忘。
今天,我用讲故事 + 源码分析 + 实战案例的方式,带你彻底搞懂事件分发。
读完这篇文章,你将收获:
- ✅ 彻底理解三个核心方法的作用和关系
- ✅ 掌握事件分发的完整流程(向下传递 + 向上回传)
- ✅ 看懂核心源码,不再死记硬背
- ✅ 轻松解决各种滑动冲突
- ✅ 面试时侃侃而谈,惊艳面试官
一、故事开篇:一家公司的任务派发
想象你在一家公司工作,公司结构是这样的:
老板(Activity)
└── 部门经理(Window)
└── 项目组长(ViewGroup,比如LinearLayout)
└── 程序员(View,比如Button)
某天,客户提了一个需求(用户点了一下屏幕),这个任务需要从老板一层层下派,找到能干的员工。
这家公司有三条奇葩规定:
| 规定 | 对应方法 | 作用 |
|---|---|---|
| 任务派发制度 | dispatchTouchEvent | "这活儿谁来干?" |
| 中途拦截制度 | onInterceptTouchEvent | "这活儿我中途截了,自己干!"(只有组长及以上有这权力) |
| 任务执行制度 | onTouchEvent | "行,这事儿我来搞定!" |
这三者的关系,用伪代码一看就懂:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onInterceptTouchEvent(ev)) { // 要不要中途拦截?
return onTouchEvent(ev); // 拦截了,自己处理
} else {
return child.dispatchTouchEvent(ev); // 不拦截,下发给小弟
}
}
二、三个核心方法详解
1. dispatchTouchEvent - 分发者
职责: 事件分发的总入口,决定事件是"自己处理"还是"往下传"。
调用时机: 只要事件能传递到当前View,此方法一定会被调用。
返回值: true 表示事件被消耗,false 表示没有被消耗(会向上回传)。
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 你可以在这里做任何预处理
Log.d("TAG", "事件到达了:" + this.getClass().getSimpleName());
// 调用父类执行默认分发逻辑
return super.dispatchTouchEvent(ev);
}
2. onInterceptTouchEvent - 拦截者(只有ViewGroup有)
职责: 决定是否拦截事件,拦截后事件就不再往下传。
调用时机: 在dispatchTouchEvent内部被调用,不是每次事件都会调用。
返回值: true 表示拦截,false 表示不拦截(默认)。
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
switch(ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// 通常在DOWN时做初始化
return false; // 默认不拦截
case MotionEvent.ACTION_MOVE:
// 根据滑动距离决定是否拦截
if (Math.abs(deltaX) > touchSlop) {
return true; // 拦截,自己处理
}
break;
}
return super.onInterceptTouchEvent(ev);
}
3. onTouchEvent - 处理者
职责: 实际处理事件,比如点击、滑动等。
调用时机: 在dispatchTouchEvent内部被调用(要么自己被调用,要么子View返回false后自己被调用)。
返回值: true 表示消耗了事件,false 表示没消耗(会向上回传)。
@Override
public boolean onTouchEvent(MotionEvent event) {
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 手指按下
return true; // 我要处理这个事件序列
case MotionEvent.ACTION_MOVE:
// 手指移动
performDrag(event);
return true;
case MotionEvent.ACTION_UP:
// 手指抬起
performClick();
return true;
}
return super.onTouchEvent(event);
}
三、完整流程图解
流程图1:正常情况(Button响应点击)
流程图2:拦截情况(ViewGroup抢活)
流程图3:向上回传(Button搞不定)
四、核心规则(面试必背)
规则1:事件序列 = 一个完整的手势
从手指按下(DOWN)到抬起(UP),中间可能有多个移动(MOVE),这叫做一个事件序列。
ACTION_DOWN → ACTION_MOVE → ACTION_MOVE → ... → ACTION_MOVE → ACTION_UP
重要: 同一个序列的事件,通常会交给同一个View处理。
规则2:一旦拦截,全程接管 ⭐⭐⭐
这是最重要的规则!
如果某个ViewGroup拦截了任意一个事件(DOWN、MOVE或UP),那么这个序列的后续所有事件都归它处理:
- ❌ 不会再往下传给子View
- ❌ 不会再调用
onInterceptTouchEvent(不用再问了)
// 示例:在MOVE时拦截
case MotionEvent.ACTION_MOVE:
if (Math.abs(deltaX) > 10) {
return true; // 拦截!
}
// 后果:后续的MOVE和UP都不会再传给子View了
规则3:不消费DOWN,后果很严重 ⭐⭐⭐
如果一个View的onTouchEvent在DOWN事件时返回false:
- ❌ 这个序列的后续事件(MOVE、UP)都不会再给它了
- 🔄 事件会回传给父View处理
为什么? 因为ViewGroup内部维护了一个mFirstTouchTarget链表,只有消费了DOWN的View才会被记录进去。
// 源码验证
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 只有子View返回true,才会被记录
newTouchTarget = addTouchTarget(child, idBitsToAssign);
}
// 后续事件判断
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
// 有记录才会继续分发
} else {
intercepted = true; // 没有记录,直接拦截,不再分发
}
规则4:View默认"能吃下"事件
只要View满足以下任一条件:
clickable = true(可点击)longClickable = true(可长按)
那么它的onTouchEvent默认返回true(消耗事件)。
注意: enable = false(禁用状态)也不影响这个规则!
// Button默认clickable = true
// TextView默认clickable = false
button.setOnTouchListener(...); // 会自动设置clickable = true
规则5:优先级(重要!)
OnTouchListener.onTouch > onTouchEvent > OnClickListener.onClick
button.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
// 如果这里返回true,onTouchEvent和onClick都不会执行
return true;
}
});
button.setOnClickListener(v -> {
// 只有当onTouch返回false,且onTouchEvent处理了,才会执行
});
规则6:子View可以请求父View别拦截
// 在子View中调用
getParent().requestDisallowInterceptTouchEvent(true);
作用: 告诉父View:"这活我自己干,你别抢!"
注意: 这招对DOWN事件无效,因为每次DOWN事件开始时,系统会重置这个标志位。
// 源码证明
if (actionMasked == MotionEvent.ACTION_DOWN) {
// DOWN事件会重置状态
cancelAndClearTouchTargets(ev);
resetTouchState(); // 这里会清除FLAG_DISALLOW_INTERCEPT标志
}
五、源码核心解读
1. Activity → ViewGroup
// Activity.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent ev) {
if (getWindow().superDispatchTouchEvent(ev)) {
return true; // 有人处理了
}
return onTouchEvent(ev); // 没人处理,Activity自己处理
}
// PhoneWindow.superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event); // mDecor是顶级View
}
2. ViewGroup.dispatchTouchEvent(核心逻辑)
public boolean dispatchTouchEvent(MotionEvent ev) {
// 1. 判断是否拦截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev); // 询问是否拦截
} else {
intercepted = false;
}
} else {
intercepted = true; // 没有目标且不是DOWN,直接拦截
}
// 2. 如果不拦截,分发给子View
if (!intercepted) {
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = children[i];
if (isTransformedTouchPointInView(x, y, child, null)) {
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 子View处理了,记录下来
mFirstTouchTarget = addTouchTarget(child, idBitsToAssign);
break;
}
}
}
}
// 3. 如果没有子View处理,自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, ...);
}
return handled;
}
3. View.dispatchTouchEvent
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 优先给OnTouchListener处理
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true; // OnTouchListener消费了
}
// OnTouchListener没消费,交给onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
return result;
}
六、实战:解决滑动冲突
场景1:ViewPager + RecyclerView(经典嵌套滑动)
问题描述: ViewPager里嵌套了纵向滚动的RecyclerView,用户斜着滑时,两个控件都想抢事件。
期望行为:
- 左右滑 → ViewPager翻页
- 上下滑 → RecyclerView滚动
解决方案: 在RecyclerView中判断滑动方向
public class CustomRecyclerView extends RecyclerView {
private float lastX, lastY;
private int touchSlop;
public CustomRecyclerView(Context context, AttributeSet attrs) {
super(context, attrs);
touchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
}
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastX = ev.getX();
lastY = ev.getY();
// DOWN事件:先请求父View不要拦截
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
float deltaX = ev.getX() - lastX;
float deltaY = ev.getY() - lastY;
// 判断滑动方向
if (Math.abs(deltaX) > Math.abs(deltaY) + touchSlop) {
// 横向滑动:交给ViewPager处理
getParent().requestDisallowInterceptTouchEvent(false);
return false; // 自己不处理
} else if (Math.abs(deltaY) > touchSlop) {
// 纵向滑动:自己处理,阻止父View拦截
getParent().requestDisallowInterceptTouchEvent(true);
}
break;
}
return super.dispatchTouchEvent(ev);
}
}
场景2:可滑动的ViewPager + 可滑动的内部View(水平冲突)
问题描述: ViewPager里嵌套了横向滚动的RecyclerView,用户想滑动内部列表时,ViewPager总是抢走事件。
期望行为:
- 在内部RecyclerView上滑动 → 内部列表滚动
- 在空白区域滑动 → ViewPager翻页
解决方案: 使用requestDisallowInterceptTouchEvent
public class HorizontalRecyclerView extends RecyclerView {
@Override
public boolean onTouchEvent(MotionEvent e) {
switch (e.getAction()) {
case MotionEvent.ACTION_DOWN:
// 告诉ViewPager:先别拦截,让我看看
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_MOVE:
// 如果已经滑动到边界,让ViewPager处理
if (!canScrollHorizontally(deltaX > 0 ? 1 : -1)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
return super.onTouchEvent(e);
}
}
场景3:自定义下拉刷新布局
public class PullToRefreshLayout extends FrameLayout {
private float lastY;
private boolean isRefreshing = false;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (isRefreshing) {
return false; // 刷新中,不拦截
}
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
lastY = ev.getY();
return false; // 不拦截,让子View先处理
case MotionEvent.ACTION_MOVE:
float deltaY = ev.getY() - lastY;
// 下拉且子View已经到顶部,拦截事件自己处理
if (deltaY > 20 && !canChildScrollUp()) {
return true; // 拦截,开始下拉刷新
}
break;
}
return false;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理下拉刷新的动画逻辑
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float deltaY = event.getY() - lastY;
// 移动下拉头
moveHeader(deltaY);
return true;
case MotionEvent.ACTION_UP:
// 执行刷新或回弹
if (isReadyToRefresh()) {
startRefresh();
} else {
resetHeader();
}
return true;
}
return super.onTouchEvent(event);
}
private boolean canChildScrollUp() {
View child = getChildAt(0);
if (child instanceof ViewGroup) {
return child.canScrollVertically(-1);
}
return false;
}
}
七、常见问题 FAQ
Q1:onInterceptTouchEvent 为什么不是每次事件都会被调用?
A:因为一旦ViewGroup决定拦截,后续事件就不会再调用它了。
// 源码中的判断
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
intercepted = onInterceptTouchEvent(ev); // 只有这里会调用
} else {
intercepted = true; // 直接拦截,不调用
}
Q2:requestDisallowInterceptTouchEvent 为什么对 DOWN 无效?
A:因为每次DOWN事件开始时,系统会重置状态。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState(); // 这里会清除 FLAG_DISALLOW_INTERCEPT
}
Q3:一个View既设置了OnTouchListener又设置了OnClickListener,会怎样?
A:先执行OnTouchListener.onTouch,如果返回false才执行onTouchEvent,然后才可能触发onClick。
// 执行顺序
OnTouchListener.onTouch()
→ 如果返回false → onTouchEvent()
→ 如果消费了且是UP事件 → onClick()
Q4:View的enable=false时,还能收到点击事件吗?
A:能收到,但不会响应点击。只要clickable=true,onTouchEvent仍然返回true。
// 源码验证
if ((viewFlags & ENABLED_MASK) == DISABLED) {
// 即使disabled,只要clickable或longClickable为true,仍然返回true
return (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE));
}
Q5:如何调试事件分发问题?
A:使用adb命令或自定义日志。
// 方法1:打印事件日志
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
Log.d("Event", getClass().getSimpleName() + " dispatch: " + ev.getAction());
return super.dispatchTouchEvent(ev);
}
// 方法2:使用dumpsys
adb shell dumpsys input
// 方法3:使用Layout Inspector查看View层级
八、记忆口诀
事件分发三方法,分发拦截和处理
Activity先收到,一层一层往下递
领导能拦也能放,员工能吃不放弃
一旦拦截全接管,后面不再问你哩
搞不定就往回传,最终老板兜个底
监听优先于回调,onClick排最后滴
DOWN事件最特殊,不消费就没后续
子View想抢控制权,请求父View别拦截
九、总结对比表
| 对比项 | dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
|---|---|---|---|
| 谁有 | View和ViewGroup | 只有ViewGroup | View和ViewGroup |
| 作用 | 分发事件 | 拦截事件 | 处理事件 |
| 默认返回值 | 取决于内部逻辑 | false | true(如果clickable) |
| 调用时机 | 每次事件都会 | 有条件地调用 | 事件交给当前View时 |
| 是否必须实现 | 一般不重写 | 根据需要重写 | 根据需要重写 |
| 规则 | 说明 |
|---|---|
| 传递顺序 | Activity → Window → ViewGroup → View(由外向内) |
| 回传顺序 | View → ViewGroup → Window → Activity(由内向外) |
| 拦截后果 | 后续事件全部自己处理,不再往下传 |
| DOWN不消费 | 后续事件收不到,向上回传 |
| 优先级 | OnTouchListener > onTouchEvent > OnClickListener |
| 默认消费 | clickable或longClickable为true时,onTouchEvent返回true |