Android View 事件分發機制 源碼解析

標簽: Android 源碼解析 View


關于View的事件分發機制,網上有不少好文。基本上的模式都是:總體概括->源碼分析(Demo實驗)->得出結論,由于每個人的切入角度不同,看得越多反而覺得腦子越暈(二手知識)。因此,本篇博客會直接從結論入手,帶著疑問再去源碼種尋找答案,盡量做到把二手知識還原成一手知識。不當、不足之處歡迎指出和討論~

*注:源碼基于api-24

1.結論

結論出處來源于主席的《Android開發藝術探索》,在原書中作者是按照整個事件的分發流程來講述的,這里不再拾人牙慧。我們換一個切入點,直接從結論入手在源碼中尋找答案。

1.1《Android開發藝術探索》

1).同一個事件序列是指手機接觸屏幕那一刻起,到離開屏幕那一刻結束,有一個down事件,若干個move事件,一個up事件構成。
我們直接先看在事件分發機制中最重要的幾個方法的簽名:

public boolean dispatchTouchEvent(MotionEvent event)        
public boolean onInterceptTouchEvent(MotionEvent ev)      
public boolean onTouchEvent(MotionEvent event)

不論是哪個方法,無一例外都對于 MotionEvent 而言的。即任意一個事件都可能會獨立的走完以上三個步驟。另外個人認為 ACTION_CANCEL 這個事件也是應該被算在一個【事件序列】中的。理由如下:

  1. 在 onTouchEvent 中處理了 ACTION_CANCEL 這個事件,主要是對View狀態和標記位的重置:
 public boolean onTouchEvent(MotionEvent event) {
    \\...
    switch(action){
        case MotionEvent.ACTION_UP:
            \\...
            break;
        case MotionEvent.ACTION_DOWN:
            \\...
            break;
        case MotionEvent.ACTION_CANCEL:
              setPressed(false);
              removeTapCallback();
              removeLongPressCallback();
              mInContextButtonPress = false;
              mHasPerformedLongPress = false;
              mIgnoreNextUpEvent = false;
            break;
        case MotionEvent.ACTION_MOVE:
            \\...
            break;
    }
    return true;
    \\...
 }

2.從官方文檔中,我也可以看出 ACTION_DOWN 也是應該被算入一個 gesture 中的,也規定了我們應該如何處理它:

The current gesture has been aborted. You will not receive any more points in it. You should treat this as an up event, but not perform any action that you normally would.

2).某個View一旦決定攔截事件,那么這個事件序列之后的事件都會由它來處理,并且不會再調用onInterceptTouchEvent。
一個 View 既然有截攔事件的能力(注意不是消費)那么它一定是一個 ViewGroup 。因為只有 ViewGroup 中有 onInterceptTouchEvent 這個方法。而其既然截攔了事件,就是說它的 onInterceptTouchEvent 方法返回了 true。OK 條件有了,順著源碼我們再去證明后面的結論。找到 onInterceptTouchEvent 方法被調用的地方:

public boolean dispatchTouchEvent(MotionEvent ev){
      // ...
      // 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 (!canceled && !intercepted) {
        //...
    }
    
      // 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 {
        //...
    }
}

ACTION_DOWN到來時,mFirstTouchTarget 還沒有被賦值,但滿足前半部分條件,進入 if 分支 ——> 默認沒有設置 FLAG_DISALLOW_INTERCEPT 這個標記位,disallowIntercept = false ,再進入下一個 if 分支 ——> 變量 intercepted 被賦值為 onInterceptTouchEvent 的返回值,即為 true。再往下看,最長的那個 if 分支我們進不去了(突然輕松了好多~)。mFirstTouchTarget 為空 ——> 調用 dispatchTransformedTouchEvent 這個方法:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
 // 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);
        }
}

由于 child 為 null ,直接調用了其父類,即 View 的 dispatchTouchEvent 方法。到這里,后面的代碼我們也暫時不需要仔細去分析了。由于 mFirstTouchTarget 在整個 ACTION_DOWN 的流程中都沒有被賦值,在之后的事件序列中,變量 intercepted 依然為 true,依然調用其父類 View 的 dispatchTouchEvent 方法,最終都會交由其 onTouchEvent 方法處理,結論 2) 得證。

