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


引子

Android事件分發其實是老生常談了,但是說實話,我覺得很多人都只是懂其大概,模棱兩可。不信我可以先拋出幾個問題:

  • ACTION_DOWN和其他觸摸事件的處理方式一樣嗎?如果不,有什么不同之處?
  • 如果手指落下時落在某個View范圍內,該View會收到Down事件,當手指未抬起但移動到別的位置時,該View還會收到MOVE,UP事件嗎?
  • 如果一個ViewGroup的所有子View都沒消費DOWN事件,這些子View在手指抬起前還會收到其他事件嗎?
  • 子View一定可以通過requestDisallowIntercept干預父布局分發嗎?
  • 如果有多個View都包含觸摸坐標,它們都能接收到事件分發嗎?如果不是,誰會接受?原理?
  • 多指操作怎么處理的?

應該會有不少讀者無法答對上面的所有問題吧,多數文章都在講事件分發的遞歸調用鏈,整體概念確實重要,但還不夠深入,比如沒有把ACTION_DOWN事件單獨拿出來講。如果真想徹底弄懂分發原理,必須得把源碼的思路理清楚,然后才能對證下葯。本文的目的就是從源碼層次梳理一下,重點放在ViewGroup的dispatchTouchEvent方法上,這個方法是事件分發的核心中的核心!我們借此以小見大,理解事件分發的機制。ps,本文着重在源碼和分析,就不怎么畫圖了(其實是懶),大家可以看網上相關圖片,隨便一搜很多。本文力求深入淺出,我來深入源碼,然后盡量用淺顯的語言講出來。

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

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

stack.png

好家伙,原來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;  
     // 注意這個條件,后者代表着有子view消費事件。后面會講
   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;
                    break;
                }

            }
        }
    }

        //第三步:分發非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,也就是說,這個方法進行了遞歸的調用,感興趣的同學可以自己閱讀其源碼。遍歷的范圍是什么呢?源碼中告訴我們是按照z軸順序排列好的一個view list,這里的z軸順序保證了我們先把事件分發給z軸坐標大的值,也就是更靠外層的view。另外,這個分發方法有個返回值,如果為true,則為mFirstTouchTarget賦值,否則其值仍為null。最后有個方法,addTouchTarget,這個方法一方面為mFirstTouchTarget賦值,另外也構建了一個鏈表,鏈表保存的什么呢?其實這個跟多指操作有關,它保存了所有“mFirstTouchTarget”,mFirstTouchTarget是啥呢?其實字面意思很明白了,就是手指碰觸的位置最外層的View,對於多個手指來說,每個手指都會有一個mFirstTouchTarget,於是就保存到了這個鏈表中了。為什么要保存mFirstTouchTarget呢?很簡單,為了讓后續的該事件組的其他事件知道誰要處理事件啊!要不然總不能每個事件都要判斷一下,那效率就低多了。這里就得出來一個結論,Down事件的分發決定了那個view要捕獲事件,如果捕獲了,后續的事件就直接分發給它,也就是說move up等事件的分發交給誰,取決於它們的起始事件Down由誰捕獲

  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 {
                    分發給之前保存的mFirstTouchTarget對應的子View;
                }
            }
        }

    }

到這里,我們就把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事件是事件鏈的起點,決定了要不要消費事件,而且將消費的子View保存下來給后面使用。如果所有的子View都不消費down事件或者壓根沒有子View,會使得mFirstTouchTarget為null,后面的所有事件就不再分發給子view了,直接由本view group處理。當然這里的交給本人處理,實際上可能它也不消費,會繼續往上傳,最終“歸”到Activity處理。
越來越感到讀源碼的重要性,Let's read the fucking sourceCode!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM