Android事件传递机制一直都是一个痛点,希望这篇文章能够给你点不一样的
基础知识—>源码分析—>进阶—>应用场景
基础知识
触摸事件对应MotionEvent类,三种事件类型:ACTION_DOWN,ACTOIN_MOVE,ACTION_UP。
事件传递的三个阶段:
-
分发(Dispatch)
方法:
public boolean dispatchTouchEvent(MotionEvent ev) -
拦截(Intercept)
方法:
public boolean onInterceptTouchEvent(MotionEvent ev) -
消费(Consume)
方法:
public boolean onTouchEvent(MotionEvent event)
Android中拥有事件处理能力的类有3种:
| 类 | dispatchTouchEvent | onInterceptTouchEvent | onTouchEvent |
|---|---|---|---|
| Activity | ⭕️ | ⭕️ | |
| ViewGroup | ⭕️ | ⭕️ | ⭕️ |
| View | ⭕️ | ⭕️ |
正常状态下事件传递机制如下图(以下仅针对ACTION_DOWN事件):

关于上图有几点说明(仅针对ACTION_DOWN事件的传递):
dispatchTouchEvent和onTouchEvent一旦return true,终结事件传递;-
dispatchTouchEvent和onTouchEventreturn false,事件都回传给父控件的onTouchEvent处理。dispatchTouchEvent返回值为 false,意味着事件停止往子View分发,并往父控件回溯。onTouchEvent返回值为 false,意味着不消费事件,并往父控件回溯。 -
return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径。
ViewGroup的dispatchTouchEvent方法返回super的时候,默认调用onInterceptTouchEvent -
**
onInterceptTouchEventreturn true时, 拦截事件并交由自己的onTouchEvent处理 **onInterceptTouchEventreturn super和false, 不拦截事件,并将事件传递给子View。super.onInterceptTouchEvent(ev)的默认实现返回值为false。
源码分析
知其然,还要知其所以然。通过源码分析,可能会更深刻的理解View的事件分发的真正原理。
Activity的事件分发机制
首先看一下Activity的dispatchTouchEvent源码:
/**
* Called to process touch screen events. You can override this to
* intercept all touch screen events before they are dispatched to the
* window. Be sure to call this implementation for touch screen events
* that should be handled normally.
*
* @param ev The touch screen event.
*
* @return boolean Return true if this event was consumed.
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
// 事件序列开始一般都是ACTION_DOWN,此处一般为true
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
// 空方法,主要用于屏保
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
上面这段代码,关键的就是:getWindow().superDispatchTouchEvent(ev)
Window是抽象类,PhoneWindow是Window的唯一实现类,Window的superDispatchTouchEvent(ev)是一个抽象方法,在PhoneWindow类中看一下superDispatchTouchEvent(ev)的实现:
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
// mDecor是DecorView的实例, DecorView是视图的顶层view,继承自FrameLayout,是所有界面的父类
return mDecor.superDispatchTouchEvent(event);
}
继续追踪一下mDecor.superDispatchTouchEvent(event)方法:
public boolean superDispatchTouchEvent(MotionEvent event) {
// DecorView继承自FrameLayout,那么它的父类就是ViewGroup
// 而super.dispatchTouchEvent(event)方法,其实就应该是ViewGroup的dispatchTouchEvent()
return super.dispatchTouchEvent(event);
}
显然,当一个点击事件发生时,事件最先传到Activity的dispatchTouchEvent进行事件分发,最终是调用了ViewGroup的dispatchTouchEvent方法, 这样事件就从Activity传递到了ViewGroup。
ViewGroup的事件分发机制
-
ViewGroup拦截事件
ViewGroup的
dispatchTouchEvent方法较长,分段进行说明。// Check for interception. final boolean intercepted; // 关注点1 if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 关注点2 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 { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true; }关注点1: 当事件由
ViewGroup子元素成功处理时,会被赋值并指向子元素,即当ViewGroup不拦截事件并将事件交由子元素处理时,mFirstTouchTarget != null成立。-
关注点2:
FLAG_DISALLOW_INTERCEPT标记位,通过requestDisallowInterceptTouchEvent方法进行设置,一般用于子View中。FLAG_DISALLOW_INTERCEPT一旦设置之后,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。原因参见以下代码:// Handle an initial down. if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture. // The framework may have dropped the up or cancel event for the previous gesture // due to an app switch, ANR, or some other state change. cancelAndClearTouchTargets(ev); resetTouchState(); }ViewGroup会在
ACTION_DOWN事件到来时做重置状态的操作。在resetTouchState方法中重置FLAG_DISALLOW_INTERCEPT标记位。因此,子View调用requestDisallowInterceptTouchEvent方法并不能影响ViewGroup对ACTION_DOWN事件的处理。 -
结论:
当ViewGroup决定拦截事件后,那么后续的点击事件将默认交给它处理并且不再调用它的
onInterceptTouchEvent方法。FLAG_DISALLOW_INTERCEPT标记位的作用是让ViewGroup不再拦截事件,前提是ViewGroup不拦截ACTION_DOWN事件。
-
ViewGroup不拦截事件
ViewGroup不拦截事件的时候,事件会向下分发交由它的子View进行处理:
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); // If there is a view that has accessibility focus we want it // to get the event first and if not handled we will perform a // normal dispatch. We may do a double iteration but this is // safer given the timeframe. if (childWithAccessibilityFocus != null) { if (childWithAccessibilityFocus != child) { continue; } childWithAccessibilityFocus = null; i = childrenCount - 1; } // 判断子元素能否接收到点击事件 // 1. 子元素是否在播放动画 // 2. 点击事件的坐标是否落在子元素区域内 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { // Child is already receiving touch within its bounds. // Give it the new pointer in addition to the ones it is handling. newTouchTarget.pointerIdBits |= idBitsToAssign; break; } resetCancelNextUpFlag(child); // 关注点1 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); // 关注点2 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); }-
关注点1:
dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法:if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } -
关注点2: 当子元素的
dispatchTouchEvent返回值为true时,mFirstTouchTarget就会被赋值,并跳出for循环,终止对子元素的遍历:newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true;mFirstTouchTarget被赋值是在addTouchTarget内部实现的:private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }可以看出,
mFirstTouchTarget是一种单链表结构。mFirstTouchTarget是否被赋值将直接影响Viewgroup对事件的拦截策略。如果mFirstTouchTarget为null,ViewGroup默认拦截同一序列中的所有点击事件。 -
关注点3: 当ViewGroup没有子元素,或者子元素的
dispatchTouchEvent返回值为false,在这两种情况下,ViewGroup会自己处理点击事件:// Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }dispatchTransformedTouchEvent的第三个参数child为null,从之前的分析可知,super.dispatchTouchEvent(event)会被调用。
-
View的事件分发机制
View的事件分发机制相对简单一些,先看它的dispatchTouchEvent方法:
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
...
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
ListenerInfo li = mListenerInfo;
// 关注点1
if (li != null && li.mOnTouchListener != null
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
if (!result && onTouchEvent(event)) {
result = true;
}
}
...
return result;
}
代码中可以看出,OnTouchListener优先级高于onTouchEvent。
关注点1:View对点击事件的处理过程,三个判断条件,
-
li != null && li.mOnTouchListener != null: 判断是否设置了OnTouchListener -
(mViewFlags & ENABLED_MASK) == ENABLED:判断当前点击的控件是否enable,很多View默认是enable的,因此该条件恒定为true -
li.mOnTouchListener.onTouch(this, event):回调onTouch方法,如果返回值为true的话,上述三个条件全部成立,从而整个方法直接返回true;返回值为false的时候,就会去执行onTouchEvent(event)方法。
再看一下onTouchEvent的实现:
public boolean onTouchEvent(MotionEvent event) {
...
// 不可用状态下的View照样会消耗点击事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
(viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
focusTaken = requestFocus();
}
if (prepressed) {
// The button is being released before we actually
// showed it as pressed. Make it show the pressed
// state now (before scheduling the click) to ensure
// the user sees it.
setPressed(true, x, y);
}
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
// 关注点1
performClick();
}
}
}
if (mUnsetPressedState == null) {
mUnsetPressedState = new UnsetPressedState();
}
if (prepressed) {
postDelayed(mUnsetPressedState,
ViewConfiguration.getPressedStateDuration());
} else if (!post(mUnsetPressedState)) {
// If the post failed, unpress right now
mUnsetPressedState.run();
}
removeTapCallback();
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
-
关注点1: 当
ACTION_UP事件发生时,会触发performClick方法: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; }如果View设置了
OnClickListener,那么performClick方法内部会调用它的onClick方法。 -
总结:
onTouch的优先级高于onClick
-
控件被点击时,
onTouch返回false—>dispatchTouchEvent方法返回false—>执行onTouchEvent—>在performClick方法里回调onClickonTouch返回true—>dispatchTouchEvent方法返回true—>不执行onTouchEvent,显然onClick方法也不会被调用
进阶
ACTION_MOVE和ACTION_UP相关
先来看看两个实验:
-
在View的
dispatchTouchEvent返回false并且在ViewGroup的onTouchEvent返回true
红色的箭头代表ACTION_DOWN事件的流向
蓝色的箭头代表ACTION_MOVE和ACTION_UP事件的流向
ViewDispatch_2 -
在
ViewGroup的onTouchEvent返回true
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
ViewDispatch_03
总结一下:
如果在某个控件的
dispatchTouchEvent返回true消费终结事件,那么收到ACTION_DOWN的函数也能收到ACTION_MOVE和ACTION_UP。在哪个View的
onTouchEvent返回true,那么ACTION_MOVE和ACTION_UP的事件从上往下传到这个View后就不再往下传递了,而直接传给自己的onTouchEvent并结束本次事件传递过程。-
ACTION_DOWN事件在哪个控件消费了(return true), 那么ACTION_MOVE和ACTION_UP就会从上往下(通过dispatchTouchEvent)做事件分发往下传,就只会传到这个控件,不会继续往下传如果
ACTION_DOWN事件是在dispatchTouchEvent消费,那么事件到此为止停止传递如果
ACTION_DOWN事件是在onTouchEvent消费的,那么会把ACTION_MOVE或ACTION_UP事件传给该控件的onTouchEvent处理并结束传递。
onTouch()和onTouchEvent()的区别
两个方法都是在View的
dispatchTouchEvent中调用,但onTouch优先于onTouchEvent执行。如果在
onTouch方法中返回true将事件消费掉,onTouchEvent将不会再执行。-
View的dispatchTouchEvent方法中:
if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; }onTouch能够执行需要的两个前提:-
mOnTouchListener不为空 - 当前点击的控件必须是
ENABLED
因此如果你有一个控件是非enable的,那么给它注册
onTouch事件将不会执行。 -
应用场景—滑动冲突的解决
滑动冲突在Android开发中一直都是一个痛点,之前的所有讲解,就像是所有的招式,滑动冲突,就是我们的用武之地。
常见滑动冲突场景
-
外部滑动和内部滑动方向不一致
ViewPager和Fragment配合使用组成的页面滑动效果。这种冲突的解决方式,一般都是根据水平滑动还是竖直滑动(滑动的距离差)来判断到底是由谁来拦截事件。
-
外部滑动和内部滑动方向一致
内外两层同时能上下滑动或者能同时左右滑动。这种一般都是根据业务来进行区分。
以上两种场景的嵌套
滑动冲突的解决方式
-
外部拦截法
外部拦截法,就是所有事件都先经过父容器的拦截处理,由父容器来决定是否拦截。这种方式需要重写父容器的
onInterceptTouchEvent方法,伪代码如下:public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercepted = false; int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercepted = false; break; case MotionEvent.ACTION_MOVE: if (父容器需要当前点击事件) { intercepted = true; } else { intercepted = false; } break; case MotionEvent.ACTION_UP: intercepted=false; break; default: break; } mLastXIntercept = x; mLastYIntercept = y; return intercepted; }几点说明:
- 不拦截
ACTION_DOWN事件。一旦父容器拦截ACTION_DOWN,则后续的ACTION_MOVE和ACTION_UP事件都会直接交由父容器处理,无法传递给子元素。 -
ACTION_MOVE事件根据具体需求来决定是否拦截。 -
ACTION_UP事件必须返回false,ACTION_UP事件本身没什么意义,但如果父容器在ACTION_UP返回true会导致子元素无法接收ACTION_UP事件,无法响应onClick事件。
- 不拦截
-
内部拦截法
内部拦截法是指父容器不拦截任何事件,所有事件都传递给子元素。内部拦截法需要配合
requestDisallowInterceptTouchEvent方法才能正常工作。这种方式需要重写子元素的dispatchTouchEvent方法,伪代码如下:public boolean dispatchTouchEvent(MotionEvent ev) { int x = (int) ev.getX(); int y = (int) ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: getParent().requestDisallowInterceptTouchEvent(true); break; case MotionEvent.ACTION_MOVE: int deltaX = x - mLastX; int deltaY = y - mLastY; if (父容器需要当前点击事件) { getParent().requestDisallowInterceptTouchEvent(false); } break; case MotionEvent.ACTION_UP: break; default: break; } mLastX = x; mLastY = y; return super.dispatchTouchEvent(ev); }父元素需要默认拦截除
ACTION_DOWN事件以外的其他事件,父元素修改如下:public boolean onInterceptTouchEvent(MotionEvent ev) { if (ev.getAction()==MotionEvent.ACTION_DOWN) { return false; } else { return true; } }ACTION_DOWN事件并不受FLAG_DISALLOW_INTERCEPT这个标记位的控制。一旦父容器拦截ACTION_DOWN事件,那么所有的事件都无法传递到子元素中去。
参考
- 图解Android事件分发机制
- Android事件分发机制详解:史上最全面、最易懂
- Android开发艺术探索