3)正常情況下,一個事件序列只能被一個View攔截并消耗。這個原因可以參考第2條,因為一旦攔截了某個事件,那么這個事件序列里的其他事件都會交給這個View來處理,所以同一事件序列中的事件不能分別由兩個View同時處理,但是我們可以通過特殊手段做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
這條結論的前半句已經分析過了,我們直接來看看后半句的兩個結論:
a)同一事件序列中的事件不能分別由兩個View同時處理:

 if(!canceled &&!intercepted){
        //...
        final View[] children = mChildren;
        for (int i = childrenCount - 1; i >= 0; i--) {
            final int childIndex = customOrder
                    ? getChildDrawingOrder(childrenCount, i) : i;
            final View child = (preorderedList == null)
                    ? children[childIndex] : preorderedList.get(childIndex);

            // If there is a view that has accessibility focus we want it
            // to get the event first and if not handled we will perform a
            // normal dispatch. We may do a double iteration but this is
            // safer given the timeframe.
            if (childWithAccessibilityFocus != null) {
                if (childWithAccessibilityFocus != child) {
                    continue;
                }
                childWithAccessibilityFocus = null;
                i = childrenCount - 1;
            }

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

            newTouchTarget = getTouchTarget(child);
            if (newTouchTarget != null) {
                // Child is already receiving touch within its bounds.
                // Give it the new pointer in addition to the ones it is handling.
                newTouchTarget.pointerIdBits |= idBitsToAssign;
                break;
            }

            resetCancelNextUpFlag(child);
            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                // Child wants to receive touch within its bounds.
                mLastTouchDownTime = ev.getDownTime();
                if (preorderedList != null) {
                    // childIndex points into presorted list, find original index
                    for (int j = 0; j < childrenCount; j++) {
                        if (children[childIndex] == mChildren[j]) {
                            mLastTouchDownIndex = j;
                            break;
                        }
                    }
                } else {
                    mLastTouchDownIndex = childIndex;
                }
                mLastTouchDownX = ev.getX();
                mLastTouchDownY = ev.getY();
                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                alreadyDispatchedToNewTouchTarget = true;
                break;
            }

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

回到 Viewgroup 中最長的那個 if 分支(該來的還是回來的)。找到
調用 dispatchTransformedTouchEvent 方法的那個 if 分支:

 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)){
 //...
 break;
 }

關于 dispatchTransformedTouchEvent 這個上面已經提到過,這里傳入的 child 不為空,所以直接調用 child 的 dispatchTouchEvent 方法,該方法又會去調用 child 的 onTouchEvent 方法。根據該結論給出的條件,可知第一個子 View 處理了該事件,即 onTouchEvent 方法返回 true。最終 dispatchTransformedTouchEvent 方法也返回 true 進入到該 if 分支。直接看到這個 if 分支的最后:一個 break 語句。這樣我們就直接跳出了最外層遍歷子 View 的 for 循環。這個事件也就傳遞不到其它的子 View 了。結論得證。

b)可以通過特殊手段做到,比如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其他View處理。
上面我們證明了:在正常情況下,一個事件是不能同時由兩個 View 處理的,那么如果要讓兩個 View 都去處理同一個事件呢?先不去思考結論中給出的方法,思考一下造成兩個 View 不能同時處理某個事件的根本原因。這主要是因為在遍歷子 View 的時候 dispatchTransformedTouchEvent 這個方法返回 true ,進入到 if 分支,最后 break 跳出了整個遍歷。那么很簡單,我們只要讓 dispatchTransformedTouchEvent 這個方法返回 false 就可以了。上面我們講過在正常情況下,這個方法會調用 View 的 dispathcTouchEvent 方法,繼而調用 View 的 onTouchEvent 方法。那么我們只需要讓 onTouchEvent 這個方法返回 false 即可。

 public boolean onTouchEvent(MotionEvent event) {
    \\...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch(action){
                case MotionEvent.ACTION_UP:
                    \\...
                    break;
                case MotionEvent.ACTION_DOWN:
                    \\...
                    break;
                case MotionEvent.ACTION_CANCEL:
                    \\...
                    break;
                case MotionEvent.ACTION_MOVE:
                    \\...
                    break;
            }
        return true;
    }
    return false;
 }

根據上面的源碼,一個 View 只要是可點擊的,就最終會返回 true (消費該事件)。因此,如果我們想要讓 View 自己去處理該事件同時還返回 false 的話就,重寫 View 的 onTouchEvent 方法,在處理完事件后返回 false 即可。這樣,就可以讓多個 View 去處理同一事件。最后,我們再看看結論中給出的方法,其實總體上思路也是差不多的。最大的區別在于,我們給出的方法中事件還需要經過層層傳遞給我們指定的 View,而結論中給出的方法則是直接將事件傳遞給了指定的 View 更加簡單粗暴。最后,結論3得證。

