Android進階寶典 -- 事件衝突怎麼解決?先從Android事件分發機制開始說起

語言: CN / TW / HK

相信夥伴們在日常的開發工作中,一定會遇到事件衝突的問題,e.g. 一個頁面當手指滑動的時候,會翻到下一頁;點選的時候,需要響應頁面中的元素點選事件,這個時候如果沒有處理滑動事件,可能遇到的問題就是在滑動翻頁的時候卻只響應了點選事件,這個就是點選事件與滑動事件的衝突。其實還有很多常見的經典事件,e.g. RecyclerView巢狀滑動,ViewPager與RecyclerView巢狀滑動等,所以這個時候我們需要對事件分發非常瞭解,才能針對需求做相應的處理。

1 Android 事件分發機制

這是一個老生常談的問題,相信夥伴們都瞭解常見的Android事件型別:ACTION_DOWN、ACTION_MOVE、ACTION_UP,分別代表手指按下螢幕的事件、手指滑動的事件以及手指抬起的事件,那麼從手指按下到事件響應,中間經歷了什麼呢?我們從Google的原始碼中去尋找答案。

1.1 事件分發流程

因為對於元件來說,這個事件要麼消費要麼不消費(事件處理),而對於容器來說,還需要做的一件事就是分發事件,通常是先分發後處理,而View就只是處理事件。

image.png

因此在進行事件衝突處理的時候,對於事件是否向下分發給子View消費,就需要在父容器中做攔截,子View僅做事件消費。

1.2 View的事件消費

首先我們先不看事件是如何分發的,先關注下事件是如何被處理的,在View的dispatchTouchEvent方法中,就包含對事件的處理全過程。 ```java 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方法,我們看下這個方法實現。 ```java 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方法。java 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,那麼點選事件將不再響應。 ```kotlin 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方法。

```java 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: java 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 kotlin override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { return true }

1.1.2 ViewGroup攔截事件

那麼既然攔截了事件,那麼當前ViewGroup就需要決定到底處不處理事件,如果不處理就需要向上傳遞。

因為ViewGroup攔截了事件,因此intercepted = true,在1.3開頭的程式碼中,我標記了2個位置,一個是攔截會走的位置,一個是沒有攔截會走的位置。

核心程式碼2:

java 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方法。 ```java 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: ```java 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軸順序排序, ```java ArrayList 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之後,首先需要判斷它是否具備消費事件的可能性。 java if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } 第一個條件:View是可見的,或者 getAnimation() != null java protected boolean canReceivePointerEvents() { return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() != null; } 第二個條件:當前View在點選(x,y)的範圍之內,如果離著手指點選的位置很遠,肯定不可能消費。 java 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,那麼會進入下面程式碼塊中。 ```java 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就不再為空了。

java 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事件的處理; java 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不攔截事件的邏輯中。

java 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重新賦值,然後繼續往下走。

java 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事件中進行處理。

java 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的值,來讓父容器決定是否攔截事件。

```kotlin 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,為什麼沒生效呢? java 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都不處理,最終再扔給你處理。

```kotlin 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事件,所有標誌位不會被清除,此時會走到這裡。

java if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } 此時,父容器的onInterceptTouchEvent返回的是true,要攔截子View的事件了,此時intercepted = true,因為mFirstTouchTarget != null,所以在攔截邏輯裡,是會走到else程式碼塊中的。

java 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。 ```java 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
}

} ``` 相較於內部攔截法,外部攔截就顯得比較簡單了,完全由父容器發牌決定。