信任同伴们在日常的开发工作中,必定会遇到事情抵触的问题,e.g. 一个页面当手指滑动的时分,会翻到下一页;点击的时分,需求呼应页面中的元素点击事情,这个时分假如没有处理滑动事情,或许遇到的问题便是在滑动翻页的时分却只呼应了点击事情,这个便是点击事情与滑动事情的抵触。其实还有许多常见的经典事情,e.g. RecyclerView嵌套滑动,ViewPager与RecyclerView嵌套滑动等,所以这个时分咱们需求对事情分发十分了解,才干针对需求做相应的处理。

1 Android 事情分发机制

这是一个老生常谈的问题,信任同伴们都了解常见的Android事情类型:ACTION_DOWN、ACTION_MOVE、ACTION_UP,别离代表手指按下屏幕的事情、手指滑动的事情以及手指抬起的事情,那么从手指按下到事情呼应,中心阅历了什么呢?咱们从Google的源码中去寻觅答案。

1.1 事情分发流程

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

Android进阶宝典 -- 事件冲突怎么解决?先从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) {
                    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)) {
                                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这个属性要求十分严厉,必须要设置为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 (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;
                }
            }
        }
        //-------------这儿是阻拦之后会走的当地-------------//
        // 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 targets 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
        // 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;
                }
                //-----判别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 x, 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 < 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 & 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 & 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
    }
}

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