1. 程式人生 > >你真的看懂Android事件分發了嗎?

你真的看懂Android事件分發了嗎?

引子

Android事件分發其實是老生常談了,但是說實話,我覺得很多人都只是懂其大概,模稜兩可。本文的目的就是再次從原始碼層次梳理一下,重點放在ViewGroup的dispatchTouchEvent方法上,這個方法是事件分發的核心中的核心!我們藉此以小見大,理解事件分發的機制。ps,本文著重在原始碼和分析,就不怎麼畫圖了(其實是懶),大家可以看網上相關圖片,隨便一搜很多。

先簡單講一下事件分發的源頭

很多人講事件分發,都說其開始是從Activity的dispatchTouchEvent開始的,大家可以簡單這麼理解,但是肯定會有人疑問,Activity的這個方法從哪兒呼叫的呢?我寫了一個簡單的Demo,然後在Activity的dispatchTouchEvent方法里加了一個斷點得到其函式呼叫棧,看下圖:

好傢伙,原來Activity分發之前還有這麼多過程,簡單梳理了一下:大概是從InputEventReceiver開始,經過ViewRootImpl,裡面各種InputStage呼叫之後,最後給了DecorView,然後DecorView傳給的Activity。其實這裡挺有意思的,本來DecorView先獲取到事件的,但是後來它又分配給了Activity,Activity之後又通過phoneWindow把事件傳回給了DecorView,一來一回,就是為了讓Activity去處理一下事件而已。Activity傳給DecorView之後,DecorView會呼叫superDispatchTouchEvent

方法:

    public boolean superDispatchTouchEvent(MotionEvent event){
        return super.dispatchTouchEvent(event);
    }

因為DecorView是一個FrameLayout,它最終還是呼叫了我們熟悉的ViewGroup的dispatchTouchEvent(),這也是本文的主角。所謂的事件分發,本質上就是一個遞迴函式的呼叫,這個遞迴函式就是dispatchTouchEvent,至於onIntercepterTouchEvent,onTouchEvent,OnTouchListener,onClickListener...balabala都是在這個遞迴函式裡面的操作而已,最核心,最骨幹的還是dispatchTouchEvent,所以我們來分析它:

ViewGroup的事件分發

大家應該或多或少讀過其原始碼,原始碼雖然不是太長,但乍一看還是會頭大的,我想大多數人可能大概看懂了其邏輯,對於裡面很多東西不明所以。比如mFirstTouchTarget是幹嘛的?臨時變數alreadyDispatchedToNewTouchTarget是幹嘛的?裡面好像有連結串列啊,幹嘛使的?

這裡稍微補充一句,對於事件分發來說,從使用者按下到抬起,這是一組事件,以ACTION_DOWN為開頭,UP或CANCEL結束。我們後面分析的也是這一組事件。

原始碼較長,我寫了虛擬碼給大家看看,說是虛擬碼,其實還是比較全面詳細的,省略了部分函式引數,但重點的程式碼都包含了,重點看註釋。如果嫌長,可以直接先看後面的結論,再回頭看虛擬碼。

