Android事件分發機制本質是樹的深度遍歷(圖+原始碼)

cheneasternsun發表於2018-07-22

什麼是事件分發機制?


相關方法

事件分發機制相關的幾個方法:
View

  • dispatchTouchEvent():處理事件。(:不分發)
  • onTouchEvent():觸發 onClick() 等點選事件的回撥。
    onTouchEvent() 在基類 View 的 dispatchTouchEvent() 中被呼叫。onTouchEvent()在 clickable 或 longclickable 或 contextclickable 時預設返回true,即消耗事件。

ViewGroup

  • dispatchTouchEvent():分發事件

    1. 判斷當前事件是否需要呼叫 onInterceptTouchEvent() 對事件進行攔截。
    2. 若 onInterceptTouchEvent() 返回 true 則攔截,並處理事件。否則,則將事件分發給子View。
    3. 若沒有子View 或事件不被下層 View 消耗(即所有子View的 dispatchTouchEvent() 返回false)則呼叫基類 View 的dispatchTouchEvent() 對事件進行處理。
  • onInterceptTouchEvent():決定是否攔截事件(即是否分發給子View),返回true 表示攔截,否則不攔截。
    onInterceptTouchEvent()(View沒有這個方法)在 ViewGroup 的dispatchTouchEvent() 中被呼叫。onInterceptTouchEvent()預設返回false,即不攔截。

  • onTouchEvent():繼承自 View,不重寫。

概念

概念:事件分發機制就是事件(MotionEvent)如何在view tree 中分發、處理的一種規則。

  1. 事件分發
    概念:一個事件產生後,會先從Activity分發給Window,Window再分發給它裡面的頂級View(是view tree 的根)。頂級View接受到事件後會呼叫自身的dispatchTouchEvent(),在該方法中會迭代呼叫子View的 dispatchTouchEvent() 直到事件被攔截或消耗若事件被攔截或消耗則結束分發/遍歷),這個過程實質就是對view tree的深度遍歷

    事件分發只發生在 Activity 和 ViewGroup 中,View 不分發。 Activity 和 ViewGroup 對事件的分發都是通過呼叫 dispatchTouchEvent() 。

    • 事件分發何時結束?

      1. 事件被攔截時。
      2. 事件被消耗時。
      3. 遍歷完整個 view tree 時。

    事件攔截:即 onInterceptTouchEvent() 返回值是 true。只有ViewGroup才能攔截事件。
    事件處理結果:即 dispatchTouchEvent() 返回值,true 表示消耗,false表示不消耗。
    事件消耗:即 dispatchTouchEvent() 返回值是 true。

  2. 事件處理
    事件可以在 Activity 或 ViewGroup 或 View 中被處理,而且可被多次處理直到被消耗才停止處理。

    Activity:對事件的處理是通過呼叫自身的 onTouchEvent()。
    View:View 接收到事件後會呼叫 dispatchTouchEvent() 直接對事件進行處理。在該方法裡會觸發onTouch() 或 onTouchEvent() 對事件進行處理。
    ViewGroup:若沒有重寫dispatchTouchEvent(),都是通過呼叫基類 View 的 dispatchTouchEvent() 並將其返回值作為該 ViewGroup 的 dispatchTouchEvent() 的返回值。

    • ViewGroup 何時對事件進行處理?
      1、攔截事件或沒有子View 時。
      2、下層的所有View 都未消耗事件時。

    • View 何時對事件進行處理?
      View 接受到事件後,(不會進行分發,因為沒有子View)會直接處理事件,並將事件處理結果返回給它的ViewGroup。

注:事件處理不等於事件消耗。

事件分發


事件在view tree中分發的流程(圖解)

“分發樹”:當觸碰事件產生時,ACTION_DOWN 產生時所觸控到的所有 View 按照父子關係可以組成一個 view tree 即“分發樹”,“分發樹”的根節點是頂級View,“分發樹”是整個Window 中完整的 view tree 的一部分,ACTION_DOWN 就是在這個“分發樹”中分發的。

事件在view tree中分發的流程:

  1. ACTION_DOWN 事件
    ACTION_DOWN 在具體應用程式app區域下分發的幾種常見情形:
    view tree的深度遍歷

    不瞭解Android 手機介面組成參考:Android手機介面組成
    總之,ACTION_DOWN 會深度遍歷“分發樹”並確定“消耗樹”。注:這裡的“消耗樹”指的就是上圖中的“消耗路徑”。

  2. 後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)
    後續同一序列事件都是沿著這一“消耗樹”分發(深度遍歷,但通常都是線性結構)的,且可被中途攔截但“消耗樹”不變。

若ACTION_DOWN找不到“消耗路徑”(即不被任何View消耗)也不被Activity消耗,那麼後續事件就會消失,不會被處理。

