Android之View和ViewGroup事件分发
学习android一段时间了,觉得事件分发是一个比较难的部分,自定义控件需要这方面的知识,因此花了一段时间研究了一下,在此记录下自己学习的过程,供以后学习使用。
View的时间分发过程dispatchTouchEvent—> onTouch –-> onTouchEvent
/** * Pass the touch screen motion event down to the target view, or this * view if it is the target. * * @param event The motion event to be dispatched. * @return True if the event was handled by the view, false otherwise. */ public boolean dispatchTouchEvent(MotionEvent event) { if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } if (onFilterTouchEventForSecurity(event)) { //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { return true; } if (onTouchEvent(event)) { return true; } } if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } return false; }
dispatchTouchEvent的返回值由是否设置触摸回调的返回值或者onTouchEvent的返回值决定。
一个触摸事件发生后,首先如果设置了触摸事件的侦听,并且返回了true,表示该事件已经被消费了,dispatchTouchEvent方法会直接返回true。如果回调返回了false。那么会执行onTouchEvent方法,dispatchTouchEvent的返回值由onTouchEvent的返回值决定。默认情况下,只要可以点击并且是enable的,都会返回true。下面是onTouchEvent方法
/** * Implement this method to handle touch screen motion events. * * @param event The motion event. * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { final int viewFlags = mViewFlags; if ((viewFlags & ENABLED_MASK) == DISABLED) { if (event.getAction() == 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)); } if (mTouchDelegate != null) { if (mTouchDelegate.onTouchEvent(event)) { return true; } } if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) { switch (event.getAction()) { 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); } if (!mHasPerformedLongPress) { // 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)) { 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(); } break; case MotionEvent.ACTION_DOWN: mHasPerformedLongPress = false; if (performButtonActionOnTouchDown(event)) { break; } // Walk up the hierarchy to determine if we're inside a scrolling container. boolean isInScrollingContainer = isInScrollingContainer(); // For views inside a scrolling container, delay the pressed feedback for // a short period in case this is a scroll. if (isInScrollingContainer) { mPrivateFlags |= PFLAG_PREPRESSED; if (mPendingCheckForTap == null) { mPendingCheckForTap = new CheckForTap(); } postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away setPressed(true); checkForLongClick(0); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); break; case MotionEvent.ACTION_MOVE: final int x = (int) event.getX(); final int y = (int) event.getY(); // Be lenient about moving outside of buttons if (!pointInView(x, y, mTouchSlop)) { // Outside button removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks removeLongPressCallback(); setPressed(false); } } break; } return true; } return false; }
长按事件是在ACTION_DOWN 里面执行的,点击事件是在ACTION_UP里面执行的。长按事件的执行逻辑如下:
class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; public void run() { if (isPressed() && (mParent != null) && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick()) { mHasPerformedLongPress = true; } } } public boolean performLongClick() { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; ListenerInfo li = mListenerInfo; if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { handled = showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
如果执行了长按事件,并且长按的回调返回了true,那么mHasPerformedLongPress= true,就不会再去执行performClick();
因此想要长按事件和单击事件同时发生,就要在长按的回调函数返回false。
单击事件和长按事件其实都是封装在触摸事件里面的。
ViewGroup的事件分发过程dispatchTouchEvent---> onInterceptTouchEvent
public boolean onInterceptTouchEvent(MotionEvent ev) { return false; }
默认情况下onInterceptTouchEvent返回false,即不拦截。
/** * {@inheritDoc} */ @Override public boolean dispatchTouchEvent(MotionEvent ev) { if (!onFilterTouchEventForSecurity(ev)) { return false; } final int action = ev.getAction(); final float xf = ev.getX(); final float yf = ev.getY(); final float scrolledXFloat = xf + mScrollX; final float scrolledYFloat = yf + mScrollY; final Rect frame = mTempRect; // 是否禁用拦截,如果为true表示不能拦截事件;反之,则为可以拦截事件 boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; // ACTION_DOWN事件,即按下事件 if (action == MotionEvent.ACTION_DOWN) { if (mMotionTarget != null) { // this is weird, we got a pen down, but we thought it was // already down! // XXX: We should probably send an ACTION_UP to the current // target. mMotionTarget = null; } // If we're disallowing intercept or if we're allowing and we didn't // intercept。如果不允许事件拦截或者不拦截该事件,那么执行下面的操作 if (disallowIntercept || !onInterceptTouchEvent(ev)) // 1、是否禁用拦截、是否拦截事件的判断 // reset this event's action (just to protect ourselves) ev.setAction(MotionEvent.ACTION_DOWN); // We know we want to dispatch the event down, find a child // who can handle it, start with the front-most child. final int scrolledXInt = (int) scrolledXFloat; final int scrolledYInt = (int) scrolledYFloat; final View[] children = mChildren; final int count = mChildrenCount; for (int i = count - 1; i >= 0; i--) // 2、迭代所有子view,查找触摸事件在哪个子view的坐标范围内 final View child = children[i]; // 该child是可见的 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE || child.getAnimation() != null) { // 3、获取child的坐标范围 child.getHitRect(frame); // 4、判断发生该事件坐标是否在该child坐标范围内 if (frame.contains(scrolledXInt, scrolledYInt)) // offset the event to the view's coordinate system final float xc = scrolledXFloat - child.mLeft; final float yc = scrolledYFloat - child.mTop; ev.setLocation(xc, yc); child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; // 5、child处理该事件,如果返回true,那么mMotionTarget为该child。正常情况下, // dispatchTouchEvent(ev)的返回值即onTouchEcent的返回值。因此onTouchEcent如果返回为true, // 那么mMotionTarget为触摸事件所在位置的child。 if (child.dispatchTouchEvent(ev)) //默认的实现下View.dispatchTouchEvent(ev)返回值一定为true // Event handled, we have a target now. mMotionTarget = child; return true; //表示子view已经能将触摸时间消费掉 } } } } } }// end if boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) || (action == MotionEvent.ACTION_CANCEL); if (isUpOrCancel) { // Note, we've already copied the previous state to our local // variable, so this takes effect on the next event mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT; } // The event wasn't an ACTION_DOWN, dispatch it to our target if // we have one. final View target = mMotionTarget; // 6、如果mMotionTarget为空,那么执行super.dispatchTouchEvent(ev), // 即View.dispatchTouchEvent(ev),就是该View Group自己处理该touch事件,只是又走了一遍View的分发过程而已. (指没有找到view,也可能是下面两种情况) // 1,拦截事件 或者2.在不拦截事件target view的onTouchEvent返回false的情况都会执行到这一步. 这种情况下 //执行super.dispatchTouchEvent(ev);也就是当成view来分发事件,过程同 view的时间分发过程一致 if (target == null) { // We don't have a target, this means we're handling the // event as a regular view. ev.setLocation(xf, yf); if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; } return super.dispatchTouchEvent(ev); // 调用super.dispatchTouchEvent(ev); 表示子view没能将触摸时间消费掉,就会将触摸事件传递给父view } // if have a target, see if we're allowed to and want to intercept its // events // 7、如果没有禁用事件拦截,并且onInterceptTouchEvent(ev)返回为true,即进行事件拦截. //-----似乎只有target!=null也就是子view处理down还返回true,然后拦截事件发生了才会执行下面的if //也就是对move 和 up 事件的拦截会执行到这里。 //由于在down时child.dispatchTouchEvent(ev)返回了true,所以target有了值。下面的代码是让子view执行ACTION_CANCEL事件 if (!disallowIntercept && onInterceptTouchEvent(ev)) { final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; ev.setAction(MotionEvent.ACTION_CANCEL); ev.setLocation(xc, yc); // if (!target.dispatchTouchEvent(ev)) { //拦截move事件,会让子view分发cancel事件 // target didn't handle ACTION_CANCEL. not much we can do // but they should have. } // clear the target mMotionTarget = null; // Don't dispatch this event to our own view, because we already // saw it when intercepting; we just want to give the following // event to the normal onTouchEvent(). return true; } if (isUpOrCancel) { mMotionTarget = null; } // finally offset the event to the target's coordinate system and // dispatch the event. final float xc = scrolledXFloat - (float) target.mLeft; final float yc = scrolledYFloat - (float) target.mTop; ev.setLocation(xc, yc); if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) { ev.setAction(MotionEvent.ACTION_CANCEL); target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT; mMotionTarget = null; } // 事件不拦截,且target view在ACTION_DOWN时返回true,那么后续事件由target来处理事件 // 执行到这里的条件是子view 在ACTION_DOWN时返回true,这样target不为null,并且 //还不会执行target == null 的判断才会执行到这里 return target.dispatchTouchEvent(ev); }
拦截的使用方法
@Override public booleanonInterceptTouchEvent(MotionEvent ev) { int action =ev.getAction(); switch (action) { case MotionEvent.ACTION_DOWN: return true ; caseMotionEvent.ACTION_MOVE: return true ; caseMotionEvent.ACTION_UP: return true ; } return false; }
1.如果你在DOWNretrun true ,则DOWN,MOVE,UP子View都不会捕获事件;onInterceptTouchEvent(ev) return true的时候,mMotionTarget 为null ;
2.如果你在MOVEreturn true , 则子View在MOVE和UP都不会捕获事件。onInterceptTouchEvent(ev) return true的时候,此时target还不为null,会执行target.dispatchTouchEvent(ev)来分发cancel事件,接着会把mMotionTarget置为null ;
3.拦截down事件,会执行到target==null,然后调用super.dispatchTouchEvent(ev);即父view来处理。
4.拦截move事件,子view会处理cancel事件,父view不会处理该触摸事件,并且让mMotionTarget = null;结果return true。认为这一个动作已完成,不会再回传到父控件的OnTouchEvent中处理(通过源代码发现确实是这样子)。但是实际上触摸事件是一连串事件,下一个move事件发生后,会判断target == null,执行return super.dispatchTouchEvent(ev);也就是让拦截的ViewGroup来处理后续的MOVE、UP事件(我是这么理解的^_^)。
总结一下就是:触摸事件对DOWN事件不进行拦截,因此DOWN事件可以被子View正常的处理。但是在MOVE时对事件进行了拦截,那么子View就无法接收到MOVE以及后面的事件了,它会收到一个CANCEL事件,后续的事件将会被拦截的ViewGroup的onTouchEvent进行处理。
5.requestDisallowInterceptTouchEvent(boolean) 用于设置是否允许拦截
如果ViewGroup的onInterceptTouchEvent(ev)当ACTION_MOVE时return true ,即拦截了子View的MOVE以及UP事件;那还有补救的措施。requestDisallowInterceptTouchEvent(true)便可以使子view接收到,因为会跳过if(!disallowIntercept && onInterceptTouchEvent(ev)),执行returntarget.dispatchTouchEvent(ev);通过源码很容易解释。
6.但是如果是在ACTION_DOWN时返回true来拦截的,那么子view无论怎么做都不可能捕获任何事件,因为此时target == null,肯定会执行return super.dispatchTouchEvent(ev);
7.onTouchEvent收到ACTION_DOWN,是否一定能收到ACTION_MOVE,ACTION_UP?收到ACTION_MOVE,能否说明它已经收到过ACTION_DOWN?
第一个如果MOVE和UP被父View拦截就收不到了。第二个可能是ViewGroup拦截了子View的MOVE事件,虽然DOWN事件传递了下去被子View消费,但是由于拦截了MOVE接下来会在ViewGroup中处理MOVE事件。
最后,一个重要的知识点:一般的View默认都是不可点击的。例如:View、ViewGroup、ImageView。但所有的View默认都是enable的除非设置为disable。这个时候就要小心:他们在处理默认的onTouchEvent的时候返回的是false!(disable并且clickable或者longclickable会返回true表明已经被消费)返回false意味着对它们的touch事件会向上抛。
参考的文章:http://blog.csdn.net/lmj623565791/article/details/39102591
http://blog.csdn.net/lmj623565791/article/details/38960443
http://blog.csdn.net/xiaanming/article/details/21696315