//本原始碼來自 api 28,不同版本略有不同。
public boolean dispatchTouchEvent(MotionEvent ev) {
    // 第一步:處理攔截
   boolean intercepted;  
     // 注意這個條件,後面會講
   if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
    // 子view呼叫了parent.requestDisallowInterceptTouchEvent干預父佈局的攔截,不讓它爸攔截它
       final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
       if (!disallowIntercept) {
             intercepted = onInterceptTouchEvent(ev);
             ev.setAction(action); 
         } else {
             intercepted = false;
         }
     } else {
        //既不是DOWN事件,mFirstTouchTarget還是null,這種情況挺常見:如果ViewGroup的所有的子View都不消費                //事件,那麼當ACTION_MOVE等非DOWN事件到來時,都被攔截了。
         intercepted = true;
     }

    // 第二步,分發ACTION_DOWN
    boolean handled = false;
    boolean alreadyDispatchedToNewTouchTarget = false; //注意這個變數,會用到
   // 不攔截才會分發它,如果攔截了,就不分發ACTION_DOWN了
    if (!intercepted) {
        //處理DOWN事件,捕獲第一個被觸控的mFirstTouchTarget,mFirstTouchTarget很重要,
        儲存了消費了ACTION_DOWN事件的子view
        if (ev.getAction == MotionEvent.ACTION_DOWN) {
            //遍歷所有子view(看原始碼知子View是按照Z軸排好序的)
            for (int i = childrenCount - 1; i >= 0; i--) {
                //子view如果:1.不包含事件座標 2. 在動畫  則跳過
                if (!isTransformedTouchPointInView() || !canViewReceivePointerEvents()) {
                    continue;
                }
                //將事件傳遞給子view的座標空間,並且判斷該子view是否消費這個觸控事件(分發Down事件)
                if (dispatchTransformedTouchEvent()) {
                    //將該view加入頭節點,並且賦值給mFirstTouchTarget
                    newTouchTarget = addTouchTarget(child, idBitsToAssign);
                    alreadyDispatchedToNewTouchTarget = true;
                }

            }
        }
    }

        //第三步:分發非DOWN事件
        //如果沒有子view捕獲ACTION_DOWN,則交給本ViewGroup處理這個事件。我們看到,這裡並沒有判斷是否攔截,
        //為什麼呢?因為如果攔截的話,上面的程式碼不會執行,就會導致mFirstTouchTarget== null,於是就走下面第一                         //個條件裡的邏輯了
        if (mFirstTouchTarget == null) {
            super.dispatchTouchEvent(ev); //呼叫View的dispatchTouchEvent,也就是自己處理
        } else {
            //遍歷touchTargets連結串列,依次分發事件
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                if (alreadyDispatchedToNewTouchTarget) {
                  handled = true
                } else {
                    if (dispatchTransformedTouchEvent()) {
                      handled = true;
                    }
                  target = target.next;
                }
            }
        }

        //處理ACTION_UP和CANCEL,手指抬起來以後相關變數重置
        if (ev.getAction == MotionEvent.ACTION_UP) {
            reset();
        }
    }
    return handled;
}

總結一下:ViewGroup事件分發分為三步

  1. 第一步:判斷要不要攔截:這裡的條件分支要看清,外層的判斷語句意思是,要麼肯定會攔截,要麼可能不攔截,可能不攔截的話需要滿足以下兩個條件之一:

    1. 事件是DOWN事件。

    2. 非DOWN事件也可以,但是需要滿足mFirstTouchTarget != null 。這個條件意味著什麼呢?意味著在之前的DOWN事件中,至少有一個子View捕獲(消費)了DOWN事件,也就是意味著對於這一組分發事件來說,有子View願意處理這個事件。

    在可能攔截的情況下,我們進入攔截判斷流程,很簡單: 先看子view有沒有調parent.requestDisallowIntercept,如果呼叫了,不攔截,沒有的話走到onIntercepteTouchEvent方法,根據其返回值決定是否攔截。

  2. 第二步:如果沒有攔截,分發DOWN事件:遍歷所有子View,檢視觸控區域是否有子view有資格消費這個事件,判斷依據有二:子View是否有動畫?以及觸控點是否落在子View的範圍內。如果前兩者都滿足,則將DOWN事件分發給子View,這一步引出了一個重要的方法:dispatchTransformedTouchEvent ,這個方法乾的活就是最重要的事情:分發給子view,也就是說,這個方法進行了遞迴的呼叫,感興趣的同學可以自己閱讀其原始碼。另外,這個分發方法有個返回值,如果為true,則為mFirstTouchTarget賦值,否則其值仍為null。這一步最後有個方法,addTouchTarget,這個方法牽扯到了連結串列的構建,連結串列儲存的什麼呢?其實對於任何一個事件的位置座標,螢幕上可能有多個View都包含了該座標,分發事件的時候必然要讓所有這些View都分發一遍,這些被分發的View就被儲存到一個連結串列當中,方便後面的遍歷。

  3. 第三步:分發其他事件:首先判斷mFirstTouchTarget,如果為null,說明前一步的DOWN事件沒有子view消費掉,這種情況表示該ViewGroup的孩子View都不打算處理事件,這種情況自然要交給ViewGroup自身處理,程式碼裡交給了super.dispatchTouchEvent,也就是呼叫了ViewGroup的父類View處理(onTouchEvent)。如果不為null,說明有子View要處理事件,進入else語句裡,把事件分發下去。 這裡眼尖的讀者應該看到了,第二步不會已經分發了DOWN事件了嗎,這裡為啥還要再分發一次呢?不重複了嗎,這裡就到了前面講的另外一個變量出場了,alreadyDispatchedToNewTouchTarget,這個變數在虛擬碼裡第二步的開頭提到了,當第二步裡有子View消費了事件後,該變數會變成true,此時第三步會判斷該值,如果為true,就直接返回handle=true,不再分發事件了。這就避免了DOWN事件被兩次分發。對於其他事件,這個變數肯定是false,所以一定會走else的邏輯,進行分發。

