Android事件分發實際上是老生常談了,可是說實話,我以爲不少人都只是懂其大概,模棱兩可。本文的目的就是再次從源碼層次梳理一下,重點放在ViewGroup的dispatchTouchEvent方法上,這個方法是事件分發的核心中的核心!咱們藉此以小見大,理解事件分發的機制。ps,本文着重在源碼和分析,就不怎麼畫圖了(實際上是懶),你們能夠看網上相關圖片,隨便一搜不少。java
不少人講事件分發,都說其開始是從Activity的dispatchTouchEvent開始的,你們能夠簡單這麼理解,可是確定會有人疑問,Activity的這個方法從哪兒調用的呢?我寫了一個簡單的Demo,而後在Activity的dispatchTouchEvent方法里加了一個斷點獲得其函數調用棧,看下圖:api
好傢伙,原來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,因此咱們來分析它:佈局
你們應該或多或少讀過其源碼,源碼雖然不是太長,但乍一看仍是會頭大的,我想大多數人可能大概看懂了其邏輯,對於裏面不少東西不明因此。好比mFirstTouchTarget是幹嗎的?臨時變量alreadyDispatchedToNewTouchTarget是幹嗎的?裏面好像有鏈表啊,幹嗎使的?動畫
這裏稍微補充一句,對於事件分發來講,從用戶按下到擡起,這是一組事件,以ACTION_DOWN爲開頭,UP或CANCEL結束。咱們後面分析的也是這一組事件。spa
源碼較長,我寫了僞代碼給你們看看,說是僞代碼,其實仍是比較全面詳細的,省略了部分函數參數,但重點的代碼都包含了,重點看註釋。若是嫌長,能夠直接先看後面的結論,再回頭看僞代碼。code
//本源碼來自 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事件分發分爲三步orm
第一步:判斷要不要攔截:這裏的條件分支要看清,外層的判斷語句意思是,要麼確定會攔截,要麼可能不攔截,可能不攔截的話須要知足如下兩個條件之一:blog
事件是DOWN事件。遞歸
非DOWN事件也能夠,可是須要知足mFirstTouchTarget != null 。這個條件意味着什麼呢?意味着在以前的DOWN事件中,至少有一個子View捕獲(消費)了DOWN事件,也就是意味着對於這一組分發事件來講,有子View願意處理這個事件。
在可能攔截的狀況下,咱們進入攔截判斷流程,很簡單: 先看子view有沒有調parent.requestDisallowIntercept,若是調用了,不攔截,沒有的話走到onIntercepteTouchEvent方法,根據其返回值決定是否攔截。
第二步:若是沒有攔截,分發DOWN事件:遍歷全部子View,查看觸摸區域是否有子view有資格消費這個事件,判斷依據有二:子View是否有動畫?以及觸摸點是否落在子View的範圍內。若是前二者都知足,則將DOWN事件分發給子View,這一步引出了一個重要的方法:dispatchTransformedTouchEvent
,這個方法乾的活就是最重要的事情:分發給子view,也就是說,這個方法進行了遞歸的調用,感興趣的同窗能夠本身閱讀其源碼。另外,這個分發方法有個返回值,若是爲true,則爲mFirstTouchTarget賦值,不然其值仍爲null。這一步最後有個方法,addTouchTarget,這個方法牽扯到了鏈表的構建,鏈表保存的什麼呢?其實對於任何一個事件的位置座標,屏幕上可能有多個View都包含了該座標,分發事件的時候必然要讓全部這些View都分發一遍,這些被分發的View就被保存到一個鏈表當中,方便後面的遍歷。
第三步:分發其餘事件:首先判斷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的很是簡單
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方法。根據以上分析:
按鈕內:
alreadyDispatchedToNewTouchTarget
這個變量爲true,因此直接返回true。這樣,是否是就解釋了兩層View都添加click事件時的響應結果了~
用的來講,事件分發分兩步,攔截和分發,其中分發有兩種狀況,Down事件和非 Down事件,down事件是事件鏈的起點,決定了要不要消費事件,會影響後續的全部非down事件的分發,若是down事件不消費,會使得mFirstTouchTarget爲null,後面的全部事件就再也不分發給子view了,直接由本view group處理。 愈來愈感到讀源碼的重要性,Let's read the fucking sourceCode!