“消耗路徑”:從頂級View到消耗事件的View的最短路徑就是“消耗路徑”(線性結構的),該“消耗路徑”會與已有的“消耗樹”合併(若存在“消耗樹”的話。通常情況下是沒有的,因為ACTION_DOWN時會清空)。
“消耗樹”:一條或多條“消耗路徑”合併形成的。“消耗路徑”也是“消耗樹”的一種,只不過它只有一條“消耗路徑”。
同一個事件序列:指從手指接觸螢幕(觸發ACTION_DOWN事件)到手指離開螢幕(觸發ACTION_UP)以及期間產生的一系列事件。
注意:同一事件序列只有一條“消耗路徑”,因此,覺得混亂的話可以直接把“消耗樹”當做“消耗路徑”。

下面是將“分發樹”簡化為線性結構後事件分發的幾種情形:

紅色:代表ACTION_DOWN事件的分發路徑,不是“消耗路徑”。
藍色:代表ACTION_MOVE 和 ACTION_UP 事件分發路徑,也是“消耗路徑”。

1、我們重寫ViewGroup1 的dispatchTouchEvent 方法,直接返回true消費這次事件
ACTION_DOWN 事件從(Activity的dispatchTouchEvent)——–> (ViewGroup1 的dispatchTouchEvent) 後結束傳遞,事件被消費(如下圖紅色的箭頭程式碼ACTION_DOWN 事件的流向)。
1
2、我們在View 的onTouchEvent 返回true消費這次事件
4
3、我們在ViewGroup 2 的onTouchEvent 返回true消費這次事件
5
4、我們在Activity 的onTouchEvent 返回true消費這次事件
7
5、我們在View的dispatchTouchEvent 返回false(即重寫並直接返回false)並且Activity 的onTouchEvent 返回true消費這次事件
8
上面例子來源:圖解 Android 事件分發機制

注:下面的原始碼都來自Android-23版本。

事件在各結點中分發的流程(圖+原始碼)

Activity

  • 流程圖
    Activity的 dispatchTouchEvent() 的流程:
    Activity的dispatchTouchEvent方法的流程

  • 原始碼

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

Activity對事件的分發是通過Window來分發的,並不攔截。
注意:Window只負責幫Activity分發事件,不處理,也就是沒有onTouchEvent方法,可以把它的分發功能當成Activity的一部分,然後忽略它。

ViewGroup

  • View和ViewGroup中的方法:
    View:dispatchTouchEvent();onTouchEvent()
    ViewGroup:重寫dispatchTouchEvent();onInterceptTouchEvent();
    注意:ViewGroup及其子類並未重寫onTouchEvent()。

  • 流程圖
    ViewGroup的dispatchTouchEvent方法的流程:
    ViewGroup的dispatchTouchEvent方法的流程

TouchTarget連結串列:每個ViewGroup都持有一個mFirstTouchTarget變數,該變數指向一個TouchTarget連結串列(不帶頭結點)的首結點,該連結串列的結點用於存放當前Viewgroup的子結點,該子結點必須是“消耗樹”上的結點。TouchTarget連結串列會在ACTION_DOWN被清空即“消耗樹”消亡。總之,ViewGroup 的 TouchTarget連結串列存放該 ViewGroup 在“消耗樹”上的所有子結點,mFirstTouchTarget指向它的首結點。

  • 事件在 ViewGroup 中分發的流程(即 ViewGroup 的 dispatchTouchEvent() 的流程):

    若為 ACTION_DOWN 事件:

    1. 是否攔截
      當前 ViewGroup 結點若攔截事件則結束分發,否則會將事件分發給它的子View。

    2. 只分發給“分發樹”上的子結點
      遍歷 Window 中完整的 view tree,若當前結點不在觸碰區域內則進入下一結點,不對其分發事件。其實就是隻在“分發樹”上分發。
      注:在事件是 pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等)時會形成一個“分發樹”。

    3. 更新 TouchTarget 列表
      在遍歷它在“分發樹”上的子結點時,若遍歷到的子結點在當前 ViewGroup 的 TouchTarget 連結串列中則將事件分發給該子結點(即呼叫該子結點的dispatchTouchEvent())並結束對當前 ViewGroup 的子結點的遍歷,否則,遍歷每個子結點,呼叫每個子結點的 dispatchTouchEvent()直到被消耗即返回 true 才結束遍歷並將該子結點插入到 TouchTarget 連結串列的頭部(其實就是合併到已有的“消耗樹”上,如果有“消耗樹”的話)。總之,遍歷子結點時,若子結點在已有的“消耗樹”上則分發給它並結束更新,否則呼叫子結點的 dispatchTouchEvent(),若返回true則將該子結點插入連結串列頭部結束更新。

    4. 分發。
      遍歷 TouchTarget列表,呼叫每個元素(即該 ViewGroup 在“分發樹”上的每個子View)的 dispatchTouchEvent() 。

    若是後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP):

    1. 是否攔截
      “消耗樹”上的 ViewGroup 結點若攔截事件則結束分發,否則會將事件分發給它的子View。

    2. 遍歷TouchTarget列表,呼叫每個元素(即該 ViewGroup 在“消耗樹”上的每個子View)的 dispatchTouchEvent() 。

    是否攔截:當 ViewGroup 的 disallowIntercept 為 false 也就是允許攔截時,若事件為ACTION_DOWN 或mFirstTouchTarget != null 必定觸發當前ViewGroup的onInterceptTouchEvent()。滑動衝突的處理就是在 ACTION_MOVE 事件在“消耗樹”上分發時在中途將它攔截,參考:Android 滑動衝突的處理

  • 原始碼