4)一個View如果開始處理事件,如果它不處理down事件(onTouchEvent里面返回了false),那么這個事件序列的其他事件就不會交給它來繼續處理了,而是會交給它的父元素去處理。

public boolean dispatchTouchEvent(MotionEvent ev){
      // ...
      // 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 (!canceled && !intercepted) {
        //...
         for (int i = childrenCount - 1; i >= 0; i--) {
            //...
             if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                //...
                newTouchTarget = addTouchTarget(child, idBitsToAssign);                                                                 
                //...
                break;
            }
         }
    }
    
      // 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 {
        //...
    }
}

根據上述代碼,當子 View 沒有去處理 ACTION_DOWN 事件時,dispatchTransformedTouchEvent 方法返回 false。沒有調用 addTouchTarget 方法,這個方法又做了些什么呢?

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

我們可以看到在這個方法中,mFirstTouchTarget 這個變量會被賦值。而由于沒有進入到該 if 分支,mFirstTouchTarget 始終為 null,當下一個事件來臨時 intercepted 為 true,進入到最后一個 if 分支并調用 dispatchTransformedTouchEvent 這個方法。由于傳入的 child 為空, ViewGroup 又會直接去調用父類 View 中的 dispatchTouchEvent 方法,最終調用 View 的 onTouchEvent 方法。即整個事件最終就交給父 View 去處理了。結論4得證。

5)如果一個View處理了down事件,卻沒有處理其他事件,那么這些事件不會交給父元素處理,并且這個View還能繼續受到后續的事件。而這些未處理的事件,最終會交給Activity來處理。
回到結論4)中的代碼,當 View 除理了 ACTION_DOWN 時間后,由于 onTouchEvent 返回 true ,所以最終 dispatchTransformedTouchEvent 也會返回 true ,此時執行 addTouchTarget 方法,mFirstTouchTarget 也會被賦值。當其它事件來臨時,會進入到最后一個 else 分支中,讓我們看看這個分支里做了些什么:

    //...
            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;
                }
            }

    //...   
    return handled;        

可以看到在這個分支中會一步步遍歷 TouchTarget 這樣一個類似鏈表的結構,若 dispatchTransformedTouchEvent 返回 true ,則 handled 被設置為 true ,整個 ViewGroup 方法也最終會返回 handled 的值,表示事件有沒有被處理。若沒有處理,則會一級一級向上傳遞,直到 DecorView, 最后再交給 PhoneWindow :

   @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

而 PhoneWinow 的 superDispatchTouchEvent 是在 Activity 的 dispatchTouchEvent 方法中被調用的,若該方法返回 false 事件會最終交給 Activity 的 onTouchEvent 去處理。至此,我們也看到事件分發的起源是 Activity。結論 5) 得證。

  public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

6)ViewGroup 的 onInterceptToucheEvent 默認返回 false,也就是默認不攔截事件。
直接看 ViewGroup 中onInterceptTouchEvent 的默認實現:

public boolean onInterceptTouchEvent(MotionEvent ev) {
    return false;
}

至于為什么返回 false 就代表不回去截攔事件,上面的結論中以及論述過了,這里簡單回顧下:
ACTION_DOWN 事件到來 ——> onInterceptTouchEvent 返回 false 所以 intercepted 為 false ——> 進入到遍歷子 View 的循環 ——> 事件被分發給子 View ——> mFirstTouchTarget 被賦值 ——> 其它事件到來 ——> mFirstTouchTarget 不為空 ——> onInterceptTouchEvent 返回 false 所以 intercepted 依然為 false ——> 依然進入到到子 View 的循環 ——> 事件被分發給子 View。結論6)得證。

7)View 沒有 InterceptTouchEvent 方法,如果有事件傳過來,就會直接調用 onTouchEvent 方法。
cmd + F 在 View 類中查詢,的確沒有 InterceptTouchEvent 方法。我們再反過來想想,道理其實也是顯而易見的:由于子 View 的概念是對于 ViewGroup 來講的,是否要去截攔事件也是對于 ViewGroup 來說的。并且作為一個在 ViewTree 中位于末尾的元素來講,View 也只需處理好傳遞給自己的事件即可。我們再來看看 View 的 dispatchTouchEvent 方法:

    public boolean dispatchTouchEvent(MotionEvent event) {
        //...
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        //...
        return result;
    }

根據前面的分析,我們可以發現不論是 View 還是 ViewGroup,事件的分發總是從 dispatchTouchEvent 這個方法開始。在 View 的 dispatchTouchEvent 方法中,默認情況下,最終會調用 View 的 onTouchEvent fa那個法。結論7)得證。

8)View的onTouchEvent方法默認都會消耗事件,也就是默認返回true,除非他是不可點擊的(longClickable和clickable同時為false)。
9)View的enable屬性不會影響onTouchEvent的默認返回值。就算一個View是不可見的,只要他是可點擊的(clickable或者longClickable有一個為true),它的onTouchEvent默認返回值也是true。
有關這兩條結論的整個這部分的代碼如下:

 public boolean onTouchEvent(MotionEvent event) {
    \\...
    if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch(action){
                case MotionEvent.ACTION_UP:
                    \\...
                    break;
                case MotionEvent.ACTION_DOWN:
                    \\...
                    break;
                case MotionEvent.ACTION_CANCEL:
                    \\...
                    break;
                case MotionEvent.ACTION_MOVE:
                    \\...
                    break;
            }
        return true;
    }
    return false;
 }

可以看到,只要進入了 if 語句,整個 onTouchEvent 方法的返回值就一定是 true ,也就代表消耗了改事件。而要進入該 if 語句的條件則是 View 的 clickable 或 longClickable 以及 contextClickable 這三個屬性其中任意一個為 true 即可。需要聲明的是 contextClickable 這個屬性是在 api23 中 加入的,而作者的結論是基于 api21 的。所以我們這里的結論會更作者的結論有所出入。關于 contextClickable 這個屬性根據網上的資料來看可能跟帶有觸筆或帶有鼠標功能的設備有關。結論8)和結論9)得證。

10)onClick 方法會執行的前提是當前 View 是可點擊的,并且它收到了 down 和 up 事件。
我們單獨抽出 ACTION_DOWN 和 ACTION_UP 這兩個分支的代碼來看:

        case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

                        if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if (!post(mUnsetPressedState)) {
                            // If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break;

        case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Walk up the hierarchy to determine if we're inside a scrolling container.
                    boolean isInScrollingContainer = isInScrollingContainer();

                    // For views inside a scrolling container, delay the pressed feedback for
                    // a short period in case this is a scroll.
                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
                    } else {
                        // Not inside a scrolling container, so show the feedback right away
                        setPressed(true, x, y);
                        checkForLongClick(0);
                    }
                    break;

關于“首先 View 能執行點擊事件的前提是 View 是可點擊的“這一點在上兩條的論述中已經說明了。我們再來看看為什么 ACTION_DOWN 和 ACTION_UP 這兩個事件都被需要。當 ACTION_DOWN 事件來臨時,mHasPerformedLongPress 被賦值為 false ,同時通過調用 setPressed 方法,mPrivateFlags 也被賦值。繼而在 ACTION_UP 事件來臨時,進入到 if 分支 調用 performClick 方法。performClick 方法如下:

  public boolean performClick() {
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
        return result;
    }

可見,在該方法中回調了我們所設置的監聽事件,從而 View 的 onClick 方法被調用。結論10)得證。

11)事件傳遞過程是由外向內的,也就是事件會先傳給父元素在向下傳遞給子元素。但是子元素可以通 requestDisallowInterceptTouchEvent 來干預父元素的分發過程,但是 down 事件除外(因為 down 事件方法里,會清除所有的標志位)。
關于“事件的傳遞過程是由外向內的這一點”在上面的論述中我們或多或少已經分析到了。這里我們主要關注一下 requestDisallowInterceptTouchEvent 這個方法:

 public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }

當我們在子 View 中掉用該方法時,父 View 的 mGroupFlags 會被設置。

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

由于 mGroupFlags 已經被設置了,所以當 ACTION_MOVE 和 ACTION_UP 事件到來時,此時 disallowIntercept 為 true (當 mGroupFlags 未被設置的時候,disallowIntercept 為 false,也即默認情況下均為 false),最終 intercepted 被賦值為 false 。這樣一來,本來應該有父 View 處理的事件都交由子 View 去處理了。那 ACTION_DOWN 事件呢?當 ACTION_DOWN 事件到來時,回到最開頭,我們發現會調用 resetTouchState 方法:

private void resetTouchState() {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }

在該方法中,mGroupFlags 這個標記為會被清空。從而對于 ACTION_DOWN 事件來說,無論子 View 是否調用了 requestDisallowInterceptTochEvent 這個方法設置了父 View 中 mGroupFlags 這標記位,都不會對 ACTION_DOWN 這個事件的分發造成影響。結論11)得證。

2.待續

關于 Android View 事件分發機制這一塊,網上還有許許多多“有趣”的結論,本文也會陸續收集一些有趣的結論,然后試著從源碼角度分析。歡迎大家和我討論~

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容