美文网首页
事件分发流程之View源码详细分析

事件分发流程之View源码详细分析

作者: 6FingerGodSword | 来源:发表于2020-04-07 17:49 被阅读0次

    背景:接着上一篇ViewGroup的源码分析,我们知道,父view接收到事件后,会寻找有没有要处理该事件的子view,如果没有就由父view自己处理事件,如果有那么就会把事件分发给子View。

    那么就走到View的事件分发里去看看,相比起ViewGroup来说View的事件处理简单的多。我们详细看看View的事件分发处理,OnTouchListener、OnClickListener、以及View.onTouch()。

    直接上代码:

       /**
         * 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();
            }
    
            if (onFilterTouchEventForSecurity(event)) {
    
                /** 如果view 包含enable标志,并且事件是拖动滚动条,那么事件被标志为消费 */
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //noinspection SimplifiableIfStatement
                /**
                 *  如果TouchListener不为空,view 为enable
                 *  那么先执行TouchListener的onTouch()方法,
                 *
                 *  如果TouchListener的onTouch()方法消费了事件,
                 *  那么view自己的onTouchEvent(event)方法就不会执行了
                 *
                 * */
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
    
                /** 如果事件没有被消费,执行view自己的onTouchEvent(event)方法 */
                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;
        }
    

    这就是全部的代码,非常少,我们捡干货说:

     if (onFilterTouchEventForSecurity(event)) {
    

    主要的代码逻辑在这个条件判断里面,里面主要做了三个判断:

    1. view是否是enable,且目前事件作用于滚动条上。
    2. 检查是否设置了OnTouchListener监听。
    3. 事件是否被消费了,如果以上两步都没有消费掉这个事件,那么最后交给View自己的onTouchEvent(event)方法去处理。

    注意:第三步和前面两步是互斥的,如果前两步消费了事件,那么第三步就不会执行。关键判断代码如下:

               // 第三步执行判断
              if (!result && onTouchEvent(event)) {
                    result = true;
                }
    

    到这里View的这个方法就没有了,是不是很简单。

    因为OnTouchListener是应用层自己定义传到View的,没什么好说的,那么我们就看看View自己的onTouchEvent(event)方法:

     public boolean onTouchEvent(MotionEvent event) {
            final float x = event.getX();
            final float y = event.getY();
            final int viewFlags = mViewFlags;
            final int action = event.getAction();
    
            final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
    
            /** 如果view状态为DISABLED, 直接返回false */
            if ((viewFlags & ENABLED_MASK) == DISABLED) {
                if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                    setPressed(false);
                }
                mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                // A disabled view that is clickable still consumes the touch
                // events, it just doesn't respond to them.
                return clickable;
            }
    
            if (mTouchDelegate != null) {
                if (mTouchDelegate.onTouchEvent(event)) {
                    return true;
                }
            }
    
            /**
             * 如果clickable = true,则表示view将消费此事件
             * */
            if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
                switch (action) {
                    case MotionEvent.ACTION_UP:
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    
                        if ((viewFlags & TOOLTIP) == TOOLTIP) {
                            handleTooltipUp();
                        }
    
                        if (!clickable) {
                            removeTapCallback();
                            removeLongPressCallback();
                            mInContextButtonPress = false;
                            mHasPerformedLongPress = false;
                            mIgnoreNextUpEvent = false;
                            break;
                        }
    
                        /** 如果mPrivateFlags 包含 PFLAG_PREPRESSED 标识, 那么 prepressed = true */
                        boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
    
                        /** 如果view不在滚动容器内,并且手指已经滑出view的区域,那么不执行一下逻辑 */
                        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);
                            }
    
                            /** 如果已经处理了长按事件,那么mHasPerformedLongPress = true,所以就不会在执行点击事件 */
                            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.
                                    /**
                                     * 发送一个Runnable消息而不是直接调用performClick().
                                     * 这个会让view的其他状态在点击行为开始之前得到更新,。
                                     * */
                                    if (mPerformClick == null) {
                                        mPerformClick = new PerformClick();
                                    }
    
                                    /**
                                     *
                                     * 如果click消息放入消息队列失败,那么直接调用performClick()方法
                                     *
                                     * */
                                    if (!post(mPerformClick)) {
                                        performClickInternal();
                                    }
                                }
                            }
    
                            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();
                        }
                        mIgnoreNextUpEvent = false;
                        break;
    
                    case MotionEvent.ACTION_DOWN:
                        if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                            mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                        }
                        mHasPerformedLongPress = false;
    
                        if (!clickable) {
                            checkForLongClick(0, x, y);
                            break;
                        }
    
                        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.
                        /** 在【Down】事件下,view是否在一个可以滚动的容器内部,如果在那么加上 PFLAG_PREPRESSED 标识 */
                        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
                            setPressed(true, x, y);
                            checkForLongClick(0, x, y);
                        }
                        break;
    
                    case MotionEvent.ACTION_CANCEL:
                        if (clickable) {
                            setPressed(false);
                        }
                        removeTapCallback();
                        removeLongPressCallback();
                        mInContextButtonPress = false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        break;
    
                    case MotionEvent.ACTION_MOVE:
                        if (clickable) {
                            drawableHotspotChanged(x, y);
                        }
    
                        // Be lenient about moving outside of buttons
                        /**
                         *  如果在移动过程中,手指的坐标移出了View的区域,那么将清除掉所有的特征
                         *  比如长按,点击等
                         *  */
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
                        break;
                }
    
                return true;
            }
    
            return false;
        }
    

    往大的方向看,就是:如果clickable = true,那么这个方法就返回true,也就表示view消费这个事件。clickable 为true的条件也很简单 :CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 只要拥有这三个标识中的任一一个那么就表示这个View是可点击的。也就是会消费掉事件。

    在这个方法里面,我们主要关注一下两个事件的处理:

    1. 点击事件如何处理的。
    2. 长按事件如何处理的。
    1. 点击事件的处理:
    setPressed(true, x, y);
    
    public void setPressed(boolean pressed) {
            final boolean needsRefresh = pressed != ((mPrivateFlags & PFLAG_PRESSED) == PFLAG_PRESSED);
    
            if (pressed) {
                mPrivateFlags |= PFLAG_PRESSED;
            } else {
                mPrivateFlags &= ~PFLAG_PRESSED;
            }
    
            if (needsRefresh) {
                refreshDrawableState();
            }
            dispatchSetPressed(pressed);
        }
    

    在【Down】事件下给View 加上PFLAG_PRESSED 标识。

    if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    

    在【Up】事件下如果长按事件没有被处理,那么就执行 performClickInternal();方法,这个方法最终调用了,OnClickListener.onClick()方法。在进入这个条件时,会先把长按事件 从消息队列里面移除掉。保证不会触发长按逻辑。点击事件就结束了。
    还有一个注意的地方,就是在【Move】事件下,如果手指的触摸点移出了View的区域,那么会将点击和长按全部从消息队列移除

                       /**
                         *  如果在移动过程中,手指的坐标移出了View的区域,那么将清除掉所有的特征
                         *  比如长按,点击等
                         *  */
                        if (!pointInView(x, y, mTouchSlop)) {
                            // Outside button
                            // Remove any future long press/tap checks
                            removeTapCallback();
                            removeLongPressCallback();
                            if ((mPrivateFlags & PFLAG_PRESSED) != 0) {
                                setPressed(false);
                            }
                            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                        }
    
    1. 长按事件的处理:
    checkForLongClick(0, x, y);
    
    private void checkForLongClick(int delayOffset, float x, float y) {
            if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
                mHasPerformedLongPress = false;
    
                if (mPendingCheckForLongPress == null) {
                    mPendingCheckForLongPress = new CheckForLongPress();
                }
                mPendingCheckForLongPress.setAnchor(x, y);
                mPendingCheckForLongPress.rememberWindowAttachCount();
                mPendingCheckForLongPress.rememberPressedState();
                postDelayed(mPendingCheckForLongPress,
                        ViewConfiguration.getLongPressTimeout() - delayOffset);
            }
        }
    
       /**
         * Defines the default duration in milliseconds before a press turns into
         * a long press
         */
        private static final int DEFAULT_LONG_PRESS_TIMEOUT = 500;
    
    

    同样的也是在【Down】事件的时候向消息队列里面发送了一个延迟执行的长按事件消息,时间大概是500ms。如果按下的时间超过了500ms,那么长按消息就会被执行,最终mHasPerformedLongPress = true; 这个字段被赋值 true,然后到【Up】事件

      /** 如果已经处理了长按事件,那么mHasPerformedLongPress = true,所以就不会在执行点击事件 */
         if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
    

    【Up】中这个条件就不满足,也就是说不会去执行点击相关的代码了。

    结尾

    综上:View事件分发在View里面只有简单3步,然后View的里面OnTouchListener.onTouchEvent() > View.onTouchEvent() > OnClickListener.onClick()

    相关文章

      网友评论

          本文标题:事件分发流程之View源码详细分析

          本文链接:https://www.haomeiwen.com/subject/jhbnphtx.html