public boolean dispatchTouchEvent(MotionEvent ev) {   
        //關鍵步驟:
        //1、是否攔截事件。
        //2、更新TouchTarget 列表。
        //3、分發。

        ......
        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            ......

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {             
                //每次事件序列開始(即發生ACTION_DOWN事件時)都會
                //重置mFirstTouchTarget、mGroupFlags的值為初始值(分別為null 和 允許攔截)。
                resetTouchState();
            }

            //1、是否攔截事件
            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || mFirstTouchTarget != null) {
            //mFirstTouchTarget指向一個TouchTarget列表的首結點。
            //TouchTarget列表是Linked List,若事件分發給某個子View後可被“下層”消耗,
            //則新增該子View到列表頭部(採用的是頭插法)。
            //“下層”包括當前ViewGroup裡面的所有在當前事件觸控範圍內的View,不僅僅指它的子View。
            //簡單的說就是當前事件之前的事件被下層消耗則不為null。
                final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
                //disallowIntercept是否不被允許攔截事件。
                //可通過子元素呼叫requestDisallowInterceptTouchEvent(boolean b)來
                //設定mGroupFlags的值,從而改變disallowIntercept。
                //但是這一條件判斷對ACTION_DOWN無效(總為true),因為會重置mGroupFlags(見上面程式碼)。
                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;
            }


            //下面的程式碼都是講事件是如何往下層分發的,分兩步:2、3

            //2、更新TouchTarget 列表,事件為pointer down時才需要。
            //pointer down包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等,參考下面判斷條件。
            //注意:新增的元素必須是事件範圍內也就是觸控到的區域內的子View,同時,沿著這個子View往下分發能消耗事件,
            //或者說,呼叫該子View的dispatchTouchEvent方法返回結果是true。
            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //非canceld && 不攔截 時才向下層分發事件              
            if (!canceled && !intercepted) {
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                //注意:以下幾種情況才需要更新TouchTarget 列表
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {     

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null && childrenCount != 0) {
                        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.
                            //若該View不被觸碰到或不能相應事件則剔除,其實就是隻允許對“分發樹”遍歷
                            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) {//子元素已經在TouchTarget連結串列中,結束遍歷。
                                // 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;
                            }

                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            //dispatchTransformedTouchEvent()會呼叫子元素的dispatchTouchEvent()。
                                // Child wants to receive touch within its bounds.
                                ......

                                //採用頭插法將子元素新增到TouchTarget列表中
                                newTouchTarget = addTouchTarget(child, idBitsToAssign);
                                //已找到消耗此事件的路徑且已分發給下層的目標View
                                alreadyDispatchedToNewTouchTarget = true;
                                break;
                            }
                        }
                    }

                    //並未找到消耗事件的路徑 && mFirstTouchTarget 不為空時,將TouchTarget連結串列中
                    //最近最少被新增的target賦給它,即將連結串列的最後一個結點的引用賦給newTouchTarget。
                    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;
                    }
                }
            }

            //3、分發:往下層分發事件。
            // Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //當做普通View對待,而不是ViewGroup。
                //會呼叫super.dispatchTouchEvent()方法,最終呼叫自身的onTouchEvent方法.
                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) {
                    //在上面更新TouchTaget列表時已分發完畢。
                        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;
    }

關鍵步驟:
1、是否攔截事件。

  • 當ViewGroup的disallowIntercept為false也就是允許攔截時,若事件為ACTION_DOWN 或mFirstTouchTarget != null 一定會觸發當前ViewGroup的onInterceptTouchEvent()。
  • 子元素能夠通過呼叫requestDisallowIntercept(boolean b)來控制父容器能否呼叫onInterceptTouchEvent()。

