事件分发流程
当一个点击事件产生后,事件总是先传递给Activity, Activity再传递给Window,最后Window再传递给顶级View,顶级View接收到事件后,就会按照事件分发机制去分发事件。
它的传递过程遵循如下顺序:Activity -> Window -> DecorView -> 层层ViewGroup -> 子View
如果没有任何View消耗事件,事件会依次往回传递(从子View到父View),最后到Activity的onTouchEvent。
事件分发的三个核心方法
dispatchTouchEvent
用来进行事件的分发。如果事件能够传递给当前View,那么此方法一定会被调用,返回结果受当前View的onTouchEvent和下级View的dispatchTouchEvent方法的影响,表示是否消耗当前事件。
- 所属类:View/ViewGroup
- 作用:事件分发入口
- 返回值意义:true事件被消费,false事件未处理
onInterceptTouchEvent
在dispatchTouchEvent方法中调用,用来判断是否拦截某个事件,如果当前View拦截了某个事件,那么在同一个事件序列当中,此方法不会被再次调用,返回结果表示是否拦截当前事件。
- 所属类:ViewGroup
- 作用:是否拦截事件
- 返回值意义:true拦截,子view不接收,false不拦截
onTouchEvent
在dispatchTouchEvent方法中调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一个事件序列中,当前View无法再次接收到事件。
- 所属类:View
- 作用:事件处理
- 返回值意义:true事件被处理,false事件未处理
伪代码表示
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean consume = false;
if (onInterceptTouchEvent(ev)) {
consume = onTouchEvent(ev);
} else {
consume = child.dispatchTouchEvent(ev);
}
return consume;
}
对于一个根ViewGroup来说,点击事件产生后,首先会传递给它,这时它的dispatchTouchEvent就会被调用,如果这个ViewGroup的onInterceptTouchEvent方法返回true就表示它要拦截当前事件,接着事件就会交给这个ViewGroup处理,即它的onTouchEvent方法就会被调用;如果这个ViewGroup的onInterceptTouchEvent方法返回false就表示它不拦截当前事件,这时当前事件就会继续传递给它的子元素,接着子元素的dispatchTouchEvent方法就会被调用,如此反复直到事件被最终处理。
事件分发源码深度剖析
Activity的事件分发入口
// Activity.java - 事件分发的起点
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction(); // 空方法,可重写用于监听用户交互
}
// 1. 将事件交给Window
if (getWindow().superDispatchTouchEvent(ev)) {
return true; // Window处理了事件
}
// 2. Window没处理,Activity自己处理
return onTouchEvent(ev);
}
// PhoneWindow.java
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event); // 交给DecorView
}
首先事件开始交给Activity所附属的Window进行分发,如果返回true,整个事件循环就结束了,返回false意味着事件没人处理,所有View的onTouchEvent都返回了false,那么Activity的onTouchEvent就会被调用。
ViewGroup的dispatchTouchEvent()核心逻辑
ViewGroup.dispatchTouchEvent()是事件分发机制中最复杂、最核心的方法
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
boolean handled = false;
// 1. 安全检查:事件是否被过滤
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// 2. 处理ACTION_DOWN:新的事件序列开始
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev); // 清除之前的触摸目标
resetTouchState(); // 重置状态
}
// 3. 检查是否拦截
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); // 恢复action,防止被修改
} else {
intercepted = false; // 禁止拦截
}
} else {
// 没有触摸目标且不是DOWN事件,直接拦截
intercepted = true;
}
// 4. 如果没有拦截且不是取消事件
if (!intercepted && !canceled) {
// 5. 如果是DOWN/POINTER_DOWN事件,寻找新的触摸目标
if (actionMasked == MotionEvent.ACTION_DOWN
|| (actionMasked == MotionEvent.ACTION_POINTER_DOWN
&& !isMouseEvent)) {
// 遍历子View,寻找能接收事件的View
final int childrenCount = mChildrenCount;
if (childrenCount != 0) {
// 从后向前遍历(Z轴顺序,后面的在上层)
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 检查子View是否能接收触摸事件
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue; // 不能接收或坐标不在范围内,跳过
}
// 分发给子View
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 子View处理了事件
mLastTouchDownTime = ev.getDownTime();
mLastTouchDownIndex = i;
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break; // 找到目标,跳出循环
}
}
}
}
}
// 6. 如果没有找到目标,自己处理
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 7. 有目标,分发给目标
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true; // 已经处理过
} else {
// 传递给目标
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
}
predecessor = target;
target = next;
}
}
}
return handled;
}
重点说明
1.拦截判断
ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget ! = null(当事件由ViewGroup的子元素成功处理时,mFirstTouchTarget会被赋值并指向子元素)。
2.FLAG_DISALLOW_INTERCEPT
FLAG_DISALLOW_INTERCEPT这个标记位是通过requestDisallowInterceptTouchEvent方法来设置的,一般用于子View中。一旦设置后,ViewGroup将无法拦截除了ACTION_DOWN以外的其他点击事件(因为ACTION_DOWN会触发重置状态,导致该标志位无效)。
3.子元素能否接收
当ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理。是否能够接收点击事件主要由两点来衡量:子元素是否在播动画和点击事件的坐标是否落在子元素的区域内。
// view.java
protected boolean canReceivePointerEvents() {
// 是否在播动画
return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
}
// View.java
public boolean pointInView(float localX, float localY) {
return localX >= 0 && localX < (mRight - mLeft)
&& localY >= 0 && localY < (mBottom - mTop);
}
// ViewGroup.java
// 检查触摸点是否在子View的范围内
protected boolean isTransformedTouchPointInView(float x, float y, View child,
PointF outLocalPoint) {
final float[] point = getTempLocationF();
point[0] = x;
point[1] = y;
// 1. 转换坐标到子View的坐标系
transformPointToViewLocal(point, child);
// 2. 检查是否在子View边界内
final boolean isInView = child.pointInView(point[0], point[1]);
if (isInView && outLocalPoint != null) {
outLocalPoint.set(point[0], point[1]);
}
return isInView;
}
4.实际分发事件
dispatchTransformedTouchEvent方法,实际上调用的就是子元素的dispatchTouchEvent方法。在它的内部有如下一段内容,而在上面的代码中child传递的不是null,因此它会直接调用子元素的dispatchTouchEvent方法(具体实现之后再关注),这样事件就交由子元素处理了,从而完成了一轮事件分发。
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
handled = child.dispatchTouchEvent(transformedEvent);
}
}
如果子元素的dispatchTouchEvent返回true,mFirstTouchTarget就会被赋值,同时跳出for循环。 mFirstTouchTarget真正的赋值过程在addTouchTarget内部完成。
// ViewGroup.java
public boolean dispatchTouchEvent(MotionEvent ev) {
// ······
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break; // 找到目标,跳出循环
// ······
}
private TouchTarget addTouchTarget(View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
如果没有找到目标,这包含两种情况:第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回了false,这一般是因为子元素在onTouchEvent中返回了false。在这两种情况下,ViewGroup会自己处理点击事件,这里第三个参数child为null,它会调用super.dispatchTouchEvent(event)。
if (mFirstTouchTarget == null) {
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
}
如果找到目标,就转到了View的dispatchTouchEvent方法,即点击事件开始交由View来处理。
执行优先级:OnTouchListener.onTouch()> onTouchEvent(),如果OnTouchListener消费了事件,则不会执行onTouchEvent。
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
// 1. 安全检查
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 2. 优先处理OnTouchListener
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true; // OnTouchListener消费了事件
}
// 3. 如果OnTouchListener没有消费,调用onTouchEvent
if (!result && onTouchEvent(event)) {
result = true; // onTouchEvent消费了事件
}
}
return result;
}
onTouchEvent()的默认实现
// View.java
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 1. 检查View是否可用
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
// 2. 检查是否有触摸代理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// 3. 检查View是否可点击
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_DOWN:
// 按下状态
mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
if (!clickable) {
setPressed(true, x, y);
}
break;
case MotionEvent.ACTION_MOVE:
// 检查是否还在View范围内
if (!pointInView(x, y, TOUCH_SLOP)) {
removeTapCallback();
removeLongPressCallback();
if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
}
break;
case MotionEvent.ACTION_UP:
// 触发点击事件
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 触发点击
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
}
break;
case MotionEvent.ACTION_CANCEL:
// 取消状态
setPressed(false);
removeTapCallback();
removeLongPressCallback();
break;
}
return true; // 可点击的View默认消耗事件
}
return false; // 不可点击的View不消耗事件
}
重要结论:
- 只要View是可点击的(clickable、longClickable、contextClickable),无论是否disable,它的
onTouchEvent()就会返回true,表示消费事件。 - 当ACTION_UP事件发生时,会触发performClick方法,如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li ! = null && li.mOnClickListener ! = null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this); result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
事件分发总结
- 同一事件序列:从ACTION_DOWN开始,到ACTION_UP或ACTION_CANCEL结束
- 一旦消费,全程负责:如果View消费了ACTION_DOWN,整个事件序列都会交给它
- 拦截特权:ViewGroup可以在onInterceptTouchEvent中拦截事件
- 禁止拦截:子View可以通过requestDisallowInterceptTouchEvent()禁止父View拦截(对ACTION_DOWN无效)
- OnTouchListener优先:优先级高于onTouchEvent
- 可点击性决定消费:可点击的View默认消费事件
滑动冲突解决方案
滑动冲突的三种类型
| 类型 | 场景 | 示例 |
|---|---|---|
| 内外滑动方向不同 | 外部和内部滑动方向垂直 | ViewPager内嵌ListView |
| 内外滑动方向相同 | 外部和内部滑动方向一致 | ScrollView内嵌ListView |
| 嵌套组合 | 以上两种组合 | 多层嵌套 |
解决方案一:外部拦截法(父View主导)
父View根据需要决定是否拦截,重写onInterceptTouchEvent():
public class ParentView extends ViewGroup {
private float mLastX, mLastY;
private float mInterceptX, mInterceptY;
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
boolean intercepted = false;
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
intercepted = false; // 必须返回false,否则子View无法接收事件
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastX;
float deltaY = y - mLastY;
// 判断滑动方向:水平滑动距离 > 垂直滑动距离
if (Math.abs(deltaX) > Math.abs(deltaY)) {
intercepted = true; // 水平滑动,父View拦截
} else {
intercepted = false; // 垂直滑动,父View不拦截
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
intercepted = false; // UP事件不拦截,让子View处理点击
break;
}
return intercepted;
}
@Override
public boolean onTouchEvent(MotionEvent event) {
// 处理父View的滑动逻辑
switch (event.getAction()) {
case MotionEvent.ACTION_MOVE:
float deltaX = event.getX() - mInterceptX;
// 水平滑动处理
scrollBy(-(int) deltaX, 0);
mInterceptX = event.getX();
break;
}
return true;
}
}
外部拦截法要点:
- ACTION_DOWN必须返回false
- ACTION_MOVE根据业务逻辑判断
- ACTION_UP一般返回false
内部拦截法(子View主导)
子View通过requestDisallowInterceptTouchEvent()控制父View的拦截:
public class ChildView extends ViewGroup {
private float mLastX, mLastY;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
float x = ev.getX();
float y = ev.getY();
switch (ev.getAction()) {
case MotionEvent.ACTION_DOWN:
// ❗禁止父View拦截
getParent().requestDisallowInterceptTouchEvent(true);
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_MOVE:
float deltaX = x - mLastX;
float deltaY = y - mLastY;
// 判断如果是垂直滑动,允许父View拦截
if (Math.abs(deltaY) > Math.abs(deltaX)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
mLastX = x;
mLastY = y;
break;
case MotionEvent.ACTION_UP:
getParent().requestDisallowInterceptTouchEvent(false);
break;
}
return super.dispatchTouchEvent(ev);
}
}
// 父View需要配合修改
public class ParentView extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
return false;
} else {
return true; // 默认拦截非DOWN事件
}
}
}
内部拦截法要点:
- 子View在ACTION_DOWN时请求禁止拦截
- 子View在ACTION_MOVE中根据条件决定是否允许拦截
- 父View需要配合,默认拦截非DOWN事件
性能优化
减少事件分发层级
每多一层ViewGroup,事件分发就要多遍历一次。
<!-- 优化前:多层嵌套 -->
<LinearLayout>
<LinearLayout>
<LinearLayout>
<TextView />
</LinearLayout>
</LinearLayout>
</LinearLayout>
<!-- 优化后:减少层级 -->
<ConstraintLayout>
<TextView />
</ConstraintLayout>
避免在onTouchEvent中频繁创建对象
// 错误:每次触摸都创建新对象
@Override
public boolean onTouchEvent(MotionEvent event) {
Rect rect = new Rect(); // 频繁创建
getDrawingRect(rect);
return rect.contains((int) event.getX(), (int) event.getY());
}
// 正确:复用对象
private Rect mTempRect = new Rect();
@Override
public boolean onTouchEvent(MotionEvent event) {
getDrawingRect(mTempRect);
return mTempRect.contains((int) event.getX(), (int) event.getY());
}