系列文章传送门 (持续更新中..) :
- 很多安卓初学者都对 View 的事件分发机制感到困惑,但是这是务必要掌握的知识点。日常开发中要处理复杂的滑动冲突问题,就需要对事件分发的流程足够熟悉。在上一篇文章里, 我们了解了 Activity 的窗口结构, 今天我们看一下 View 的点击事件具体是怎样分发。
话不多说, 先上图
图片取自 -- 这里分析的点击事件, 首先我们要明白分析的对象就是 MotionEvent。当一个点击事件产生时, 它的传递顺序是 Activity -> Window(PhoneWindow) -> View(DecorView),即事件先传递给 Activity,Activity再传给PhoneWindow,最后再传递给顶级View即DecorView。DecorView 接收到事件后,就会按照事件分发机制去分发事件。
事件分发的过程由三个很重要的方法来共同完成:
-
public boolean dispatchTouchEvent(MotionEvent event) { }
- 用来事件的分发。如果 view 能接收到事件,那么此方法一定会调用。返回的结果受当前 view 的 onTouchEvent 和下级 view 的 onInterceptTouchEvent 的结果影响,表示是否分发当前事件
-
public boolean onInterceptTouchEvent(MotionEvent ev) { }
- 在上面方法内部调用,用来决定是否拦截某个事件。如果当前 view 拦截了某个事件,那么在同一事件序列中,此方法不会再次调用。返回结果表示是否拦截事件
-
public boolean onTouchEvent(MotionEvent event) { }
- 在 dispatchTouchEvent 方法中调用,同来处理点击事件。返回值表示是否消耗当前事件,如果不消耗,那么在同一个事件序列中,当前 view 无法再次接收到事件。
这三个方法的关系可以用下面的伪代码来直观的表示:
public boolean dispatchTouchEvent(MotionEvent ev) { boolean result=false; if(onInterceptTouchEvent(ev)){ result=super.onTouchEvent(ev); }else{ result=child.dispatchTouchEvent(ev); }return result;复制代码
通过上面的伪代码, 我们可以直观的明白事件分发的大体流程。当一个点击事件产生时,根 View 接收到事件并调用 dispatchTouchEvent() 来对事件进行分发, 在方法内部先调用 onInterceptTouchEvent() ,如果返回 true 就表示要拦截这个事件,那么接下来这个事件就会交给这个 ViewGroup 通过调用自己的 onTouchEvent() 来处理。如果这个 ViewGroup 的 onInterceptTouchEvent 返回 false,表示它自己不拦截当前事件,事件就会分发给它的子view,即调用子view 的 dispatchTouchEvent(), 进行下一轮分发, 如此反复直到事件最终被处理。
下面,让我大致看一下源码中的分发流程。
1. Activity 分发过程:
#Activitypublic boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev);}复制代码
Activity 接收到事件后, 首先交给 Activity 所属的 Window 分发, 如果返回 true,整个事件的循环就结束了,返回 false 则表示事件没人处理,所有view 的 onTouchEvent 都返回了 false, 则调用 Activity 自己的 onTouchEvent 来处理事件。从上一篇文章里了解到这个 Window 实现子类就是 PhoneWindow。
#PhoneWindowpublic boolean superDispatchKeyShortcutEvent(KeyEvent event) { return mDecor.superDispatchKeyShortcutEvent(event);}复制代码
PhoneWindow 接着把事件分发给 DecorView, 也证实了之前说分的发过程 Activity -> PhoneWindow -> DecorView
2. 顶级 View 的分发过程:
在这里, 事件分发到了顶级View以后, 会调用 ViewGroup 的 dispatchTouchEvent() 方法, 从这里开始, 后面就是View 之间的事件分发了.
#DecorViewpublic boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event);}复制代码
我们继续看 ViewGroup 中的 dispatchTouchEvent()
方法, 方法有点长, 大体的代码解释我都标明在代码里面了, 方便大家理解:
public boolean dispatchTouchEvent(MotionEvent ev) { ... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); // 在 resetTouchState ()内部会重置标记位 FLAG_DISALLOW_INTERCEPT, // 因此 requestDisallow...() 不能影响 ViewGroup 对 ACTION_DOWN 的拦截 resetTouchState(); } final boolean intercepted; /** * 注意 if 的判断条件: * 1. 如果是 ACTION_DOWN , 则 if 判断的条件为true * 2. 如果 ViewGroup 不拦截 ACTION_DOWN 并且交给子view 处理了事件 , 则会在后面的方法中给 * mFirstTouchTarget 赋值,即 mFirstTouchTarget != null。此时 if 的判断条件为 true * 3. 如果 ViewGroup 拦截了事件,则 mFirstTouchTarget = null, if 的判断条件为 false * 后面的 move、up都直接进 else{} 里面, 就不会再调用自己的onInterceptTouchEvent() * 方法,该事件序列的其它事件都会交给自己处理 */ if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // 此处的标志位 FLAG_DISALLOW_INTERCEPT 由子类的requestDisallowInterceptTouchEvent // 决定但是在 ACTION_DOWN 来临时,会在 resetTouchState() 中将该标记位重置, 所以子类不能影 // 响父类对 ACTION_DOWN 的处理 if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); // 判断是否拦截事件: ViewGroup 默认是不拦截的 ev.setAction(action); // restore action in case it was changed } else { intercepted = false; } } else { intercepted = true; } ... // 如果事件没有被拦截, 则会进入这里面 if (!canceled && !intercepted) { ... 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 能否接受到点击事件, 如果可以, 则把事件交给该 子view 处理 // 1. 子view 是否在执行动画且是 VISIBLE 状态; // 2.点击事件的坐标是否在 子view 的区域内 if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } ... // 方法内部把事件分发给子view 的 dispatchTouchEvent() 方法处理 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){ ... // 如果子元素开始处理事件,即它的 dispatchTouchEvent() 返回 true,会走到这里, // 并且在 addTouchTarget() 方法内部会给 mFirstTouchTarget 赋值 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } } // 如果该事件没有被处理, 会走到这里面 if (mFirstTouchTarget == null) { handled = dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS); // 注意这里参数 child 传入的是 null , 内部则会调用 View 的 dispatchTouchEvent()方法, 然后调用 onTouchEvent 自己处理事件 } else {} ... return handled;}复制代码
上面 dispatchTouchEvent() 中调用的几个方法:
private void resetTouchState() { clearTouchTargets(); resetCancelNextUpFlag(this); mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; // 重置标记位 FLAG_DISALLOW_INTERCEPT mNestedScrollAxes = SCROLL_AXIS_NONE;}public boolean onInterceptTouchEvent(MotionEvent ev) { return false; // ViewGroup 默认返回 false}private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) { final boolean handled; final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); if (child == null) { // 等于null时, 会调用 View 的dispatchTouchEvent() 方法, 因为View没有子元素, 所以会直接交给自己处理 handled = super.dispatchTouchEvent(event); } else { // 传递的参数 child 不等于 null, 则会调用子元素的 dispatchTouchEvent(),从而完成了一轮事件的分发 handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; }}private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; // 给 mFirstTouchTarget 赋值 return target;}复制代码
- 从 ViewGroup 的事件分发方法中可以看出,:
- 首先判断是否要调用自己的
onInterceptTouchEvent()
方法有两个条件: 事件为ACTION_DOWN
和mFirstTouchTarget != null
, 而mFirstTouchTarget
是在当前的 ViewGroup 不拦截事件, 并且子元素开始处理事件时会被赋值并指向子元素。所以一旦当前的 ViewGroup 拦截了事件,则mFirstTouchTarget != null
就不成立, 而后续的 move、up 事件,由于 if 的判断条件为 false,导致都不会再调用 ViewGroup 的onInterceptTouchEvent
, 并且同一事件序列的其它事件都会默认交给它处理FLAG_DISALLOW_INTERCEPT
这标记是由子类的requestDisallowInterceptTouchEvent
来决定的,但是在ACTION_DOWN
发生时,会在resetTouchState()
中重置这个标志位,。所以当子view 设置了FLAG_DISALLOW_INTERCEPT
,它的 ViewGroup 将无法拦截除了ACTION_DOWN
以外的事件。因此,当面对ACTION_DOWN
事件来临时,ViewGroup 总是会调用自己的onInterceptTouchEvent
方法来决定是否拦截事件。- 当 ViewGroup 决定拦截事件后,后序的点击事件默认会交给它自己处理, 而不会重复再调用
onInterceptTouchEvent
。所以,onInterceptTouchEvent
不是总是被调用的,当我们要提前处理点击事件时,要使用 dispatchTouchEvent- 如果子元素的
dispatchTouchEvent
返回 true, 那么就会跳出 ViewGroup 遍历子集的循环,并在addTouchTarget()
给mFirstTouchTarget
赋值- 在遍历所有子元素后事件没有被合适的处理, 有两种情况: 1. ViewGroup 没有子元素; 2. 或者 子view 处理了点击事件但是在
dispatchTouchEvent(
) 方法里面返回 false, 一般是onTouchEvent()
返回了false
3. View 对点击事件的处理过程:
- View 对点击事件的处理过程简单一点, 因为 View 是一个单独的元素, 没有子元素所以无法向下分发事件, 因此只能自己处理事件。
先看它的 dispatchTouchEvent :
public boolean dispatchTouchEvent(MotionEvent event) { ... if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; // 首先判断有没有设置 onTouchListener, 如果 onTouch() 返回 true, 则不会再走 onTouchEvent() // 说明 mOnTouchListener.onTouch() 的优先级要比 onTouchEvent() 高 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; }复制代码
从这段源码可以看出,:View 对点击事件的处理过程, 首先判断有没有设置 OnTouchListener ,如果 OnTouchListener 中的 onTouch 中返回了 true, 那么 onTouchEvent 不会被调用, 可见 OnTouchListener 的优先级高于 onTouchEvent , 这样做的好处是方便在外界处理点击事件
继续看 onTouchEvent :
public boolean onTouchEvent(MotionEvent event) { // 从这里可以看出来, 即使当view 处于 DISABLED 不可用的状态时, 它依然可以消耗点击事件 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); } // 如果 View 设置有代理, 那么会执行 mTouchDelegate.onTouchEvent(event), 工作机制类似 onTouchListener if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } }// 此处可以看出来只要 clickable 和 long_clickable 有一个为 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 (!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)) { // 这里内部会调用 onClick() (如果设置了 mOnClickListener) performClick(); } } } return true; } return false;}public boolean performClick() { final boolean result; final ListenerInfo li = mListenerInfo; // 这里可以看到如果设置了 mOnClickListener, 则会调用 onClick() 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 的 LONG_CLICKABLE 属性默认是 false, 而 CLICKABLE 属性的值和具体的 view 有关, 确切来说可点击的 view 为 true, 不可点击的为 false. 例如 TextView 是不可点击的, Button 是可点击的。可以通过 setOnClickListener 和 setOnLongClickListener设置 CLICKABLE 和 LONG_CLICKABLE 为 false。
public void setOnClickListener(@Nullable OnClickListener l) { if (!isClickable()) { setClickable(true); } getListenerInfo().mOnClickListener = l;}public void setOnLongClickListener(@Nullable OnLongClickListener l) { if (!isLongClickable()) { setLongClickable(true); } getListenerInfo().mOnLongClickListener = l; }复制代码