聊聊Android事件分发的那点事
做Android也有一段时间了,反反复复用到一些事件分发的情况,反反复复被困扰,以下是一些总结,希望对大家有所帮助。直接进入主题
网上关于事件分发的文章也不少,有很多方式来理解事件分发机制,比如Demo,Log打印,源码分析等等,但是楼主今天准备把这些结合一下,用另外一种更加简单易懂的方式来说说这个事,希望能达到目的。
首先我们先弄清楚几个事情:
1,事件的分发主要涉及的部分:Activity,ViewGroup,View
View的子类有TextView,ImageView等等,ViewGroup的子类有LinearLayout,RelativeLayout等,当然ViewGroup也是View的一个子类,如果这个地方还有问题的,也先不用特别在意,不影响对于事件分发的理解。
2,事件分发机制主要涉及的几个方法:dispatchTouchEvent(),onInterceptTouchEvent(),onTouchEvent(),以及OnTouchListener.onTouch();
先说最最最基类的View,它本身拥有dispatchTouchEvent()和onTouchEvent()两个方法,并且拥有OnTouchListener的接口。用来处理touch事件
中间说说ViewGroup,它是继承自View,自然也拥有上述方法,并且它重写了dispatchTouchEvent(),所以它的子类的分发都会走它的dispatch方法而不是View的,并且ViewGroup增加了一个方法onInterceptTouchEvent(),用来打断touch事件,这个我们后面再聊。
最后我们说一下Activity,Activity拥有自己的dispatchTouchEvent()和onTouchEvent()。
3,上面说的这些方法都有一个boolean的返回值,返回true的话说明事件被消费,返回false说明事件没有被消费,不知道这个的童鞋可以先默默记一下,在往下看。
好,这些东西弄清楚了,我们可以往下走了。下面我想大致把事件分发的机制用简单概括的方式给童鞋说一下,让大家先有个大致的理解,我尽量写的通俗易懂
事件分发主要涉及DOWN,MOVE,UP事件,首先当手指按下,DOWN事件触发,最外层的Activity的dispatchTouchEvent()最先接到DOWN消息,返回false没有消费,传递给View层,比如Activity的最外层是一个LinearLayout,简称A,A的dispatchTouchEvent()接到DOWN事件,因为LinearLayout是一个ViewGroup类型,它可能包含许多子View,所以dispatchTouchEvent()方法中调用了onInterceptTouchEvent(),如果返回false就会把事件分发给子View,反之则不会,当然默认是返回false;那么假设A有一个TextView的子View,简称B,这时B就会接到DOWN事件,B属于一个目标View,因为它不会再有子View了,这时会调用B的onTouchEvent()方法消费DOWN,如果返回false,向上传递给A,A的onTouchEvent()消费,如果返回false,则返回给Activity的OnTouchEvent()。
简单的画一个图大家可以看一下,如果一个DOWN事件没有消费的话,就会按照这样的逻辑走,在往回传递事件时,一旦返回为true,事件就会被消费掉
如果我们将LinearLayout的onInterceptTouchEvent中的返回值改为true,也就是不像下分发DOWN事件。
相信看到这里,大家对于事件分发应该有了一些比较深的认识,那么我们继续从源码的角度来分析一下整个事件分发的过程
第一个,我们先看看Activity的源码
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 }
很简单,如果返回true,说明这个事件被消费掉了,会直接调用Activity的onTouchEvent()方法,看15行代码,我们追到Window的superDispatchTouchEvent()方法中
1 /** 2 * Used by custom windows, such as Dialog, to pass the touch screen event 3 * further down the view hierarchy. Application developers should 4 * not need to implement or call this. 5 * 6 */ 7 public abstract boolean superDispatchTouchEvent(MotionEvent event);
看注释,它会将touch event传递到view层,所以我们不去关心这个,只需要知道如果这个touch事件没有被消费的话,它会传递到view层,那下一个接收到touch事件的应该是最外层的LinearLayoutA了
1 @Override 2 public boolean dispatchTouchEvent(MotionEvent ev) { 3 if (mInputEventConsistencyVerifier != null) { 4 mInputEventConsistencyVerifier.onTouchEvent(ev, 1); 5 } 6 7 boolean handled = false; 8 if (onFilterTouchEventForSecurity(ev)) { 9 final int action = ev.getAction(); 10 final int actionMasked = action & MotionEvent.ACTION_MASK; 11 12 // Handle an initial down. 13 if (actionMasked == MotionEvent.ACTION_DOWN) { 14 // Throw away all previous state when starting a new touch gesture. 15 // The framework may have dropped the up or cancel event for the previous gesture 16 // due to an app switch, ANR, or some other state change. 17 cancelAndClearTouchTargets(ev); 18 resetTouchState(); 19 } 20 21 // Check for interception. 22 final boolean intercepted; 23 if (actionMasked == MotionEvent.ACTION_DOWN 24 || mFirstTouchTarget != null) { 25 final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; 26 if (!disallowIntercept) { 27 intercepted = onInterceptTouchEvent(ev); 28 ev.setAction(action); // restore action in case it was changed 29 } else { 30 intercepted = false; 31 } 32 } else { 33 // There are no touch targets and this action is not an initial down 34 // so this view group continues to intercept touches. 35 intercepted = true; 36 } 37 38 // Check for cancelation. 39 final boolean canceled = resetCancelNextUpFlag(this) 40 || actionMasked == MotionEvent.ACTION_CANCEL; 41 42 // Update list of touch targets for pointer down, if needed. 43 final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0; 44 TouchTarget newTouchTarget = null; 45 boolean alreadyDispatchedToNewTouchTarget = false; 46 if (!canceled && !intercepted) { 47 if (actionMasked == MotionEvent.ACTION_DOWN 48 || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) 49 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 50 final int actionIndex = ev.getActionIndex(); // always 0 for down 51 final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) 52 : TouchTarget.ALL_POINTER_IDS; 53 54 // Clean up earlier touch targets for this pointer id in case they 55 // have become out of sync. 56 removePointersFromTouchTargets(idBitsToAssign); 57 58 final int childrenCount = mChildrenCount; 59 if (newTouchTarget == null && childrenCount != 0) { 60 final float x = ev.getX(actionIndex); 61 final float y = ev.getY(actionIndex); 62 // Find a child that can receive the event. 63 // Scan children from front to back. 64 final View[] children = mChildren; 65 66 final boolean customOrder = isChildrenDrawingOrderEnabled(); 67 for (int i = childrenCount - 1; i >= 0; i--) { 68 final int childIndex = customOrder ? 69 getChildDrawingOrder(childrenCount, i) : i; 70 final View child = children[childIndex]; 71 if (!canViewReceivePointerEvents(child) 72 || !isTransformedTouchPointInView(x, y, child, null)) { 73 continue; 74 } 75 76 newTouchTarget = getTouchTarget(child); 77 if (newTouchTarget != null) { 78 // Child is already receiving touch within its bounds. 79 // Give it the new pointer in addition to the ones it is handling. 80 newTouchTarget.pointerIdBits |= idBitsToAssign; 81 break; 82 } 83 84 resetCancelNextUpFlag(child); 85 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 86 // Child wants to receive touch within its bounds. 87 mLastTouchDownTime = ev.getDownTime(); 88 mLastTouchDownIndex = childIndex; 89 mLastTouchDownX = ev.getX(); 90 mLastTouchDownY = ev.getY(); 91 newTouchTarget = addTouchTarget(child, idBitsToAssign); 92 alreadyDispatchedToNewTouchTarget = true; 93 break; 94 } 95 } 96 } 97 98 if (newTouchTarget == null && mFirstTouchTarget != null) { 99 // Did not find a child to receive the event. 100 // Assign the pointer to the least recently added target. 101 newTouchTarget = mFirstTouchTarget; 102 while (newTouchTarget.next != null) { 103 newTouchTarget = newTouchTarget.next; 104 } 105 newTouchTarget.pointerIdBits |= idBitsToAssign; 106 } 107 } 108 } 109 110 // Dispatch to touch targets. 111 if (mFirstTouchTarget == null) { 112 // No touch targets so treat this as an ordinary view. 113 handled = dispatchTransformedTouchEvent(ev, canceled, null, 114 TouchTarget.ALL_POINTER_IDS); 115 } else { 116 // Dispatch to touch targets, excluding the new touch target if we already 117 // dispatched to it. Cancel touch targets if necessary. 118 TouchTarget predecessor = null; 119 TouchTarget target = mFirstTouchTarget; 120 while (target != null) { 121 final TouchTarget next = target.next; 122 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 123 handled = true; 124 } else { 125 final boolean cancelChild = resetCancelNextUpFlag(target.child) 126 || intercepted; 127 if (dispatchTransformedTouchEvent(ev, cancelChild, 128 target.child, target.pointerIdBits)) { 129 handled = true; 130 } 131 if (cancelChild) { 132 if (predecessor == null) { 133 mFirstTouchTarget = next; 134 } else { 135 predecessor.next = next; 136 } 137 target.recycle(); 138 target = next; 139 continue; 140 } 141 } 142 predecessor = target; 143 target = next; 144 } 145 } 146 147 // Update list of touch targets for pointer up or cancel, if needed. 148 if (canceled 149 || actionMasked == MotionEvent.ACTION_UP 150 || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 151 resetTouchState(); 152 } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) { 153 final int actionIndex = ev.getActionIndex(); 154 final int idBitsToRemove = 1 << ev.getPointerId(actionIndex); 155 removePointersFromTouchTargets(idBitsToRemove); 156 } 157 } 158 159 if (!handled && mInputEventConsistencyVerifier != null) { 160 mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); 161 } 162 return handled; 163 }
这个代码比较多,我们抽重点讲,首先看22行代码,可以看到intercepted参数,在看一下46行,intercepted的参数决定能不能讲touch事件分发到下一层View,而决定这个参数的重要地方是25 ~ 31行,25行的disallowIntercept参数可参考requestDisallowInterceptTouchEvent()方法,这里不细讲,就是可以让子View设置是否不让父View跳过onInterceptTouchEvent()方法。注:requestDisallowInterceptTouchEvent()方法如果灵活运用的话会很有用。如果我们什么都没设置的话,自然会进入到ViewGroup独有的onInterceptTouchEvent()方法中,我们看一下源码
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
很简单,默认是返回false的,也就是默认ViewGroup是不会拦截touch事件。我们可以实现这个函数根据自己的需要去返回true或者false。所以如果返回true的话,代码执行中会跳过46~108行,事件就不会分发到子View中。其他的代码对于我们今天讲的事情关系不大,有兴趣的童鞋可以继续研究,我们继续往下走,所以如果我们没有操作的话,这时Touch事件会进入到下一层View的dispatchTouchEvent()方法中,也就是TextViewB中,我们来看代码
1 public boolean dispatchTouchEvent(MotionEvent event) { 2 if (mInputEventConsistencyVerifier != null) { 3 mInputEventConsistencyVerifier.onTouchEvent(event, 0); 4 } 5 6 if (onFilterTouchEventForSecurity(event)) { 7 //noinspection SimplifiableIfStatement 8 ListenerInfo li = mListenerInfo; 9 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED 10 && li.mOnTouchListener.onTouch(this, event)) { 11 return true; 12 } 13 14 if (onTouchEvent(event)) { 15 return true; 16 } 17 } 18 19 if (mInputEventConsistencyVerifier != null) { 20 mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); 21 } 22 return false; 23 }
先看8到12行的代码,if 语句里面的判断注意最后一个,li.mOnTouchListener.onTouch(this, event);这个li.mOnTouchListener 其实就是我们设置的onTouchListener。也就是说我们平时实现的OnTouchListener都会在这里起作用,而且如果我们实现的方法中返回true的话,就决定了这个事件会被消费,14行的代码就不会被执行,而View本身的OnTouchEvent()方法是不会执行的。当然,默认是返回发false的,所以大家可以灵活运用,如果只是想拿到Touch事件,做一些事情,那就老老实实的返回false吧。好继续走,到14行,开始执行onTouchEvent()方法,因为View只是一个父类,有很多子类,有的子类会重写OnTouchEvent来完成自己的特性,比如基于TextView为父类的一系列View,还有ListView,GridView等等。ImageView就没有重写OnTouchEvent。如果有童鞋有兴趣可以具体研究。我们这里主要看一下View本身的onTouchEvent()方法,这里看的很清楚,如果14行的onTouchEvent()返回是true的话,这个事件就会被消费掉了。
1 /** 2 * Implement this method to handle touch screen motion events. 3 * 4 * @param event The motion event. 5 * @return True if the event was handled, false otherwise. 6 */ 7 public boolean onTouchEvent(MotionEvent event) { 8 final int viewFlags = mViewFlags; 9 10 if ((viewFlags & ENABLED_MASK) == DISABLED) { 11 if (event.getAction() == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { 12 setPressed(false); 13 } 14 // A disabled view that is clickable still consumes the touch 15 // events, it just doesn't respond to them. 16 return (((viewFlags & CLICKABLE) == CLICKABLE || 17 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)); 18 } 19 ============================================================================================================================= 20 if (mTouchDelegate != null) { 21 if (mTouchDelegate.onTouchEvent(event)) { 22 return true; 23 } 24 } 25 26 if (((viewFlags & CLICKABLE) == CLICKABLE || 27 (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { 28 switch (event.getAction()) { 29 case MotionEvent.ACTION_UP: 30 boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0; 31 if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) { 32 // take focus if we don't have it already and we should in 33 // touch mode. 34 boolean focusTaken = false; 35 if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { 36 focusTaken = requestFocus(); 37 } 38 39 if (prepressed) { 40 // The button is being released before we actually 41 // showed it as pressed. Make it show the pressed 42 // state now (before scheduling the click) to ensure 43 // the user sees it. 44 setPressed(true); 45 } 46 47 if (!mHasPerformedLongPress) { 48 // This is a tap, so remove the longpress check 49 removeLongPressCallback(); 50 51 // Only perform take click actions if we were in the pressed state 52 if (!focusTaken) { 53 // Use a Runnable and post this rather than calling 54 // performClick directly. This lets other visual state 55 // of the view update before click actions start. 56 if (mPerformClick == null) { 57 mPerformClick = new PerformClick(); 58 } 59 if (!post(mPerformClick)) { 60 performClick(); 61 } 62 } 63 } 64 65 if (mUnsetPressedState == null) { 66 mUnsetPressedState = new UnsetPressedState(); 67 } 68 69 if (prepressed) { 70 postDelayed(mUnsetPressedState, 71 ViewConfiguration.getPressedStateDuration()); 72 } else if (!post(mUnsetPressedState)) { 73 // If the post failed, unpress right now 74 mUnsetPressedState.run(); 75 } 76 removeTapCallback(); 77 } 78 break; 79 80 case MotionEvent.ACTION_DOWN: 81 mHasPerformedLongPress = false; 82 83 if (performButtonActionOnTouchDown(event)) { 84 break; 85 } 86 87 // Walk up the hierarchy to determine if we're inside a scrolling container. 88 boolean isInScrollingContainer = isInScrollingContainer(); 89 90 // For views inside a scrolling container, delay the pressed feedback for 91 // a short period in case this is a scroll. 92 if (isInScrollingContainer) { 93 mPrivateFlags |= PFLAG_PREPRESSED; 94 if (mPendingCheckForTap == null) { 95 mPendingCheckForTap = new CheckForTap(); 96 } 97 postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); 98 } else { 99 // Not inside a scrolling container, so show the feedback right away 100 setPressed(true); 101 checkForLongClick(0); 102 } 103 break; 104 105 case MotionEvent.ACTION_CANCEL: 106 setPressed(false); 107 removeTapCallback(); 108 removeLongPressCallback(); 109 break; 110 111 case MotionEvent.ACTION_MOVE: 112 final int x = (int) event.getX(); 113 final int y = (int) event.getY(); 114 115 // Be lenient about moving outside of buttons 116 if (!pointInView(x, y, mTouchSlop)) { 117 // Outside button 118 removeTapCallback(); 119 if ((mPrivateFlags & PFLAG_PRESSED) != 0) { 120 // Remove any future long press/tap checks 121 removeLongPressCallback(); 122 123 setPressed(false); 124 } 125 } 126 break; 127 } 128 return true; 129 } 130 131 return false; 132 }
这个方法中我们以19行为分界线,注意看10~18行,如果这个View是disable,但是是clickable的状态的话依旧会消费掉touch事件,所以大家要用的时候要注意,下面我们注意看一下26 ~ 27行,这个if判断很重要,因为我们可以看到这个OnTouchEvent()事件最终返回true或者false是由这个 if 决定的。那我们看一下 if 的条件((viewFlags & CLICKABLE) == CLICKABLE ||(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE),很简单,如果这个View的viewFlag如果是CLICKABLE,或者LONG_CLICKABLE的话,这个事件注定要返回true,也就是会被消费掉。那么viewFlag是怎么决定的呢,View中有一个setFlags()方法,而我们的通常见到的setVisibility,setFocusable,setClickable,等等很多方法都会调到这个方法,这里不细讲。而 if 里面的代码其实就很简单了,它根据用户的行为更改了现在的状态,比如现在是否press状态,是否focus状态等等,当然也产生了一些click事件,longClick事件等等。最终,onTouchEvent方法返回false的话,就会一层一层向上传递,就如上面的图所示了。
讲到这里,楼主基本该讲的都讲完了,再讲一些在看代码时候了解到的其他知识,希望能对大家有益。
1,我们的Touch事件一般分为三个DOWN,MOVE,UP三个事件,当我们的DOWN事件传递下去,没有View消费,又传回来的话,MOVE事件和UP事件是不会进行分发的,这一点在ViewGroup中的onInterceptTouchEvent()方法的注释中有写到,其实想想android这样设计也是正确的,DOWN事件都不消费,那这个View也不会有click,press等等事件了...
2,如果某一个ViewGroup的onInterceptTouchEvent方法返回了true(消费掉该事件),那么后面的所有事件都不会经过onInterceptTouchEvent()方法,除了ACTION_CANCEL事件。会直接到该ViewGroup的OnTouchEvent方法中。
3,最后在总结一下,Touch事件肯定是从Activity开始传递,然后按照布局的层次一层一层往下传递,最终会传递到一个目标View,也就是你点击的那个点所在的View,可能是一个ViewGroup,也可能是一个View,比如说一个Button或者一个TextView,然后根据他们各自的OnTouchEvent方法决定是否消费。如果不消费的话,又会一层一层传递上来,直到有View消费。
各位童鞋如果有其他见解或者问题,希望我们可以共同探讨。