在濃縮一下,加點大白話:

  public boolean dispatchTouchEvent(MotionEvent event) {

        boolean intercepted = false;
        if (DOWN 或者 DOWN的時候沒有孩子想處理) {
            if (孩子不讓攔截?) {
                intercepted = false;
            } else {
                intercepted = onIntercept();
            }
        } else {
          intercepted = true;
        }

        if (DOWN && !intercepted) {
            for (遍歷孩子View) {
                if(如果該孩子能消費就給分發給它,如果它真消費了DOWN事件){
                    給mFirstTouchTarget賦值 ;
                    Down事件已經分發了;
                }
            }
        }

        if (mFirstTouchTarget == null) {
            孩子都不想消費,交給我自己處理吧;
        } else {
            while(遍歷所有孩子,將事件分發下去) {
                if (DOWN事件已經分發了) {
                    return true;
                }else {
                    分發給子View,如果有人消費,返回true;
                }
            }
        }

    }

到這裡,我們就把ViewGroup的事件分發講完了,接下來分析一下View的dispatchTouchEvent

View的事件分發

View的非常簡單

public boolean dispatchTouchEvent(MotionEvent event) {
    boolean result = false;
    if (onTouchListener.onTouch()) {
        result = true;
    }
    if (!result && onTouchEvent()) {
        result = true;
    }
    return result;
}

可見,先判斷listener,如果listener返回true了,onTouchEvent就不進入了,否則,走onTouchEvent方法。

View的複雜點的地方在onTouchEvent方法的預設實現裡,裡面處理了很多onClick,onLongclick事件的邏輯,感興趣的同學可以自行閱讀原始碼,這裡只說一點,一旦設定了onClickListener或者onLongclickListener,那麼onTouchEvent就會返回true,也就是消費,其他情況下預設不消費,原始碼裡這麼寫的

        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

clickable為true,則返回true,否則返回false。

舉個例子練習一下

問題很簡單,一個FrameLayout中間放了一個按鈕,Framelayout和按鈕都添加了點選事件,那麼,請問點選按鈕和點選按鈕之外的區域事件分發過程是怎樣的?

先看按鈕之外: FrameLayout是一個ViewGroup,而且沒有重寫dispatchTouchEvent方法。根據以上分析:

  • 第一步,down來了以後,進入攔截邏輯,framelayout不攔截,所以intercepted == false
  • 第二步,處理down事件,發現觸控點沒有子view,所以不會有人處理這個事件的,mFirstTouchTarget == null
  • 第三步,交給自身處理,自身會呼叫onTouchEvent,在這裡由於設定了clickListener,返回true,消費了事件。
  • 後續move和up,由於mFirstTouchTarget == null,第一步會攔截,所以直接交給自身處理,同上面的第三步,同時,up的時候會響應click事件。

按鈕內:

  • 第一步,同上
  • 第二步, 發現觸控點有子view,mFirstTouchTarget != null,且將DOWN事件分發給了子View。
  • 第三步,mFirstTouchTarget非null,但alreadyDispatchedToNewTouchTarget這個變數為true,所以直接返回true。
  • 後續move和up,第一步不會攔截,因為不是down事件所以第二步跳過,第三步將事件分發給了子View,子View響應了點選事件,返回true,而這個過程中,ViewGroup沒有消費任何事件,所以自然不會響應onClick事件。

這樣,是不是就解釋了兩層View都新增click事件時的響應結果了~

總結

用的來說,事件分發分兩步,攔截和分發,其中分發有兩種情況,Down事件和非
Down事件,down事件是事件鏈的起點,決定了要不要消費事件,會影響後續的所有非down事件的分發,如果down事件不消費,會使得mFirstTouchTarget為null,後面的所有事件就不再分發給子view了,直接由本view group處理。
越來越感到讀原始碼的重要性,Let's read the fucking sourceCo