Android 事件分发机制源码攻略(三) —— View篇
2017-10-12 00:00
447 查看
继上篇Android 事件分发机制源码攻略(二) —— ViewGroup篇的介绍后,我们知道事件如何从Activity的dispatchTouchEvent经由顶层ViewDecorView 再到ViewGroup的dispatchTouchEvent,ViewGroup层的分发,我个人觉得是整个事件分发最为关键的一部分,理解透了ViewGroup层的事件传递,相当于对整个事件分发传递也就差不多了。现在事件传递到View层,这一篇,我们将对分析事件在View层的dispatchTouchEvent、onTouchEvent、OnTouchListener、OnClickListener这些方法的传递顺序。首先,我们来看下dispatchTouchEvent这个方法。
View的dispatchTouchEvent方法相对简单,没有复杂的逻辑。
如果给这个View设置了OnTouchListener监听并且返回true,那result就直接为true了,这直接导致事件没有进到onTouchEvent方法就结束分发了。如果没有设置OnTouchListener监听,那就会进到onTouchEvent方法,然后View的dispatchTouchEvent方法到此也就结束了。接着,我们再来看看onTouchEvent()方法。
哇,初看这个方法很长,以为很麻烦。其实里面很简单,依旧是没什么麻烦的逻辑的。以上方法可以总结成以下两点
1、如果View是可点击、可长按的,则返回true;否则返回false;
2、如果View是可点击、可长按的,优先触发performLongClick()长按事件,当抬起来时,触发onClick()事件(前提是你设置了OnClickListener监听)。
整个方法的话,除了上面说的前提外,剩下的要点就是分析在ACTION_DOWN、ACTION_MOVE、ACTION_UP所做的处理了。首先来看下ACTION_DOWN里面做了那些操作。
看到第110行有这么个方法isInScrollingContainer,我们进去看看是做了什么。
这个方法是调用其父布局的shouldDelayChildPressedState方法,那再去看看这个方法做了什么
这个注释大概的意思是父布局如果是可滑动的,应该返回true;否则,返回False;这个不属于我们这次讨论的问题,有兴趣的同学可以自行深入探究。
接着我们再看上面的第117行-124行,这几句代码的主要的用途是延迟执行CheckForTap这个任务,我们再看看这个CheckForTap做了那些操作。
这是一个实现Runnable接口的类,后面我们会看到很多的执行操作都是实现Runnable接口;这块代码跟128处的代码差不多,也就是说,两者的区别在于是否延迟执行后续任务了。好了,我们再来看看checkForLongClick这个方法做了什么。
这个方法很简单,先是判断这个View是否可以长按,为true的话,将当前位置设置下去,并修改相应的View所关联的window数量。紧接着延迟执行长按任务。
上面的代码没有太多的逻辑,最终的实现是最后面这一块。从最后这里可以看出,View的onTouchEvent事件处理首个ACTION_DOWN事件,优先响应的是长按监听(如果你有设置的话),如果该布局是可滑动的(比如listView这类),会有相应的延迟。好了,到这里ACTION_DOWN的事件到此就结束了。接着,我们来看下ACTION_MOVE事件的处理流程。
从上面的代码的第145-157行,观察注释就基本了解了。在View的范围内,基本不处理。那我们再来看看ACTION_UP做了哪些处理。
我们可以直接看第74行PerformClick,其他的代码基本上都是一些简单的判断,主要的就是PerformClick这个类。
看到这里我们知道,ACTION_UP事件最终会走到OnClickListener这个监听去。自此我们可以得出当首个ACTION_DOWN事件下来,在View的这一层首先是
我们来做对View层的事件分发做个小结。
/** * 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 the event should be handled by accessibility focus first. if (event.isTargetAccessibilityFocus()) { // We don't have focus or no virtual descendant has it, do not handle the event. if (!isAccessibilityFocusedViewOrHost()) { return false; } // We have focus and got the event, then use normal event dispatch. event.setTargetAccessibilityFocus(false); } boolean result = false; if (mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onTouchEvent(event, 0); } final int actionMasked = event.getActionMasked(); if (actionMasked == MotionEvent.ACTION_DOWN) { // Defensive cleanup for new gesture stopNestedScroll(); } //跟ViewGroup层的一样,都是安全策略 if (onFilterTouchEventForSecurity(event)) { //鼠标拖拉处理,不是分发的重点 if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement //内部类,包含各种触摸监听 ListenerInfo li = mListenerInfo; //优先判断mOnTouchListener是否为空,为空的话就跳过,不为空的话,就走到onTouchEvent方法 if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } //result为false的情况 if (!result && onTouchEvent(event)) { result = true; } } if (!result && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(event, 0); } // Clean up after nested scrolls if this is the end of a gesture; // also cancel it if we tried an ACTION_DOWN but we didn't want the rest // of the gesture. if (actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && !result)) { stopNestedScroll(); } return result; }
View的dispatchTouchEvent方法相对简单,没有复杂的逻辑。
整个方法最为关键的地方是从第41行开始,先对OnTouchListener这个监听进行判断。
如果给这个View设置了OnTouchListener监听并且返回true,那result就直接为true了,这直接导致事件没有进到onTouchEvent方法就结束分发了。如果没有设置OnTouchListener监听,那就会进到onTouchEvent方法,然后View的dispatchTouchEvent方法到此也就结束了。接着,我们再来看看onTouchEvent()方法。
/** * Implement this method to handle touch screen motion events. * <p> * If this method is used to detect click actions, it is recommended that * the actions be performed by implementing and calling * {@link #performClick()}. This will ensure consistent system behavior, * including: * <ul> * <li>obeying click sound preferences * <li>dispatching OnClickListener calls * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when * accessibility features are enabled * </ul> * * @param event The motion event. * @return True if the event was handled, false otherwise. */ public boolean onTouchEvent(MotionEvent event) { final float x = event.getX(); final float y = event.getY(); final int viewFlags = mViewFlags; final int action = event.getAction(); //如果View的状态是DISABLED,那返回值将由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; } } //如果View是可点击的,返回true,即是事件默认消费了。否则返回false; if (((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) { switch (action) { case MotionEvent.ACTION_UP: //在ACTION_DOWN选项里会设置mPrivateFlags的状态 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); } //如果这事件是从ACTION_DOWN开始,那mHasPerformedLongPress应该为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)) { performClick(); } } } if (mUnsetPressedState == null) { mUnsetPressedState = new UnsetPressedState(); } // 取消View的Press状态 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: //设置长按的触发位置标签为false 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(); } mPendingCheckForTap.x = event.getX(); mPendingCheckForTap.y = event.getY(); postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout()); } else { // Not inside a scrolling container, so show the feedback right away //设置View为Pressed,并触发长按事件 setPressed(true, x, y); checkForLongClick(0, x, y); } break; case MotionEvent.ACTION_CANCEL: setPressed(false); removeTapCallback(); removeLongPressCallback(); mInContextButtonPress = false; mHasPerformedLongPress = false; mIgnoreNextUpEvent = false; break; case MotionEvent.ACTION_MOVE: drawableHotspotChanged(x, y); // Be lenient about moving outside of buttons //移动的位置是否已经不在该View的范围内 if (!pointInView(x, y, mTouchSlop)) { // Outside button //移除CheckForTap removeTapCallback(); if ((mPrivateFlags & PFLAG_PRESSED) != 0) { // Remove any future long press/tap checks // 移除长按任务 removeLongPressCallback(); // 取消View的Press状态 setPressed(false); } } break; } return true; } return false; }
哇,初看这个方法很长,以为很麻烦。其实里面很简单,依旧是没什么麻烦的逻辑的。以上方法可以总结成以下两点
1、如果View是可点击、可长按的,则返回true;否则返回false;
2、如果View是可点击、可长按的,优先触发performLongClick()长按事件,当抬起来时,触发onClick()事件(前提是你设置了OnClickListener监听)。
整个方法的话,除了上面说的前提外,剩下的要点就是分析在ACTION_DOWN、ACTION_MOVE、ACTION_UP所做的处理了。首先来看下ACTION_DOWN里面做了那些操作。
看到第110行有这么个方法isInScrollingContainer,我们进去看看是做了什么。
/** * @hide 隐藏的方法 */ public boolean isInScrollingContainer() { ViewParent p = getParent(); while (p != null && p instanceof ViewGroup) { if (((ViewGroup) p).shouldDelayChildPressedState()) { return true; } p = p.getParent(); } return false; }
这个方法是调用其父布局的shouldDelayChildPressedState方法,那再去看看这个方法做了什么
/** * Return true if the pressed state should be delayed for children or descendants of this * ViewGroup. Generally, this should be done for containers that can scroll, such as a List. * This prevents the pressed state from appearing when the user is actually trying to scroll * the content. * * The default implementation returns true for compatibility reasons. Subclasses that do * not scroll should generally override this method and return false. */ public boolean shouldDelayChildPressedState() { return true; }
这个注释大概的意思是父布局如果是可滑动的,应该返回true;否则,返回False;这个不属于我们这次讨论的问题,有兴趣的同学可以自行深入探究。
接着我们再看上面的第117行-124行,这几句代码的主要的用途是延迟执行CheckForTap这个任务,我们再看看这个CheckForTap做了那些操作。
private final class CheckForTap implements Runnable { public float x; public float y; @Override public void run() { //更改flag mPrivateFlags &= ~PFLAG_PREPRESSED; //设置pressed为true setPressed(true, x, y); //检查并执行长按 checkForLongClick(ViewConfiguration.getTapTimeout(), x, y); } }
这是一个实现Runnable接口的类,后面我们会看到很多的执行操作都是实现Runnable接口;这块代码跟128处的代码差不多,也就是说,两者的区别在于是否延迟执行后续任务了。好了,我们再来看看checkForLongClick这个方法做了什么。
private void checkForLongClick(int delayOffset, float x, float y) { //该View是否可以长按 if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) { mHasPerformedLongPress = false; if (mPendingCheckForLongPress == null) { //实例长按任务 mPendingCheckForLongPress = new CheckForLongPress(); } //设置锚点 mPendingCheckForLongPress.setAnchor(x, y); //修改WindowAttachCount mPendingCheckForLongPress.rememberWindowAttachCount(); //延迟执行长按任务 postDelayed(mPendingCheckForLongPress, ViewConfiguration.getLongPressTimeout() - delayOffset); } }
这个方法很简单,先是判断这个View是否可以长按,为true的话,将当前位置设置下去,并修改相应的View所关联的window数量。紧接着延迟执行长按任务。
private final class CheckForLongPress implements Runnable { private int mOriginalWindowAttachCount; private float mX; private float mY; @Override public void run() { //pressed为true并且父布局不为空 if (isPressed() && (mParent != null) //基本上有调用rememberWindowAttachCount这个方法基本是相等的 && mOriginalWindowAttachCount == mWindowAttachCount) { if (performLongClick(mX, mY)) { mHasPerformedLongPress = true; } } } public void setAnchor(float x, float y) { mX = x; mY = y; } public void rememberWindowAttachCount() { mOriginalWindowAttachCount = mWindowAttachCount; } } /** * Calls this view's OnLongClickListener, if it is defined. Invokes the * context menu if the OnLongClickListener did not consume the event, * anchoring it to an (x,y) coordinate. * * @param x x coordinate of the anchoring touch event, or {@link Float#NaN} * to disable anchoring * @param y y coordinate of the anchoring touch event, or {@link Float#NaN} * to disable anchoring * @return {@code true} if one of the above receivers consumed the event, * {@code false} otherwise */ public boolean performLongClick(float x, float y) { mLongClickX = x; mLongClickY = y; final boolean handled = performLongClick(); mLongClickX = Float.NaN; mLongClickY = Float.NaN; return handled; } /** * Calls this view's OnLongClickListener, if it is defined. Invokes the * context menu if the OnLongClickListener did not consume the event. * * @return {@code true} if one of the above receivers consumed the event, * {@code false} otherwise */ public boolean performLongClick() { return performLongClickInternal(mLongClickX, mLongClickY); } /** * Calls this view's OnLongClickListener, if it is defined. Invokes the * context menu if the OnLongClickListener did not consume the event, * optionally anchoring it to an (x,y) coordinate. * * @param x x coordinate of the anchoring touch event, or {@link Float#NaN} * to disable anchoring * @param y y coordinate of the anchoring touch event, or {@link Float#NaN} * to disable anchoring * @return {@code true} if one of the above receivers consumed the event, * {@code false} otherwise */ private boolean performLongClickInternal(float x, float y) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED); boolean handled = false; final ListenerInfo li = mListenerInfo; //触发长按监听事件 if (li != null && li.mOnLongClickListener != null) { handled = li.mOnLongClickListener.onLongClick(View.this); } if (!handled) { final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y); handled = isAnchored ? showContextMenu(x, y) : showContextMenu(); } if (handled) { performHapticFeedback(HapticFeedbackConstants.LONG_PRESS); } return handled; }
上面的代码没有太多的逻辑,最终的实现是最后面这一块。从最后这里可以看出,View的onTouchEvent事件处理首个ACTION_DOWN事件,优先响应的是长按监听(如果你有设置的话),如果该布局是可滑动的(比如listView这类),会有相应的延迟。好了,到这里ACTION_DOWN的事件到此就结束了。接着,我们来看下ACTION_MOVE事件的处理流程。
从上面的代码的第145-157行,观察注释就基本了解了。在View的范围内,基本不处理。那我们再来看看ACTION_UP做了哪些处理。
我们可以直接看第74行PerformClick,其他的代码基本上都是一些简单的判断,主要的就是PerformClick这个类。
private final class PerformClick implements Runnable { @Override public void run() { performClick(); } } /** * Call this view's OnClickListener, if it is defined. Performs all normal * actions associated with clicking: reporting accessibility event, playing * a sound, etc. * * @return True there was an assigned OnClickListener that was called, false * otherwise is returned. */ 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; }
看到这里我们知道,ACTION_UP事件最终会走到OnClickListener这个监听去。自此我们可以得出当首个ACTION_DOWN事件下来,在View的这一层首先是
OnTouchListener->onTouchEvent->OnLongClickListener->OnClickListener。
我们来做对View层的事件分发做个小结。
至此,View事件分发就到此结束了。
相关文章推荐
- Android 事件分发机制源码攻略(二) —— ViewGroup篇
- Android查缺补漏(View篇)--事件分发机制源码分析
- Android 事件分发机制源码攻略(一)
- (转)Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android 事件分发机制源码和实例解析
- Android事件分发机制本质是树的深度遍历(图+源码)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- [置顶] Android事件分发机制源码分析
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)
- Android事件分发机制完全解析,带你从源码的角度彻底理解(上)
- android 事件分发机制完全解析 从源码开始(上)
- Android 进阶学习:事件分发机制完全解析,带你从源码的角度彻底理解(上)
- Android事件分发机制完全解析,带你从源码的角度彻底理解
- Android事件分发机制完全解析,带你从源码的角度彻底理解(下)