开发者

Android进阶事件分发机制解决事件冲突

目录
  • 引言
  • 1 android 事件分发机制
    • 1.1 事件分发流程
    • 1.2 View的事件消费
    • 1.3 ViewGroup的事件分发 -- ACTION_DOWN
      • 1.1.1 万事皆始于ACTION_DOWN
      • 1.1.2 ViewGroup拦截事件
      • 1.1.3 ViewGroup不拦截事件
    • 1.4 ViewGroup的事件分发 -- ACTION_MOVE
    • 2 Android事件冲突处理
      • 2.1 内部拦截法
        • 2.2 外部拦截法

        引言

        相信伙伴们在日常的开发工作中,一定会遇到事件冲突的问题,e.g. 一个页面当手指滑动的时候,会翻到下一页;点击的时候,需要响应页面中的元素点击事件,这个时候如果没有处理滑动事件,可能遇到的问题就是在滑动翻页的时候却只响应了点击事件,这个就是点击事件与滑动事件的冲突。其实还有很多常见的经典事件,e.g. RecyclerView嵌套滑动,Viewpager与RecyclerView嵌套滑动等,所以这个时候我们需要对事件分发非常了解,才能针对需求做相应的处理。

        1 Android 事件分发机制

        这是一个老生常谈的问题,相信伙伴们都了解常见的Android事件类型:ACTION_DOWN、ACTION_MOVE、ACTION_UP,分别代表手指按下屏幕的事件、手指滑动的事件以及手指抬起的事件,那么从手指按下到事件响应,中间经历了什么呢?我们从Google的源码中去寻找答案。

        1.1 事件分发流程

        因为对于组件来说,这个事件要么消费要么不消费(事件处理),而对于容器来说,还需要做的一件事就是分发事件,通常是先分发后处理,而View就只是处理事件。

        Android进阶事件分发机制解决事件冲突

        因此在进行事件冲突处理的时候,对于事件是否向下分发给子View消费,就需要在父容器中做拦截,子View仅做事件消费。

        1.2 View的事件消费

        首先我们先不看事件是如何分发的,先关注下事件是如何被处理的,在View的dispatchTouchEvent方法中,就包含对事件的处理全过程。

        public boolean dispatchTouchEvent(MotionEvent event) {
            //......
            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)) {
                if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                    result = true;
                }
                //核心代码1
                ListenerInfo li = mListenerInfo;
                if (li != null && li.mOnTouchListener != null
                        && (mViewFlags & ENABLED_MASK) == ENABLED
                        && li.mOnTouchListener.onTouch(this, event)) {
                    result = true;
                }
                //核心代码2
                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;
        }
        

        看到dispatchTouchEvent,我们可能会想,这个方法名看着像是分发事件的方法,View不是仅仅消费事件吗,还需要处理分发?其实不是这样的,因为View对于事件可以有选择的,可以选择不处理事件,那么就会往上派给父类去处理这个事件,如果能够消费,那么就在onTouchEvent中处理了。

        核心代码1:首先拿到一个ListenerInfo对象,这个对象中标记了这个View设置的监听事件,这里有几个判断条件:

        (1)ListenerInfo不为空,而且设置了OnTouchListener监听;

        (2)设置了OnTouchListener监听,而且onTouch方法返回了true

        这个时候,result设置为true;

        核心代码2:如果满足了核心代码1的全部条件,那么核心代码2就不会走到onTouchEvent这个判断条件中,因为result = true不满足条件直接break。

        那么如果设置了OnTouchListener监听,而且onTouch方法返回了false,那么result = false,核心代码2就能够执行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) {
                            handleToohttp://www.devze.comltipUp();
                        }
                        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)) {
                                        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(
                                    ViewConfiguration.getLongPressTimeout(),
                                    x,
                                    y,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                            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(
                                    ViewConfiguration.getLongPressTimeout(),
                                    x,
                                    y,
                                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                        }
                        break;
                //----------注意这里的返回值,clickable为true----------//
                return true;
            }
            //----------注意这里的返回值,clickable为false----------//
            return false;
        }
        

        这里就是对所有事件的处理,包括但不限于ACTION_DOWN、ACTION_UP,我们需要知道一点就是,View的click事件其实是在ACTION_UP中处理的。我们从上面的源码中可以看出来,在ACTION_UP中有一个方法performClickInternal,具体实现为performClick方法。

        public boolean performClick() {
            // We still need to call this method to handle the cases where performClick() was called
            // externally, instead of through performClickInternal()
            notifyAutofillManagerOnClick();
            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;
        }
        

        在这个方法中,我们貌似看到同样的一段代码,如果设置了OnClickListener监听,那么就会执行onClick方法也就是响应点击事件。

        所以通过上面的分析,我们能够了解,如果同一个View同时设置了setOnClickListener和setOnTouchListener,如果setOnTouchListener返回了false,那么点击事件是可以响应的;如果setOnTouchListener返回了true,那么点击事件将不再响应。

        binding.tvHello.setOnClickListener {
            Log.e("TAG","OnClick")
        }
        binding.tvHello.setOnTouchListener(object : View.OnTouchListener{
            override fun onTouch(v: View?, event: MotionEvent?): Boolean {
                Log.e("TAG","onTouch")
                return false
            }
        })
        

        还需要注意一点的就是,对于clickable这个android属性要求非常严格,必须要设置为true才可以进行事件的消费,也就是说在clickable为true的时候,onTouchEvent才会返回true,否则就会返回false,这个DOWN事件没有被消费,也就是说在dispatchTransformedTouchEvent方法中返回了false,此时就不会给 mFirstTouchTraget == null 赋值,后续MOVE事件进来就不会处理,这里需要非常注意。

        这里伙伴们如果不理解,可以换句话说:就是当DOWN事件来临之后,其实ViewGroup一定会将事件分发给子View,看子View要不要消费,如果子View不是clickable的,也就是说clickable = false,那么此时子View的onTouchEvent返回false,那么dispatchTouchEvent也是返回false,代表子View不消费这个事件,那么此时dispatchTransformedTouchEvent也是返回了false,mFirstTouchTraget还是空;因为子View没有消费DOWN事件,那么后续事件不会再触发了

        1.3 ViewGroup的事件分发 -- ACTION_DOWN

        前面我们介绍了View对于事件的消费,不管是click还是touch,都有对应的标准决定是否能够响应事件,最终View的dispatchTouchEvent返回值,就是result的值,只要有一个事件被消费,那么这个事件就算是到头了,但是,如果最终事件没有被消费,也就是说dispatchTouchEvent返回了false,那么父容器就能够拿到这个状态,决定谁去处理这个事件。

        所以ViewGroup就像是荷官,卡牌就是事件,她可以决定牌发到谁的手里,所以ViewGroup的事件分发机制核心就在于dispatchTouchEvent方法。

        public boolean dispatchTouchEvent(MotionEvent ev) {
            // ......
            if (onFilterTouchEventForSecurity(ev)) {
                final int action = ev.getAction();
                final int actionMasked = action & MotionEvent.ACTION_MASK;
                // 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;
                }
                // If intercepted, start normal event dispatch. Also if there is already
                // a view that is handling the gesture, do normal event dispatch.
                if (intercepted || mFirstTouchTarget != null) {
                    ev.setTargetAccessibilityFocus(false);
                }
                // Check for cancelation.
                final boolean canceled = resetCancelNextUpFlag(this)
                        || actionMasked == MotionEvent.ACTION_CANCEL;
                // Update list of touch targets for pointer down, if needed.
                final boolean isMouseEvent = ev.getSource() == InputDevice.SOURCE_MOUSE;
                final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0
                        && !isMouseEvent;
                TouchTarget newTouchTarget = null;
                boolean alreadyDispatchedToNewTouchTarget = false;
                // -------- 这里是不拦截的时候会走的地方 -------//
                if (!canceled && !intercepted) {
                    // If the event is targeting accessibility focus we give it to the
                    // view that has accessibility focus and if it does not handle it
                    // we clear the flag and dispatch the event to all children as usual.
                    // We are looking up the accessibility focused host to avoid keeping
                    // state since these events are very rare.
                    View childwithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                            ? findChildWithAccessibilityFocus() : null;
                    if (actionMasked == MotionEvent.ACTION_DOWN
                            || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                        final int actionIndex = ev.getActionIndex(); // always 0 for down
                        final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                                : TouchTarget.ALL_POINTER_IDS;
                        // Clean up earlier touch targets for this pointer id in case they
                        // have become out of sync.
                        removePointersFromTouchTargets(idBitsToAssign);
                        final int childrenCount = mChildrenCount;
                        if (newTouchTarget == null && childrenCount != 0) {
                            final float x =
                                    isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                            final float y =
                                    isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                            // Find a child that can receive the event.
                            // Scan children from front to back.
                            final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                            final boolean customOrder = preorderedList == null
                                    && isChildrenDrawingOrderEnabled();
                            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 (!child.canReceivePointerEvents()
                                        || !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;
                                }
                                // The accessibility focus didn't handle the event, so clear
                                // the flag and do a normal dispatch to all children.
                                ev.setTargetAccessibilityFocus(false);
                            }
                            if (preorderedList != null) preorderedList.clear();
                        }
                        if (nphpewTouchTarget == null && mFirstTouchTarget != null) {
                            // Did not find a child to receive the event.
                            // Assign the pointer to the least recently added target.
                            newTouchTarget = mFirstTouchTarget;
                            while (newTouchTarget.next != null) {
                                newTouchTarget = newTouchTarget.next;
                            }
                            newTouchTarget.pointerIdBits |= idBitsToAssign;
                        }
                    }
                }
                //-------------这里是拦截之后会走的地方-------------//
                // Dispatch to touch targets.
                if (mFirstTouchTarget == null) {
                    // No touch targets so treat this as an ordinary view.
                    handled = dispatchTransformedTouchEvent(ev, canceled, null,
                            TouchTarget.ALL_POINTER_IDS);
                } else {
                    // Dispatch to touch targets, excluding the new touch target if we already
                    // dispatched to it.  Cancel touch targets if necessary.
                    TouchTarget predecessor = null;
                    TouchTarget target = mFirstTouchTarget;
                    while (target != null) {
                        final TouchTarget next = target.next;
                        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                            handled = true;
                        } else {
                            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                    || intercepted;
                            if (dispatchTransformedTouchEvent(ev, cancelChild,
                                    target.child, target.pointerIdBits)) {
                                handled = true;
                            }
                            if (cancelChild) {
                                if (predecessor == null) {
                                    mFirstTouchTarget = next;
                                } else {
                                    predecessor.next = next;
                                }
                                target.recycle();
                                target = next;
                                continue;
                            }
                        }
                        predecessor = target;
                        target = next;
                    }
                }
                // Update list of touch targets for pointer up or cancel, if needed.
                if (canceled
                        || actionMasked == MotionEvent.ACTION_UP
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    resetTouchState();
                } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                    final int actionIndex = ev.getActionIndex();
                    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                    removePointersFromTouchTargets(idBitsToRemove);
                }
            }
            if (!handled && mInputEventConsistencyVerifier != null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
            }
            return handled;
        }
        

        1.1.1 万事皆始于ACTION_DOWN

        看着dispatchTouchEvent这么长的代码,是不是脑袋都昏了,我给伙伴们分下层,首先一切的事件分发都是从ACTION_DOWN事件开始,所以我们可以看下ACTION_DOWN事件是如何处理的。

        核心代码1:

        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 target开发者_JAV培训s and this action is not an initial down
            // so this view group continues to intercept touches.
            intercepted = true;
        }
        

        当ACTION_DWON事件来了之后,首先调用ViewGroup的dispatchTouchEvent方法,在上面这段代码中,就是判断ViewGroup是否要拦截这个事件,如果DOWN事件都被拦截了,就没有小弟的份了。

        所以如果当前是DOWN事件,或者mFirstTouchTarget不为空。首先这里有一个变量mFirstTouchTarget,我们可以认为这个就是可能会消费事件的View,因为首次肯定为空,但是当前为DOWN事件,所以这个条件是满足的,那么就会进入到代码块中。

        在代码块中,有一个disallowIntercept变量,这个变量标志着子View是否需要消费这个事件,如果需要消费这个事件,子View可以调用requestDisallowInterceptTouchEvent这个方法,设置为true,那么父容器就不会拦截。

        所以如果子View需要消费这个事件,那么disallowIntercept = true,这个时候intercepted = false,意味着父容器不会拦截;如果子View不消费这个事件,那么disallowIntercept = false,然后会判断ViewGroup中的onInterceptTouchEvent方法,是否由父容器消费这个事件从而决定intercepted的值。

        所以看到这里,其实我们在解决事件冲突的时候就会有两种方式:一种就是重写父容器的onInterceptTouchEvent方法,由父容器决定是否拦截;另一种就是由子View调用requestDisallowInterceptTouchEvent方法,通知父容器是否能够拦截。

        那么假设,当前ViewGroup要拦截这个事件,也就是在onInterceptTouchEvent中返回了true

        override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
            return true
        }
        

        1.1.2 ViewGroup拦截事件

        那么既然拦截了事件,那么当前ViewGroup就需要决定到底处不处理事件,如果不处理就需要向上传递。

        因为ViewGroup拦截了事件,因此intercepted = true,在1.3开头的代码中,我标记了2个位置,一个是拦截会走的位置,一个是没有拦截会走的位置。

        核心代码2:

        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        

        因为这个时候,mFirstTouchTarget还是为空的,所以会调用dispatchTransformedTouchEvent方法。

        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
            // ......
            // Perform any necessary transformations and dispatch.
            if (child == null) {
                handled = super.dispatchTouchEvent(transformedEvent);
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                transformedEvent.offsetLocation(offsetX, offsetY);
                if (! child.hasIdentityMatrix()) {
                    transformedEvent.transform(child.getInverseMatrix());
                }
                handled = child.dispatchTouchEvent(transformedEvent);
            }
            // Done.
            transformedEvent.recycle();
            return handled;
        }
        

        这时候需要注意一点,这个方法第三个参数为null; 所以当child为空的时候,就会调用父类的dispatchTouchEvent,也就是View的dispatchTouchEvent方法,在1.2小节中我们是对这个方法做过分析的,也是会决定是否处理这个事件,最终返回是否处理的结果。

        所以这一次的结果(handled的值)最终决定了当前ViewGroup是否会处理这个事件,如果不处理,那么就扔到上级再判断。

        1.1.3 ViewGroup不拦截事件

        如果ViewGroup不拦截事件,那么intercepted = false,所以会走到分发事件的代码中。

        核心代码3:

        if (!canceled && !intercepted) {
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                final int actionIndex = ev.getActionIndex(); // always 0 for down
                final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                        : TouchTarget.ALL_POINTER_IDS;
                // Clean up earlier touch targets for this pointer id in case they
                // h编程ave become out of sync.
                removePointersFromTouchTargets(idBitsToAssign);
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    final float x =
                            isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
                    final float y =
                            isMouseEvent ? ev.getYCursorPosition() : ev.getY(actionIndex);
                    // Find a child that can receive the event.
                    // Scan children from front to back.
                    final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                    final boolean customOrder = preorderedList == null
                            && isChildrenDrawingOrderEnabled();
                    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;
                        }
                        //-----判断View是否有消费的可能性---------//
                        if (!child.canReceivePointerEvents()
                                || !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;
                        }
                        // The accessibility focus didn't handle the event, so clear
                        // the flag and do a normal dispatch to all children.
                        ev.setTargetAccessibilityFocus(false);
                    }
                    if (preorderedList != null) preorderedList.clear();
                }
                if (newTouchTarget == null && mFirstTouchTarget != null) {
                    // Did not find a child to receive the event.
                    // Assign the pointer to the least recently added target.
                    newTouchTarget = mFirstTouchTarget;
                    while (newTouchTarget.next != null) {
                        newTouchTarget = newTouchTarget.next;
                    }
                    newTouchTarget.pointerIdBits |= idBitsToAssign;
                }
            }
        }
        

        这里首先会判断事件是否为down事件,只有down事件才会分发,如果是move或者up事件便不会分发。所以伙伴们需要牢记一点,如果在某个控件上产生了up事件,即便是设置了onClickListener,因为没有接收到down事件,所以也不会响应点击事件。

        然后调用buildTouchDispatchChildList方法,对当前ViewGroup全部的子View根据Z轴顺序排序,

        ArrayList<View> buildOrderedChildList() {
            final int childrenCount = mChildrenCount;
            if (childrenCount <= 1 || !hasChildWithZ()) return null;
            if (mPreSortedChildren == null) {
                mPreSortedChildren = new ArrayList<>(childrenCount);
            } else {
                // callers should clear, so clear shouldn't be necessary, but for safety...
                mPreSortedChildren.clear();
                mPreSortedChildren.ensureCapacity(childrenCount);
            }
            final boolean customOrder = isChildrenDrawingOrderEnabled();
            for (int i = 0; i < childrenCount; i++) {
                // add next child (in child order) to end of list
                final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
                final View nextChild = mChildren[childIndex];
                final float currentZ = nextChild.getZ();
                // insert ahead of any Views with greater Z
                int insertIndex = i;
                while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
                    insertIndex--;
                }
                mPreSortedChildren.add(insertIndex, nextChild);
            }
            return mPreSortedChildren;
        }
        

        这里我们可以看到是按照Z轴值从高到低排序,Z值越大,说明其层级越深,最终拿到一个View的集合

        然后遍历取值的时候,是按照倒序取值的方式,因为Z值越小,说明其层级越浅,事件被消费的概率就越高;取出一个View之后,首先需要判断它是否具备消费事件的可能性。

        if (!child.canReceivePointerEvents()
                || !isTransformedTouchPointInView(x, y, child, null)) {
            ev.setTargetAccessibilityFocus(false);
            continue;
        }
        

        第一个条件:View是可见的,或者 getAnimation() != null

        protected boolean canReceivePointerEvents() {
            return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null;
        }
        

        第二个条件:当前View在点击(x,y)的范围之内,如果离着手指点击的位置很远,肯定不可能消费。

        protected boolean isTransformedTouchPointInView(float pythonx, float y, View child,
                PointF outLocalPoint) {
            final float[] point = getTempLocationF();
            point[0] = x;
            point[1] = y;
            transformPointToViewLocal(point, child);
            final boolean isInView = child.pointInView(point[0], point[1]);
            if (isInView && outLocalPoint != null) {
                outLocalPoint.set(point[0], point[1]);
            }
            return isInView;
        }
        

        所以经过层层筛选,也就只剩下一小部分可能会消费事件的View,那么怎么把他揪出来呢?经过筛选的View最终调用了dispatchTransformedTouchEvent方法,在1.1.2中我们介绍了这个方法,就是用来判断是否消费事件的,这里传入的第三个参数不为空!

        回到前面dispatchTransformedTouchEvent方法中,当child不为空的时候,走到else代码块中,最终还是调用了child的dispatchTouchEvent方法。

        所以如果当前View消费了DOWN事件,那么返回值为true,也就是dispatchTransformedTouchEvent返回了true,那么会进入下面代码块中。

        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 &lt; childrenCount; j++) {
                    if (children[childIndex] == mChildren[j]) {
                        mLastTouchDownIndex = j;
                        break;
                    }
                }
            } else {
                mLastTouchDownIndex = childIndex;
            }
            mLastTouchDownX = ev.getX();
            mLastTouchDownY = ev.getY();
            //----- 这里就是给mFirstTouchTarget赋值--------//
            newTouchTarget = addTouchTarget(child, idBitsToAssign);
            alreadyDispatchedToNewTouchTarget = true;
            break;
        }
        

        因为当前child消费了事件,那么我们前面提到的mFirstTouchTarget就是由child封装一层得来的,也就是调用了addTouchTarget方法,也就是说当一个child消费了一个DOWN事件之后,mFirstTouchTarget就不再为空了。

        private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
            final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
            target.next = mFirstTouchTarget;
            mFirstTouchTarget = target;
            return target;
        }
        

        如果全部都不处理,那么mFirstTouchTarget还是为空,走到下面还是会执行ViewGroup拦截事件的逻辑,也就是1.1.2中的逻辑,所以说,如果全部的子View都不处理,其实跟ViewGroup拦截事件的本质是一致的

        1.4 ViewGroup的事件分发 -- ACTION_MOVE

        前面我们介绍了ViewGroup对于ACTION_DOWN事件的分发处理,因为DOWN事件只有一次,MOVE可以有无数次,所以在处理完DOWN事件之后,就会有MOVE事件涌进来。

        所以还是回到前面的判断条件中,我们对于MOVE事件的分发,需要基于DOWN事件的处理;

        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags &amp; 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;
        }
        

        如果ViewGroup拦截了事件:

        那么mFirstTouchTarget == null,会走到else中,此时 intercepted = true,那么就会走到ViewGroup拦截逻辑中,会调用dispatchTransformedTouchEvent,第三个参数child == null,那么如果ViewGroup不消费不处理,就会交给上级处理。

        如果ViewGroup不拦截事件:

        那么mFirstTouchTarget != null,此时还是会判断子View是否拦截该事件,如果拦截,那么intercepted = true,还是会走上面的拦截逻辑;如果不拦截,那么intercepted = false,会走到ViewGroup不拦截事件的逻辑中。

        if (newTouchTarget == null && mFirstTouchTarget != null) {
            // Did not find a child to receive the event.
            // Assign the pointer to the least recently added target.
            newTouchTarget = mFirstTouchTarget;
            while (newTouchTarget.next != null) {
                newTouchTarget = newTouchTarget.next;
            }
            newTouchTarget.pointerIdBits |= idBitsToAssign;
        }
        

        因为只有DOWN事件的时候,才会遍历View树,如果是MOVE事件,不会进入循环,不再分发,而是走上面的逻辑,此时newTouchTarget == null 而且 mFirstTouchTarget不为空,此时会给newTouchTarget重新赋值,然后继续往下走。

        if (mFirstTouchTarget == null) {
            // No touch targets so treat this as an ordinary view.
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            // Dispatch to touch targets, excluding the new touch target if we already
            // dispatched to it.  Cancel touch targets if necessary.
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        

        因为mFirstTouchTarget != null,因此会走到else代码块中,因为alreadyDispatchedToNewTouchTarget是在事件分发时才赋值为true,所以在while循环中(一次循环,单点触控),会走else代码块,其实还是会调用dispatchTransformedTouchEvent方法判断是否处理事件,所以这就形成了一条责任链,当一个View消费了DOWN事件之后,后续的事件系统默认都会给他消费,除非特殊情况。

        2 Android事件冲突处理

        基于Android事件分发机制,DOWN事件只会执行一次,而且只是做分发工作,而MOVE事件会有无数次,所以对于事件冲突来说,只能在MOVE事件中进行处理。

        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            final boolean disallowIntercept = (mGroupFlags &amp; 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;
        }
        

        针对这种分发机制,前面也提到了两种处理方式,要么在父容器的onInterceptTouchEvent中判断是否拦截事件,要么控制disallowIntercept的值,所以就出现了2种拦截法。

        2.1 内部拦截法

        此方式指的是在子View中,通过控制disallowIntercept的值,来让父容器决定是否拦截事件。

        class MotionEventLayout(
            val mContext: Context,
            val attrs: AttributeSet? = null,
            val defStyleAttr: Int = 0
        ) : FrameLayout(mContext, attrs, defStyleAttr) {
            override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
                return true
            }
        }
        

        如果在父容器的onInterceptTouchEvent方法中返回true,那么down一定会被拦截而不会分发给子View,所以子View不会响应任何事件。

        class MotionEventChildLayout(
            val mContext: Context,
            val attrs: AttributeSet? = null,
            val defStyleAttr: Int = 0
        ) : FrameLayout(mContext, attrs, defStyleAttr) {
            private var startX = 0
            private var startY = 0
            override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
                when (ev?.action) {
                    MotionEvent.ACTION_DOWN -> {
                        //不能被拦截
                        parent.requestDisallowInterceptTouchEvent(true)
                    }
                    MotionEvent.ACTION_MOVE -> {
                        var endX = ev.rawX
                        var endY = ev.rawY
                        //竖向滑动
                        if (abs(endX - startX) > abs(endY - startY)) {
                            parent.requestDisallowInterceptTouchEvent(false)
                        }
                        startX = endX
                        startY = endY
                    }
                }
                return super.dispatchTouchEvent(ev)
            }
        }
        

        所以使用内部拦截法时,对于DOWN事件不能被拦截,需要将requestDisallowInterceptTouchEvent设置为true,这样父容器在分发事件时,就不会走自身的onInterceptTouchEvent方法(此时无论设置true或者false都是无效的),intercepted = false,此时事件就会被分发到子View。

        然后在滑动时,如果父容器支持左右滑动,子View支持上下滑动,那么就可以判断:如果横向滑动的距离大于竖直方向滑动的距离,任务在左右滑动,此时事件处理交给父容器处理;反之则交给子View处理。

        这是我们理解中的处理方式,看着好像没问题,但是实际运行时发现无效!! 我们明明设置了requestDisallowInterceptTouchEvent为true,为什么没生效呢?

        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();
        }
        

        通过源码我们发现,当DOWN事件触发之后,会清除所有的标志位,包括disallowIntercept,所以在使用内部拦截法的时候,我们需要保证外部容器不能拦截DOWN事件,其实这个不会有问题的,大不了所有的子View都不处理,最终再扔给你处理。

        override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
            if (ev?.action == MotionEvent.ACTION_DOWN) {
                super.onInterceptTouchEvent(ev)
                return false
            }
            return true
        }
        

        所以在父容器的onInterceptTouchEvent方法中,不能对DOWN事件进行拦截,这里返回了false。

        因为父容器没有拦截down事件,所以事件被分发给了子View(可以上下滑动),紧接着MOVE事件来了,全部交给了子View处理,这时的mFirstTouchTarget还是子View的。如果用户手势改成了左右滑动,那么这个过程两者是如何完成转换的呢?

        此时,mFirstTouchTarget != null,action == MOVE,disallowIntercept = false,因为是move事件,所有标志位不会被清除,此时会走到这里。

        if (!disallowIntercept) {
            intercepted = onInterceptTouchEvent(ev);
            ev.setAction(action); // restore action in case it was changed
        }
        

        此时,父容器的onInterceptTouchEvent返回的是true,要拦截子View的事件了,此时intercepted = true,因为mFirstTouchTarget != null,所以在拦截逻辑里,是会走到else代码块中的。

        while (target != null) {
            final TouchTarget next = target.next;
            if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                handled = true;
            } else {
                final boolean cancelChild = resetCancelNextUpFlag(target.child)
                        || intercepted;
                if (dispatchTransformedTouchEvent(ev, cancelChild,
                        target.child, target.pointerIdBits)) {
                    handled = true;
                }
                if (cancelChild) {
                    if (predecessor == null) {
                        mFirstTouchTarget = next;
                    } else {
                        predecessor.next = next;
                    }
                    target.recycle();
                    target = next;
                    continue;
                }
            }
            predecessor = target;
            target = next;
        }
        

        因为这个时候 intercepted = true,所以cancelChild = true,所以在dispatchTransformedTouchEvent方法中,第二个参数为true。

        private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
                View child, int desiredPointerIdBits) {
            final boolean handled;
            // Canceling motions is a special case.  We don't need to perform any transformations
            // or filtering.  The important part is the action, not the contents.
            final int oldAction = event.getAction();
            if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
                event.setAction(MotionEvent.ACTION_CANCEL);
                if (child == null) {
                    handled = super.dispatchTouchEvent(event);
                } else {
                    handled = child.dispatchTouchEvent(event);
                }
                event.setAction(oldAction);
                return handled;
            }
        }
        

        这时会触发一个ACTION_CANCEL事件,这个事件是子View事件被上层拦截的时候触发的,其实当前这个MOVE事件做的一件事,就是执行了子View的cancel事件,然后将mFirstTouchTarget置为了空;因为MOVE事件很多,所以下个MOVE事件进来之后,又会走到判断是否拦截的逻辑中。

        此时父容器会冷酷地拦截这些MOVE事件,原本属于子View的MOVE事件,而且不会往下分发,走到拦截逻辑中,因为此时mFirstTouchTarget为空,所以直接由自身决定是否消费,肯定消费了,因为在左右滑动,也就是这样完成的事件消费处理权的切换。

        2.2 外部拦截法

        那么对于外部拦截法,则是需要动态修改onInterceptTouchEvent的返回值,如果用户左右滑动,那么就拦截,onInterceptTouchEvent返回true,此时intercepted = true,就不再走事件分发流程了。

        class MotionEventLayout(
            val mContext: Context,
            val attrs: AttributeSet? = null,
            val defStyleAttr: Int = 0
        ) : FrameLayout(mContext, attrs, defStyleAttr) {
            private var startX = 0f
            private var startY = 0f
            override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean {
                var intercepted = false
                when (ev?.action) {
                    MotionEvent.ACTION_DOWN -> {
                    }
                    MotionEvent.ACTION_MOVE -> {
                        val endX = ev.rawX
                        val endY = ev.rawY
                        //竖向滑动
                        intercepted = abs(endX - startX) > abs(endY - startY)
                        startX = endX
                        startY = endY
                    }
                }
                return intercepted
            }
        }
        

        相较于内部拦截法,外部拦截就显得比较简单了,完全由父容器发牌决定。

        以上就是Android进阶事件分发机制解决事件冲突的详细内容,更多关于Android事件冲突解决的资料请关注我们其它相关文章!

        0

        上一篇:

        下一篇:

        精彩评论

        暂无评论...
        验证码 换一张
        取 消

        最新开发

        开发排行榜