Android View事件分发源码分析
引言
上一篇文章我们介绍了View的事件分发机制,今天我们从源码的角度来学习下事件分发机制。
Activity对点击事件的分发过程
事件最先传递给当前Activity,由Activity的dispatchTouchEvent进行事件分发,具体的工作是有Activity内部的Window来完成的。Window会将事件传递给DecorView,DecorView一般就是当前界面的底层容器(即setContentView所设置的View的父容器)。下面我们来看一下Activity的dispatchTouchEvent方法的源码。源代码如下:
1 /** 2 * Called to process touch screen events. You can override this to 3 * intercept all touch screen events before they are dispatched to the 4 * window. Be sure to call this implementation for touch screen events 5 * that should be handled normally. 6 * 7 * @param ev The touch screen event. 8 * 9 * @return boolean Return true if this event was consumed. 10 */ 11 public boolean dispatchTouchEvent(MotionEvent ev) { 12 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 13 onUserInteraction(); 14 } 15 if (getWindow().superDispatchTouchEvent(ev)) { 16 return true; 17 } 18 return onTouchEvent(ev); 19 }
我们分析上面的源代码,事件首先交给Activity所属的Window进行分发,如果返回true,那么说明事件被子View拦截并且处理了。如果返回false说明事件没人处理,所有的View的onTouchEvent都返回false,那么这时候只有Activity的onTouchEvent会被调用了(还记得上一篇文章写的那些结论吗?看看第5条)。
那么Window是如何将事件传递给ViewGroup的呢?我们看源代码会知道,Window是一个抽象类,Window的superDispatchTouchEvent方法也是一个抽象方法,我们需要找到Window的实现类才行。Window的实现类是PhoneWindow类,我们来看PhoneWindow是如何处理点击事件的。代码如下:
1 @Override 2 public boolean superDispatchTouchEvent(MotionEvent event) { 3 return mDecor.superDispatchTouchEvent(event); 4 }
看到这里,逻辑变得很清晰了,PhoneWindow将事件直接传递给DecorView,那DecorView又是什么呢?在上一篇文章中,我们聊到过,DecorView是顶层的View。我们接着方法往下看,我们看到如下代码:
1 public boolean superDispatchTouchEvent(MotionEvent event) { 2 return super.dispatchTouchEvent(event); 3 }
DecorView事件分发也是要依靠父类方法的,DecorView继承自FrameLayout,而后者继承自ViewGroup。很显然DecorView的事件分发过程调用的是ViewGroup里面的方法。
ViewGroup对事件分发过程
上面的分析已经讲明,事件到达顶层View后会调用ViewGroup的dispatchTouchEvent方法进行分发。下面的逻辑要分两种情况来说:
第一种:如果ViewGroup的onInterceptTouchEvent方法返回true,表示ViewGroup需要拦截该事件,这个事件会由ViewGroup来进行处理。
第二种:如果ViewGroup的onInterceptTouchEvent方法返回false,表示ViewGroup不需要拦截该事件,这时候这个事件会传递给它所在的点击事件链上的子View,这时候子View的dispatchTouchEvent会被调用,这时候事件就有顶级View传递到了下一级的View。接下来的传递过程和上面的过程类似,如此往复,直到整个事件的分发。
下面我们来看下ViewGroup中事件分发代码的逻辑,先看第一段。
1 // Check for interception. 2 final boolean intercepted; 3 if (actionMasked == MotionEvent.ACTION_DOWN 4 || mFirstTouchTarget != null) { 5 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 6 if (!disallowIntercept) { 7 intercepted = onInterceptTouchEvent(ev); 8 ev.setAction(action); // restore action in case it was changed 9 } else { 10 intercepted = false; 11 } 12 } else { 13 // There are no touch targets and this action is not an initial down 14 // so this view group continues to intercept touches. 15 intercepted = true; 16 }
从上面的代码我们知道,ViewGroup在如下两种情况下会判断是否要拦截当前事件:事件类型为ACTION_DOWN或者mFirstTouchTarget!=null。那么什么情况下mFirstTouchTarget!=null这个条件成立呢?通过后面的代码逻辑我们知道,当ViewGroup不拦截事件,并将事件交给子View处理时,mFirstTouchTarget!=null这个条件就是成立的。反过来说,一旦事件由ViewGroup拦截并且自己来处理时,mFirstTouchTarget!=null就是不成立的,那么当ACTION_MOVE和ACTION_UP事件到来时,由于if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null)这个条件为false就导致ViewGroup的onInterceptTouchEvent方法不会被调用,并且同一事件序列中的其他事件都会默认交给他来处理。
这里还有一种特殊情况,就是FLAG_DISALLOW_INTERCEPT标记位,这个标记位是通过requestDisallowInterceptTouchEvent这个方法来设置的,一般用于子View中。FLAG_DISALLOW_INTERCEPT设置后,ViewGroup将无法拦截除ACTION_DOWN以外的其他点击事件。在进行事件分发时,如果是ACTION_DOWN事件,会重置这个标记位,这导致子View中设置的这个标记位无效,因此,当面对ACTION_DOWN事件时,ViewGroup总会调用自己的dispatchTouchEvent方法来询问自己是否要拦截事件。代码如下:
// 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不拦截事件的时候,事件会向下分发交给它子View进行处理,这段代码如下:
1 final int childrenCount = mChildrenCount; 2 if (newTouchTarget == null && childrenCount != 0) { 3 final float x = ev.getX(actionIndex); 4 final float y = ev.getY(actionIndex); 5 // Find a child that can receive the event. 6 // Scan children from front to back. 7 final ArrayList<View> preorderedList = buildOrderedChildList(); 8 final boolean customOrder = preorderedList == null 9 && isChildrenDrawingOrderEnabled(); 10 final View[] children = mChildren; 11 for (int i = childrenCount - 1; i >= 0; i--) { 12 final int childIndex = customOrder 13 ? getChildDrawingOrder(childrenCount, i) : i; 14 final View child = (preorderedList == null) 15 ? children[childIndex] : preorderedList.get(childIndex); 16 17 // If there is a view that has accessibility focus we want it 18 // to get the event first and if not handled we will perform a 19 // normal dispatch. We may do a double iteration but this is 20 // safer given the timeframe. 21 if (childWithAccessibilityFocus != null) { 22 if (childWithAccessibilityFocus != child) { 23 continue; 24 } 25 childWithAccessibilityFocus = null; 26 i = childrenCount - 1; 27 } 28 29 if (!canViewReceivePointerEvents(child) 30 || !isTransformedTouchPointInView(x, y, child, null)) { 31 ev.setTargetAccessibilityFocus(false); 32 continue; 33 } 34 35 newTouchTarget = getTouchTarget(child); 36 if (newTouchTarget != null) { 37 // Child is already receiving touch within its bounds. 38 // Give it the new pointer in addition to the ones it is handling. 39 newTouchTarget.pointerIdBits |= idBitsToAssign; 40 break; 41 } 42 43 resetCancelNextUpFlag(child); 44 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 45 // Child wants to receive touch within its bounds. 46 mLastTouchDownTime = ev.getDownTime(); 47 if (preorderedList != null) { 48 // childIndex points into presorted list, find original index 49 for (int j = 0; j < childrenCount; j++) { 50 if (children[childIndex] == mChildren[j]) { 51 mLastTouchDownIndex = j; 52 break; 53 } 54 } 55 } else { 56 mLastTouchDownIndex = childIndex; 57 } 58 mLastTouchDownX = ev.getX(); 59 mLastTouchDownY = ev.getY(); 60 newTouchTarget = addTouchTarget(child, idBitsToAssign); 61 alreadyDispatchedToNewTouchTarget = true; 62 break; 63 } 64 65 // The accessibility focus didn't handle the event, so clear 66 // the flag and do a normal dispatch to all children. 67 ev.setTargetAccessibilityFocus(false); 68 } 69 if (preorderedList != null) preorderedList.clear(); 70 }
这段代码逻辑是这样的,首先遍历ViewGroup的所有子元素,然后判断子元素时候能够接收到点击事件。能否接收到点击事件主要由两点来衡量:子元素是否在播放动画和点击事件的坐标是否落在子元素的区域内。如果某一个子元素满足这两个条件,那么事件就会传递给它。
我们看代码第44行,dispatchTransformedTouchEvent实际上调用的就是子元素的dispatchTouchEvent方法。后面会介绍child这个参数时候为null带来的影响。我们来看这段代码:
1 if (child == null) { 2 handled = super.dispatchTouchEvent(event); 3 } else { 4 handled = child.dispatchTouchEvent(event); 5 }
从上面的代码我们看出来,如果child为null那么就直接调用View的dispatchTouchEvent方法,进行事件的处理。如果child!=null就调用child.dispatchTouchEvent方法进行下一轮的事件分发。如果子元素的dispatchTouchEvent方法返回true,那么mFirstTouchTarget会被赋值,并且跳出for循环(第60行代码),代码如下:
1 newTouchTarget = addTouchTarget(child, idBitsToAssign); 2 alreadyDispatchedToNewTouchTarget = true; 3 break;
如果子元素的dispatchTouchEvent返回false,ViewGroup会把事件分发给下一个子元素进行处理(结合前两段代码)。
其实mFirstTouchTarget真正的赋值过程是在addTouchTarget方法内部完成的,mFirstTouchTarget是一种单链表结构,其是否被赋值,将直接影响到ViewGroup对事件的拦截策略,如果mFirstTouchTarget为null,那么ViewGroup就默认拦截接下来同一序列中的所有点击事件,这一点在前面已经介绍过。
如果遍历ViewGroup后事件都没有被合适的处理,那么这包含两种情况,第一种是ViewGroup没有子元素;第二种是子元素处理了点击事件,但是在dispatchTouchEvent中返回false,这一般是onTouchEvent方法返回false,在这两种情况下,ViewGroup会自己处理点击事件。看下面代码:
1 // Dispatch to touch targets. 2 if (mFirstTouchTarget == null) { 3 // No touch targets so treat this as an ordinary view. 4 handled = dispatchTransformedTouchEvent(ev, canceled, null, 5 TouchTarget.ALL_POINTER_IDS); 6 }
看到第三个参数传递的null了吗?上面我们分析过,会调用View的dispatchTouchEvent方法。这时候点击事件就交给View来处理了。
View对点击事件的处理
View对点击事件的处理稍微简单一些,注意这里的View不包括ViewGroup。我们来看它的dispatchTouchEvent方法源代码:
1 /** 2 * Pass the touch screen motion event down to the target view, or this 3 * view if it is the target. 4 * 5 * @param event The motion event to be dispatched. 6 * @return True if the event was handled by the view, false otherwise. 7 */ 8 public boolean dispatchTouchEvent(MotionEvent event) { 9 // If the event should be handled by accessibility focus first. 10 if (event.isTargetAccessibilityFocus()) { 11 // We don't have focus or no virtual descendant has it, do not handle the event. 12 if (!isAccessibilityFocusedViewOrHost()) { 13 return false; 14 } 15 // We have focus and got the event, then use normal event dispatch. 16 event.setTargetAccessibilityFocus(false); 17 } 18 19 boolean result = false; 20 21 if (mInputEventConsistencyVerifier != null) { 22 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 23 } 24 25 final int actionMasked = event.getActionMasked(); 26 if (actionMasked == MotionEvent.ACTION_DOWN) { 27 // Defensive cleanup for new gesture 28 stopNestedScroll(); 29 } 30 31 if (onFilterTouchEventForSecurity(event)) { 32 //noinspection SimplifiableIfStatement 33 ListenerInfo li = mListenerInfo; 34 if (li != null && li.mOnTouchListener != null 35 && (mViewFlags & ENABLED_MASK) == ENABLED 36 && li.mOnTouchListener.onTouch(this, event)) { 37 result = true; 38 } 39 40 if (!result && onTouchEvent(event)) { 41 result = true; 42 } 43 } 44 45 if (!result && mInputEventConsistencyVerifier != null) { 46 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); 47 } 48 49 // Clean up after nested scrolls if this is the end of a gesture; 50 // also cancel it if we tried an ACTION_DOWN but we didn't want the rest 51 // of the gesture. 52 if (actionMasked == MotionEvent.ACTION_UP || 53 actionMasked == MotionEvent.ACTION_CANCEL || 54 (actionMasked == MotionEvent.ACTION_DOWN && !result)) { 55 stopNestedScroll(); 56 } 57 58 return result; 59 }
因为View(这里不包括ViewGroup)是一个单独的元素,它没有子元素因此无法向下传递事件,所以他只能自己处理事件。从上面的代码中,我们可以看出View对点击事件的处理过程,首先会判断有没有设置OnTouchListener。如果OnTouchListener中的onTouch方法返回true,那么onTouchEvent就不会被调用,可见OnTouchListener优先级是高于onTouchEvent的。
接下来我们来分析onTouchEvent的实现。我们分段来介绍,部分实现如下:
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处于不可用状态下时View照样会消耗点击事件。如果View设置有代理,那么还会执行TouchDelagate的onTouchEvent方法,这个onTouchEvent的工作机制应该和OnTouchListener类似。代码如下:
1 if (mTouchDelegate != null) { 2 if (mTouchDelegate.onTouchEvent(event)) { 3 return true; 4 } 5 }
下面我们再来看一下onTouchEvent方法对点击事件的具体处理,代码如下:
1 if (((viewFlags & CLICKABLE) == CLICKABLE || 2 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || 3 (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { 4 switch (action) { 5 case MotionEvent.ACTION_UP: 6 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; 7 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { 8 // take focus if we don't have it already and we should in 9 // touch mode. 10 boolean focusTaken = false; 11 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 12 focusTaken = requestFocus(); 13 } 14 15 if (prepressed) { 16 // The button is being released before we actually 17 // showed it as pressed. Make it show the pressed 18 // state now (before scheduling the click) to ensure 19 // the user sees it. 20 setPressed(true, x, y); 21 } 22 23 if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { 24 // This is a tap, so remove the longpress check 25 removeLongPressCallback(); 26 27 // Only perform take click actions if we were in the pressed state 28 if (!focusTaken) { 29 // Use a Runnable and post this rather than calling 30 // performClick directly. This lets other visual state 31 // of the view update before click actions start. 32 if (mPerformClick == null) { 33 mPerformClick = new PerformClick(); 34 } 35 if (!post(mPerformClick)) { 36 performClick(); 37 } 38 } 39 } 40 41 if (mUnsetPressedState == null) { 42 mUnsetPressedState = new UnsetPressedState(); 43 } 44 45 if (prepressed) { 46 postDelayed(mUnsetPressedState, 47 ViewConfiguration.getPressedStateDuration()); 48 } else if (!post(mUnsetPressedState)) { 49 // If the post failed, unpress right now 50 mUnsetPressedState.run(); 51 } 52 53 removeTapCallback(); 54 } 55 mIgnoreNextUpEvent = false; 56 break; 57 58 case MotionEvent.ACTION_DOWN: 59 mHasPerformedLongPress = false; 60 61 if (performButtonActionOnTouchDown(event)) { 62 break; 63 } 64 65 // Walk up the hierarchy to determine if we're inside a scrolling container. 66 boolean isInScrollingContainer = isInScrollingContainer(); 67 68 // For views inside a scrolling container, delay the pressed feedback for 69 // a short period in case this is a scroll. 70 if (isInScrollingContainer) { 71 mPrivateFlags |= PFLAG_PREPRESSED; 72 if (mPendingCheckForTap == null) { 73 mPendingCheckForTap = new CheckForTap(); 74 } 75 mPendingCheckForTap.x = event.getX(); 76 mPendingCheckForTap.y = event.getY(); 77 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 78 } else { 79 // Not inside a scrolling container, so show the feedback right away 80 setPressed(true, x, y); 81 checkForLongClick(0); 82 } 83 break; 84 85 case MotionEvent.ACTION_CANCEL: 86 setPressed(false); 87 removeTapCallback(); 88 removeLongPressCallback(); 89 mInContextButtonPress = false; 90 mHasPerformedLongPress = false; 91 mIgnoreNextUpEvent = false; 92 break; 93 94 case MotionEvent.ACTION_MOVE: 95 drawableHotspotChanged(x, y); 96 97 // Be lenient about moving outside of buttons 98 if (!pointInView(x, y, mTouchSlop)) { 99 // Outside button 100 removeTapCallback(); 101 if ((mPrivateFlags & PFLAG_PRESSED) != 0) { 102 // Remove any future long press/tap checks 103 removeLongPressCallback(); 104 105 setPressed(false); 106 } 107 } 108 break; 109 } 110 111 return true; 112 }
我们从上面的代码中看到,只要View的CLICKABLE和LONG_CLICKABLE其中有一个true,就会消耗掉事件。即onTouchEvent方法true,不管是不是DISABLE状态。然后就是当ACTION_UP事件发生时,会触发performClick方法。如果View设置了OnClickListener,那么performClick方法内部会调用它的onClick方法。代码如下:
1 /** 2 * Call this view's OnClickListener, if it is defined. Performs all normal 3 * actions associated with clicking: reporting accessibility event, playing 4 * a sound, etc. 5 * 6 * @return True there was an assigned OnClickListener that was called, false 7 * otherwise is returned. 8 */ 9 public boolean performClick() { 10 final boolean result; 11 final ListenerInfo li = mListenerInfo; 12 if (li != null && li.mOnClickListener != null) { 13 playSoundEffect(SoundEffectConstants.CLICK); 14 li.mOnClickListener.onClick(this); 15 result = true; 16 } else { 17 result = false; 18 } 19 20 sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); 21 return result; 22 }
最后再说一句,调用setOnClickListener和setOnLongClickListener可以改变View的CLICKABLE和LONG_CLICKABLE属性。
最后再说一句,下一篇文章通过一个简单的例子来介绍滑动冲突。(*^__^*)