Android 事件分发机制源码攻略(三) —— View篇

继上篇Android 事件分发机制源码攻略(二) —— ViewGroup篇的介绍后,我们知道事件如何从Activity的dispatchTouchEvent经由顶层ViewDecorView 再到ViewGroup的dispatchTouchEvent,ViewGroup层的分发,我个人觉得是整个事件分发最为关键的一部分,理解透了ViewGroup层的事件传递,相当于对整个事件分发传递也就差不多了。现在事件传递到View层,这一篇,我们将对分析事件在View层的dispatchTouchEvent、onTouchEvent、OnTouchListener、OnClickListener这些方法的传递顺序。首先,我们来看下dispatchTouchEvent这个方法。

  /** 
    * 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层的事件分发做个小结。

《Android 事件分发机制源码攻略(三) —— View篇》

至此,View事件分发就到此结束了。

    原文作者:Android源码分析
    原文地址: https://juejin.im/entry/59decbb05188254896245846
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