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

来源:互联网 发布:摄影测量info软件 编辑:程序博客网 时间:2024/05/16 16:12

继上篇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层的事件分发做个小结。

这里写图片描述

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