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
和onTouchEvent
return false,事件都回传给父控件的onTouchEvent
处理。dispatchTouchEvent
返回值为 false,意味着事件停止往子View分发,并往父控件回溯。onTouchEvent
返回值为 false,意味着不消费事件,并往父控件回溯。 -
return super.xxxxxx() 就会让事件依照U型的方向的完整走完整个事件流动路径。
ViewGroup
的dispatchTouchEvent
方法返回super
的时候,默认调用onInterceptTouchEvent
-
**
onInterceptTouchEvent
return true时, 拦截事件并交由自己的onTouchEvent
处理 **onInterceptTouchEvent
return 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
方法里回调onClick
onTouch
返回true—>dispatchTouchEvent
方法返回true—>不执行onTouchEvent
,显然onClick
方法也不会被调用
进阶
ACTION_MOVE和ACTION_UP相关
先来看看两个实验:
-
在View的
dispatchTouchEvent
返回false并且在ViewGroup
的onTouchEvent
返回true
红色的箭头代表ACTION_DOWN
事件的流向
蓝色的箭头代表ACTION_MOVE
和ACTION_UP
事件的流向 -
在
ViewGroup
的onTouchEvent
返回true
红色的箭头代表ACTION_DOWN 事件的流向
蓝色的箭头代表ACTION_MOVE 和 ACTION_UP 事件的流向
总结一下:
如果在某个控件的
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开发艺术探索