2、更新TouchTarget 列表。

  • 本質是尋找該事件的“消耗路徑”的下一個結點。若TouchTarget列表中有元素處於當前事件的觸控區域內,則結束更新,否則會遍歷呼叫觸控區域內的子View的dispatchTouchEvent方法,直到返回true並新增到列表。。
  • 事件屬於pointer down(包括ACTION_DOWN、ACTION_POINTER_DOWN、ACTION_HOVER_MOVE等),則要更新列表。
  • 更新列表規則:新增的元素必須是事件範圍內也就是觸控到的區域內的子View,同時,沿著這個子View往下分發能消耗事件,或者說,呼叫該子View的dispatchTouchEvent方法返回結果是true。
  • 更新列表的步驟:遍歷判斷 1、子View是在觸控區域內,不是則continue,進入下一個迴圈。2、子View是否已在列表中,是則break,結束遍歷。3、呼叫子View的dispatchTouchEvent方法,返回結果是true則用頭插法加入列表(在這一步中已進行分發,後面的“分發”程式碼不會再分發一次)。

3、 分發。

  • 遍歷TouchTarget列表,呼叫每個元素的dispatchTouchEvent方法。

總結:ACTION_DOWN若能被消耗則會確定“消耗樹”,後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿著這一“消耗樹”分發的,且可被中途攔截。

ViewGroup的分發過程可用如下虛擬碼表示:

dispatchTouchEvent(MotionEvent ev){
    boolean consume = false;//事件是否被消耗,若被消耗則該事件的分發結束

    if (onInterceptTouchEvent(ev)) {//攔截事件
        consume = super.dispatchTouchEvent(ev);//即View.dispatchTouchEvent(ev)
    }else {
        //遍歷子元素,將事件分發給子元素,直到事件被消耗。
        //其實,實際程式碼只需遍歷TouchTarget列表中的元素,不需要遍歷所有子View。
        View child = null;
        for (int index = 0; index < childNum; index++) {
            child = getChild(index);
            if (null != child) consume = child.dispatchTouchEvent(ev);
            if (consume) break;
        }

        //遍歷結束但事件沒有被消耗,對事件進行處理。
        if (!consume) consume = super.dispatchTouchEvent(ev);
    }

    return consume;
}

View

View 的 dispatchTouchEvent() 直接處理事件,不分發。

事件處理


Activity

Activity 對事件的處理都是通過呼叫自身的 onTouchEvent() 。
原始碼:

public boolean onTouchEvent(MotionEvent event) {
    if (mWindow.shouldCloseOnTouch(this, event)) {
        finish();
        return true;
    }

    return false;
}

View

無論View 還是ViewGroup,若沒有重寫dispatchTouchEvent(),對事件的處理都是通過呼叫基類View的 dispatchTouchEvent(),最終呼叫 onTouch() 或 onTouchEvent() 。

基類 View 的 dispatchTouchEvent() 流程:
若View設定了 OnTouchListener 且 onTouch() 返回 true 則dispatchTouchEvent() 返回 true,不會呼叫onTouchEvent()。否則,會呼叫 onTouchEvent() 且 onTouchEvent() 的返回值就是dispatchTouchEvent() 的返回值。

在View的onTouchEvent()中若View是clickable或longclickable的則會呼叫onClick()(若有設定OnClickListener)。clickable或longclickable或contextclickable時預設返回true,否則返回false。

  • 流程圖
    View的dispatchTouchEvent方法的流程:
    View的dispatchTouchEvent方法的流程

  • 原始碼

public boolean dispatchTouchEvent(MotionEvent event) {
        ......

        boolean result = false;//是否消耗事件
        if (onFilterTouchEventForSecurity(event)) {
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //若設定了OnTouchListener,則先呼叫onTouch()。
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            //若onTouch()沒有消耗事件則呼叫onTouchEvent()
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }

        ......
        return result;
    }

若View是enabled的,且設定了OnTouchListener,則先呼叫onTouch(),若onTouch()返回true則分發結束,否則,接著呼叫onTouchEvent()。

ViewGroup

ViewGroup對事件處理是通過呼叫基類View的dispatchTouchEvent() ,最終呼叫 onTouch() 或 onTouchEvent() 。
ViewGroup的onTouchEvent()繼承自View,本身不重寫。ViewGroup的實現類也不重寫該方法。

總結


事件分發機制概要流程:
Android事件分發機制概要流程
上面圖片來源:Android事件分發機制 詳解攻略,您值得擁有

事件分發機制詳細流程:
Android事件分發機制詳細流程

總之,ACTION_DOWN 會深度遍歷“分發樹”並確定“消耗樹”,後續同一事件序列的事件(ACTION_MOVE 或 ACTION_UP)都是沿著這一“消耗樹”分發(深度遍歷,但通常都是線性結構)的,且可被中途攔截但“消耗樹”不變。

相關文章