Android日记之事件分发机制

作者: 居居居居居居x | 来源:发表于2019-09-28 21:45 被阅读0次

    前言

    美团一面的时候,问到了这个事件分发机制,当初只是听说过,面试完后又赶紧复习了下,这一块的内容其实对于很多Android初级开发者来说理解挺困难的,也不介意一些刚学Android的萌新一来就去看事件分发机制这种东西,这篇文章就记录一下我学习事件分发机制的过程,有错误也请多多包涵。

    事件分发机制是什么

    用户通过屏幕与手机进行交互的时候,每一次的点击,长按或者拖动都是一个事件,而这些事件从屏幕经过一系列的处理并传递给各个View,由View来消费这一事件huozhe或者忽略掉这个事件,这个整个过程就是事件分发机制。
    总的来说,事件分发就是对MotionEvent事件的分发过程,每当有MotionEvent产生了以后,系统就要把这个事件传递给具体的View来进行处理,这个传递的过程就是分发过程。而且了解分发机制也有助于后面更好的分析点击滑动失效以及滑动冲突的问题。


    MotionEvent的主要事件,侵删


    点击事件的分发过程是涉及到3个重要的方法来一起共同的完成:
    • dispatchTouchEvent(MotionEvent ev)
      这个就是用来进行事件分发的方法,如果说事件要传递给当前的View,那么这个方法一定是被首先调用,然后返回结果表示当前View的onTouchEvent()和下级View的dispatchTouchEvent()方法的影响,表示是否消耗当前的事件。
    • onInterceptTouchEvent(MotionEvent ev)
      在上诉方法的内部就会开始调用,用来判断是否要拦截这个事件,如果当前View拦截了这个事件,那么在同一个事件序列中,此方法就不会再被调用了,然后返回的结果表示是否拦截了这个事件。
    • onTouchEvent(MotionEvent event)
      dispatchTouchEvent()方法中被调用,用来处理点击事件,返回结果表示是否消耗当前事件,如果不消耗,则在同一事件事件序列当中,当前View无法再次接收到事件。
      当一个点击事件发生后,它的传递过程会遵循如下的顺序:


      来源慕课网,侵删
      即事件会先传递给Activity,然后在传递给Window,Window传递给DecorView,它是一个顶级的View,然后接着会传递给ViewGroup,最后又ViewGroup按照分发机制去分发事件。这里需要注意的是,一个Activity包含着一个Window,而Window里面包含着一个DecorView,DecorView是Activity最顶层的View。
      我们按照层级来一步步讲,主要涉及的对象有3个,分别是Activity,ViewGroup和View,接下来就从源码角度来分析具体的分发事件的流程。

    Activity的分发过程

    Activity分发流程图,来自慕课,侵删
    //Activity#dispatchTouchEvent源码
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
    

    点击事件用MotionEvent来表示,当点击操作发生时,会传递到当前的Activity,由Activity的dispatchTouchEvent()进行派发,具体的工作是由Activity的Window来完成的,这里注意一下当事件开始派发的时候会判断一下点击事件是否为ACTION_DOWN,也就是事件刚刚产生的时候,如果是的话就会调用onUserInteraction()方法,这是一个默认空的实现方法,这个方法就是会在整个事件开始的时候就会被立刻调用,就可以通过重写这个方法来监听整个事件的开始的过程。刚刚也讲到事件是派发到Window,如果返回的是false就意味着事件没人处理,所有的View都返回了false,那么Activity的onTouchEvent()就会被调用。

    我们接着看Window是如何将事件传递给ViewGroup的,点进去Window发现它是一个抽象方法,这就是说要找到Window类的实现类,它的实现类就是PhoneWindow,也是Window唯一的实现类,我们就从PhoneWindow类里面看它是怎么处理事件的,代码如下:

    //Window#superDispatchTouchEvent源码
    /**
     * Used by custom windows, such as Dialog, to pass the touch screen event
     * further down the view hierarchy. Application developers should
     * not need to implement or call this.
     *
     */
    public abstract boolean superDispatchTouchEvent(MotionEvent event);
    
    
    
    //PhotoWindow#superDispatchTouchEvent源码
    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
    
    //DecorView#superDispatchTouchEvent源码
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
    

    这里就很清楚了,在PhoneWindow将事件传递给了DecorView,这里也可以发现,PhoneWindow包含了一个DecorView,那个DecorView是什么呢?它就是一个顶级View,它是继承与FrameLayout的,从这里开始,我爷们也可以从源代码中看到,最终事件传递到顶级View去,顶级View也叫根View,一般来说就是ViewGroup,到达顶级View后,就会调用ViewGroup的dispatchTouchEvent()方法。接下里就是ViewGroup的分发过程了。

    ViewGroup的分发过程



    ViewGroup分发流程图,来自慕课,侵删
    这里是主要的分发过程,源码内容比较多,我们挑几个重点的来讲。
    //ViewGroup#dispatchTouchEvent源码
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }
    
        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }
    
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            ......
        }
    
        if (!handled && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }
    

    首先看到的是onFilterTouchEventForSecurity()方法,这是判断触摸事件是否符合安全策略的方法,安全策略就是用户根据正常的使用习惯,用户只会尝试点击只会看到的view,或者ViewGroup,看不到的视图是没有办法去进行点击的,google为事件分发制定了一个安全策略,如果某一个view不处于视图的顶部的话,也就是说当前的view不是直接与用户交互的view,并且这个view它的的一个属性是该view不在顶部时,不去响应这样一个触摸事件,则不会分发这个事件,简单来说,如果当前的view被其他视图遮挡了,并且view设置了不在顶部时不响应触摸事件的话,那么该方法就会返回false。

    ......
    //ViewGroup#dispatchTouchEvent源码
    // Handle an initial down.
    if (actionMasked == MotionEvent.ACTION_DOWN) {
        // Throw away all previous state when starting a new touch gesture.
        // The framework may have dropped the up or cancel event for the previous gesture
        // due to an app switch, ANR, or some other state change.
        cancelAndClearTouchTargets(ev);
        resetTouchState();
    }
    
    // Check for interception.
    final boolean intercepted;
    if (actionMasked == MotionEvent.ACTION_DOWN
            || mFirstTouchTarget != null) {
        final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        } else {
            intercepted = false;
        }
    } else {
        // There are no touch targets and this action is not an initial down
        // so this view group continues to intercept touches.
        intercepted = true;
    }
    ......
    

    接着安全策略判断为true的时候,首先会把触摸事件的触摸目标通过resetTouchState()方法重置所有状态,为新的事件循环做准备。然后就会开始判断是否要拦截当前的事件,这里的要拦截的事件为MotionEvent.ACTION_DOWN或者mFirstTouchTarget != null,第一个好理解,第二个通过后面的代码可以看出就是说当ViewGroup不拦截事件将事件交由子元素处理时mFirstTouchTarget != null,当事件由ViewGroup拦截时,mFirstTouchTarget != null是不成立的,这个条件为false的话,ViewGroup的onInterceptTouchEvent()就不会被调用,并且同一序列的其它事件都会默认交给它处理。如果ViewGroup不拦截事件的话,事件就会向下分发交给它的子View来进行处理,如果被ViewGroup拦截的话,就不会下发给子View,会调用ViewGroup父类的dispatchTouchEvent()进行处理,下面的代码是不拦截事件后,下发给子View进行处理的过程。

    //ViewGroup#dispatchTouchEvent源码
    final View[] children = mChildren;
    for (int i = childrenCount - 1; i >= 0; i--) {
        final int childIndex = getAndVerifyPreorderedIndex(
                childrenCount, i, customOrder);
        final View child = getAndVerifyPreorderedView(
                preorderedList, children, childIndex);
    
        // If there is a view that has accessibility focus we want it
        // to get the event first and if not handled we will perform a
        // normal dispatch. We may do a double iteration but this is
        // safer given the timeframe.
        if (childWithAccessibilityFocus != null) {
            if (childWithAccessibilityFocus != child) {
                continue;
            }
            childWithAccessibilityFocus = null;
            i = childrenCount - 1;
        }
    
        if (!canViewReceivePointerEvents(child)
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
    
        newTouchTarget = getTouchTarget(child);
        if (newTouchTarget != null) {
            // Child is already receiving touch within its bounds.
            // Give it the new pointer in addition to the ones it is handling.
            newTouchTarget.pointerIdBits |= idBitsToAssign;
            break;
        }
    
        resetCancelNextUpFlag(child);
        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
            // Child wants to receive touch within its bounds.
            mLastTouchDownTime = ev.getDownTime();
            if (preorderedList != null) {
                // childIndex points into presorted list, find original index
                for (int j = 0; j < childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
    }
    

    以上代码就是首先遍历了所有ViewGroup的子元素,然后判断子元素是否能接收到点击事件,是否能接收点击事件主要是判断该点击位置是不是在子View的布局区域里面,如果在的话,事件就交由View来传递处理。
    其实总结一下ViewGroup的dispatchTouchEvent()主要就是做了3件事。

    • 去判断是否要拦截事件。
    • 在当前ViewGroup中找到用户真正点击的View。
    • 分发事件到View上。

    View的分发过程

    View分发流程图,来自慕课,侵删
    //View#dispatchTouchEvent源码
    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();
        }
            
    
        //这里开始判断有没有设置mOnTouchListener 
        if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
    
            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的点击事件处理就会简单多了,因为它是一个单独的元素,没有办法再向下传递事件,所以只能自己处理事件,首先会判断有没有设置mOnTouchListener,如果mOnTouchListener里的onTouch()返回true的话,那么onTouchEvent()就不会被调用了,意思就是说mOnTouchListener的优先级比onTouchEvent()高。

    //View#onTouchEvent源码
    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;
        }
    }
    

    接着看View里面的onTouchEvent()实现,这里是当View处于不可用的状态下也会消耗点击事件,意思就是说,不可用状态下的View照样会消耗点击事件,最后就是看onTouchEvent()对点击事件的具体处理。

    public boolean onTouchEvent(MotionEvent event) {
        ......
    
        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;
                    }
                    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);
                        }
    
                        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();
                        }
    
                        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.
                    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
                    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;
    }
    

    这里就直接放上源码了,看不懂没事(我也没看懂),翻了下书,意思就是View的CLICKABLE和LONG_CLICKABLE有一个为true,那么就会消耗掉这个事件,就是onTouchEvent()为true,然后就是当ACTION_UP事件发生时就会触发PerformClick()方法,而且如果View设置了点击事件监听,就会调用PerformClick()里的onClick()方法,到这里也就差不多结束了。

    //View#PerformClick源码
    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);
    
        notifyEnterOrExitForAutoFillIfNeeded(true);
    
        return result;
    }
    

    参考

    相关文章

      网友评论

        本文标题:Android日记之事件分发机制

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