前言
Android 事件分发机制不仅是日常面试高频考点,同时也是日常开发中滑动冲突、自定义控件、事件拦截、多指触控问题的根源。绝大多数开发者只看重结论:dispatchTouchEvent、onInterceptTouchEvent 、onTouchEvent ,但遇到复杂场景(嵌套 ScrollView、RecyclerView 横向滑动、自定义手势、多指触控冲突)依旧会踩坑。本文将基于 Android 原生 ViewGroup 核心源码,深度剖析事件分发机制。
更多精彩文章,可关注公众号全栈码小峰,随时查看!

一、核心方法
ViewGroup 继承自 View,并实现 View 中的dispatchTouchEvent 和onTouchEvent 方法 。 同时还增加一个onInterceptTouchEvent 方法 。
- dispatchTouchEvent(): 事件分发入口。返回 true:事件被当前控件消费;false:不消费,向上回传给父容器。
- onTouchEvent(): 处理触摸事件;true 代表消费事件,false 代表不处理,事件向上传递。
- onInterceptTouchEvent(): 是否拦截本次事件;true代表拦截,不再传给子 View;false 代码不拦截,将事件继续分发给子View;仅在 dispatchTouchEvent 内部调用。
ViewGroup 中事件分发过程(伪代码)如下:
ini
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
ViewGroup 的 dispatchTouchEvent()方法是整个事件分发机制的核心方法,事件分发过程从 ViewGroup 的 dispatchTouchEvent()开始, 先调用 ViewGroup 的onInterceptTouchEvent()方法判断 ViewGroup 是否自己拦截该事件,如果不拦截再分发给子View 的dispatchTouchEvent()。
二、事件分发
由于ViewGroup 的 dispatchTouchEvent 方法源码太多,这里根据事件分发流程简化涉及主干源码。
ini
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// ① ACTION_DOWN:当新一轮手势开始,清空上一轮触摸目标、重置拦截状态
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// ② 判断是否拦截事件
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (mFirstTouchTarget == null || !onInterceptTouchEvent(ev)) {
intercepted = false;
} else {
intercepted = true;
}
// ③ 不拦截:遍历子View,寻找能够消费事件的子控件
if (!intercepted) {
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// 反序遍历:后add的子View(上层View)优先接收触摸事件
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = mChildren[i];
// 判断触摸点是否落在child范围内、View可见
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(...)) {
continue;
}
// 递归分发事件给子View
if (child.dispatchTouchEvent(ev)) {
// 子View消费事件,创建TouchTarget链表,记录目标View
newTouchTarget = addTouchTarget(child);
break;
}
}
}
}
// ④ 分支判断:根据有无子View 进行触摸事件分发
if (mFirstTouchTarget == null) {
// 情况1:拦截事件 / 没有子View愿意消费
// 调用super.dispatchTouchEvent(),执行ViewGroup自身onTouchEvent
handled = super.dispatchTouchEvent(ev);
} else {
// 情况2:找到目标子View,事件下发给子View
handled = dispatchToTouchTarget(ev);
}
return handled;
}
- ACTION_DOWN:当新一轮手势开始,清空上一轮触摸目标、重置拦截状态
scss
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
当新的触摸事件来时,调用cancelAndClearTouchTargets 和 resetTouchState 将上一轮处理手势的mFirstTouchTarget 链表中的处理触摸事件的对象清除,同时将一些标记位恢复初始值,如mGroupFlags,表示是否允许父控件拦截触摸事件。
- 判断是否拦截事件
ini
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
if (mFirstTouchTarget == null || !onInterceptTouchEvent(ev)) {
intercepted = false;
} else {
intercepted = true;
}
在事件分发过程中,对于 ViewGroup 是否拦截事件有两个条件,第一个是mFirstTouchTarget 是否为 null,表示 ViewGroup 中是否有子View,如果没有子 View 或者onInterceptTouchEvent()返回 false 满足其中任何一个条件都表示不用拦截,这一点很好理解,如果 ViewGroup 中没有其他 View,不管拦不拦截,该事件最终都会交给 ViewGroup 处理。而 onInterceptTouchEvent()默认是不拦截事件的。
- 不拦截:遍历子View,寻找能够消费事件的子控件
ini
if (!intercepted) {
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// 反序遍历:后add的子View(上层View)优先接收触摸事件
for (int i = childrenCount - 1; i >= 0; i--) {
final View child = mChildren[i];
// 判断触摸点是否落在child范围内、View可见
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(...)) {
continue;
}
// 递归分发事件给子View
if (child.dispatchTouchEvent(ev)) {
// 子View消费事件,创建TouchTarget链表,记录目标View
newTouchTarget = addTouchTarget(child);
break;
}
}
}
}
如果当前触摸事件没有被 ViewGroup 拦截,首先遍历查找 ViewGroup 中的子 View,然后递归调用子 View 的dispatchTouchEvent(),将触摸事件分发给子 View,同时将子 View 添加到链表中。
- 根据有无子 View 进行触摸事件分发
ini
if (mFirstTouchTarget == null) {
// 情况1:拦截事件 / 没有子View愿意消费
// 调用super.dispatchTouchEvent(),执行ViewGroup自身onTouchEvent
handled = super.dispatchTouchEvent(ev);
} else {
// 情况2:找到目标子View,事件下发给子View
handled = dispatchToTouchTarget(ev);
}
如果 ViewGroup 中没有子 View愿意消费事件,则调用super.dispatchTouchEvent(),执行ViewGroup自身onTouchEvent,由于 ViewGroup 继承自 View,所以这里调用了 View 中的dispatchTouchEvent。如果有,则找到目标子View,将触摸事件下发给子View。
三、事件处理
当 ViewGroup 中没有子 View 时,则 ViewGroup 调用super.dispatchTouchEvent(ev)进行事件消费。这里调用的是 View 的dispatchTouchEvent(ev)。
ini
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
//1. 停止滑动事件
stopNestedScroll();
}
if (onFilterTouchEventForSecurity(event)) {
//2.是否实现onTouchListener的onTouch方法
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
//3.是否实现了onTouchEvent方法
if (!result && onTouchEvent(event)) {
result = true;
}
}
if (actionMasked == MotionEvent.ACTION_UP ||
actionMasked == MotionEvent.ACTION_CANCEL ||
(actionMasked == MotionEvent.ACTION_DOWN && !result)) {
stopNestedScroll();
}
return result;
}
在 View 的dispatchTouchEvent()方法中,可以看出如果实现了 onTouchListener 监听,并且在 onTouch 方法返回了 True,则后续的 onTouchEvent 不会执行。否则执行 onTouchEvent 方法。
ini
public boolean onTouchEvent(MotionEvent event) {
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除长按事件
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
mIgnoreNextUpEvent = false;
break;
return true;
}
return false;
}
在 onTouchEvent()中,当手指抬起时,触发了performClickInternal()方法,在此方法中调用了performClick()。这里主要处理用户点击事件。
ini
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
return result;
}
所以在一个完整的事件分发过程中,onTouchListener 的 onTouch 方法先执行,然后根据返回值来决定是否执行 onTouchEvent(),最后在 onTouchEvent 中执行onClick 方法。
四、事件拦截
在 ViewGroup 的事件分发过程中,优先调用onInterceptTouchEvent()方法,根据返回值进行事件拦截的判断。
scss
public boolean onInterceptTouchEvent(MotionEvent ev) {
//鼠标事件
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getXDispatchLocation(0), ev.getYDispatchLocation(0))) {
return true;
}
return false;
}
在onInterceptTouchEvent()方法中,除非是鼠标事件,否则默认事件分发是不拦截的。除此之外还有requestDisallowInterceptTouchEvent()方法能影响事件是否允许被拦截。在 ViewGroup 的dispatchTouchEvent() 中,disallowIntercept 决定事件是否不允许拦截。
ini
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
//是否不允许拦截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
而mGroupFlags 的值在requestDisallowInterceptTouchEvent()方法中被赋值,意味着可以通过调用 ViewGroup 的requestDisallowInterceptTouchEvent() 方法来干预事件的拦截。
typescript
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
五、总结
在整个触摸事件分发过程中,由 ViewGroup 的 dispatchTouchEvent 开始。onInterceptTouchEvent 是分发流程中的分支判断节点,onTouchEvent 是事件的终点。所有嵌套滑动冲突、手势抢占、点击失效问题,本质都是这三个方法返回值、调用时机控制不当。读懂 ViewGroup 的 dispatchTouchEvent 主干源码,理解事件分发机原理,解决问题也就事半功倍。