10分鐘理解 Android View 事件分發機制

ITKobe24發表於2016-08-05

Android開發,觸控無處不在。對於一些 不咋看原始碼的同學來說,多少對這塊都會有一些疑惑。View事件的分發機制,不僅在做業務需求中會碰到這些問題,在一些面試筆試題中也常有人問,可謂是老生常談了。我以前也看過很多人寫的這方面的文章,不是說的太囉嗦就是太模糊,還有一些在細節上寫的也有爭議,故再次重新整理一下這塊內容,十分鐘讓你搞明白View事件的分發機制。

說白了這些觸控的事件分發機制就是弄清楚三個方法,dispatchTouchEvent(),OnInterceptTouchEvent(),onTouchEvent(),和這三個方法與n個ViewGroup和View堆疊在一起的問題,再複雜的結構都能拆分成1個ViewGroup+1個View。

其實ViewGroup和View都是大同小異,View只是沒有了子容器,自然不存在攔截問題,dispatch也很簡單,所以弄明白了ViewGroup其實就懂的差不多了。

三個關鍵方法

public boolean dispatchTouchEvent(MotionEvent ev)

View/ViewGroup處理事件分發的發起者,View/ViewGroup接收到觸控事件最先調起的就是這個方法,然後在該方法中判斷是否處理攔截或是將事件分發給子容器

public boolean onInterceptTouchEvent(MotionEvent ev)

ViewGroup專用,通過該方法可以達到控制元件事件的分發方向,一般可以在該方法中判斷將事件給ViewGroup獨吞或是它繼續傳遞給子容器,是處理事件衝突的最佳地點

public boolean onTouchEvent(MotionEvent event)

觸控事件的真正處理者,最後每個事件都會在這裡被處理

核心問題

時間分發機制的難點在哪,我覺得難的地方以下幾點:三個方法呼叫規則,確定處理事件的物件以及事件衝突的解決方法。

事件傳遞規則

一般一次點選會有一系列的MotionEvent,可以簡單分為:down->move->….->move->up,當一次event分發到ViewGroup時,上述三個方法之間的 ViewGroup中呼叫順序可以用一段簡單程式碼表示

MotionEvent ev;//down or move or up or others...
viewgroup.dispatchTouchEvent(ev);

public boolean dispatchTouchEvent(MotionEvent ev){
 boolean isConsumed = false;
   if(onInterceptTouchEvent(ev)){
     isCousumed = this.onTouchEvent(ev);
   }else{
      isConsumed = childView.dispatchTouchEvent(ev);
   }
   return isConsumed;
}

返回結果true表示事件被處理了,返回false表示沒有處理。上面的程式碼通俗易懂,看起來也很簡單,一句話就能概括,ViewGroup收到事件後呼叫dispatch,在dispatch中先檢查是否要攔截,若攔截則ViewGroup吃掉事件,否則交給有處理能力的子容器處理。

不過,簡單歸簡單,寫成這樣只是為了方便理解,ViewGroup的事件處理流程當然沒這麼簡單,這裡忽略了很多細節問題,接下來繼續補充。回到上面說的,一系列事件我們經常處理的一般都是一個down,多個move和一個up,光靠上面的虛擬碼是沒辦法把這些問題都給完美解決,直接來看ViewGroup的dispatchTouchEvent。

onInterceptTouchEvent呼叫條件

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

解釋一下上面的程式碼,看起來好像很簡單,但真的很簡單嗎。。在解釋之前先說一下intercepted代表的含義,intercepted == false表示父容器ViewGroup暫時不攔截事件,事件有機會傳給子View處理,返回true表示父容器直接攔截了該系列事件,後續不會再傳遞給子View了。子View想獲取事件只能讓該值為false

onInterceptTouchEvent呼叫返回false(返回false才能傳遞給子View,對應到上面虛擬碼的else中的內容,叫事件傳遞到子容器需要滿足的內容更好理解一些)需要滿足兩個條件中的任意一個就有可能觸發(當然只是有可能):

一個是在down的時候,另一個就是mFirstTouchTarget!=null,那mFirstTouchTarget何時不為空,有興趣的同學可以看ViewGroup中的addTouchTarget這個方法的呼叫時機,mFirstTouchTarget就是在這裡賦值的,原始碼太長我就不貼了。

mFirstTouchTarget是用來儲存ViewGroup中消費了ACTION_DOWN事件的子View,即在上面虛擬碼中child.dispatchTouchEvent(ev)在ACTION_DOWN的時候返回true的View,只要有子View的dispatch在ACTION_DOWN返回true,就不會為null(這個賦值過程只發生在ACTION_DOWN裡,如果子ViewACTION__DOWN不給它賦值後面序列的事件就不會再),反之,若無子View處理,該物件即為null。當然,滿足了上述兩個條件還不行,必須還要滿足!disallowIntercept。

disallowIntercept這個變數很有意思,它的值主要受FLAG_DISALLOW_INTERCEPT這個標記影響,這個值可以被ViewGroup的子View設定,ViewGroup的子View如果呼叫了requestDisallowInterceptTouchEvent這個方法,會改變FLAG_DISALLOW_INTERCEPT,導致disallowIntercept這個值就是ture了,這種情況會跳過intercept,導致攔截失效。

但這事還沒了,FLAG_DISALLOW_INTERCEPT這個標記有一個重置的機制,檢視ViewGroup原始碼可以看到,在處理MotionEvent.ACTION_DOWN的時候會重置這個標記導致disallowIntercept失效,是不是喪心病狂,上面的一段這麼簡單的程式碼有這麼多么蛾子,這裡還能得到一個結論,ACTION_DOWN的時候肯定可以執行onInterceptTouchEvent的。

所以攔截的intercepted很重要,能影響到底是讓ViewGroup還子View處理這個事件。

上面的兩個有可能觸發攔截的條件說完了,那麼當兩個條件都不滿足的話就不會再呼叫攔截了(攔截很重要,一般ViewGroup都返回false這樣能把事件傳遞給子View,如果在ACTION_DOWN時不能走到OnInterceptTouchEvent並返回false告訴ViewGroup不要攔截,則事件再也不能傳給子View了,所以攔截一般都是要走到的,而且一般都是返回false這樣能讓子View有機會處理),這種情況一般都是在ACTION_DOWN處理完之後沒有子View當接盤俠消費ACTION_DOWN以及後續事件,從上面的虛擬碼可以看出來,這時候ViewGroup自己就很被動了,需要自己來呼叫onTouchEvent來處理,這鍋就自己背了。

再繼續說一下mFirstTouchTarget和intercepted是怎麼影響事件方向的。看原始碼:

if (!canceled && !intercepted) {
....
if (actionMasked == MotionEvent.ACTION_DOWN
                       || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                       || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
		....
		for(child : childList){
		    if(!child satisfied condition....){
		        continue;
		    }
		    newTouchTarget = addTouchTarget(child, idBitsToAssign);//在這裡給mFirstTouchTarget賦值
		}

		}
}

可以在這裡看到intercepted為false在ACTION_DOWN裡才能給上面說過的mFirstTouchTarget賦值,只有mFirstTouchTarget不為空才能讓後續事件傳遞給子View,否則根據上上面說的程式碼後續事件只能給父容器處理了。

mFirstTouchTarget就是我們後續事件傳遞的物件,很容易理解,如果在ACTION_DOWN中沒有確定這個物件,則後續事件不知道傳遞給誰自然就交給父容器ViewGroup處理了,真正處理事件傳遞的方法是dispatchTransformedTouchEvent,再看原始碼:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
	    final boolean handled;

        // Canceling motions is a special case.  We don't need to perform any transformations
        // or filtering.  The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            return handled;
        }

}

看到沒,只要引數裡傳的child為空,則ViewGroup呼叫super.dispatchTouchEvent(event),super是誰,ViewGroup繼承自View,當然是View咯,View的dispatch呼叫的誰?當然是自己的onTouchEvent(後面會說),所以這個最後還是呼叫了ViewGroup自己的onTouchEvent。

那麼當child!=null的時候呢,呼叫的是child的dispatchTouchEvent(event),如果child可能是View也可能是ViewGroup,如果是ViewGroup則繼續按照上面的虛擬碼執行事件分發,如果也是View則呼叫自己的onTouchEvent。

所以,說到底事件到底給誰處理,還是和傳進來的child有關,那這個方法在哪裡呼叫的呢,繼續看:

if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
	        ...
	        dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)
	    }

這就是為什麼mFirstTouchTarget能影響事件分發的方向的原因。就這樣,整個虛擬碼的流程是不是很清楚了。

這裡需要多說兩句,在上上面程式碼流程中,intercepted決定了這個事件會不會呼叫ViewGroup的onTouchEvent,當intercepted為true則後續流程會呼叫ViewGroup的onTouchEvent,仔細看上面的程式碼能發現,只有兩種情況為ture:一是呼叫了InterceptTouchEvent把事件攔截下來,另一個就是沒有一個子View能夠消費ActionDown。只有這兩種情況父容器ViewGroup才會自己處理
那麼問題來了,思考一個問題:如果子View處理了ACTION_DOWN但後續事件都返回false,這些沒有被處理的事件最後傳給誰處理了?各位思考之,後面再說這個問題。

孩子是誰的

繼續來擴充套件我們的虛擬碼,攔截條件判斷完之後,決定把事件繼續傳遞給子View的時候,會呼叫childView.dispatchTouchEvent(ev),問題來了,child是哪來的,繼續看原始碼

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

ViewGroup通過判斷所有的子View是否可見是否在播放動畫和是否在點選範圍內來決定它是否能夠有資格接受事件。只有滿足條件的child才能夠呼叫dispatch。

再看虛擬碼,最後dispatch返回ViewGroup的isConsumed,若isConsume == true,說明ViewGroup處理了這個點選事件(ViewGroup自身或者子View處理的),並且這個系列的點選事件會繼續傳到這個ViewGroup來處理,若isConsume == false(ACTION_DOWN時),ViewGroup沒辦法處理這個點選事件,那麼這個系類的點選事件就和該ViewGroup無緣了。會把這個事件上拋給自己的父容器或者Activity處理。

虛擬碼說完了,ViewGroup的事件傳遞規則也就差不多說完了,這麼看是不是很簡單了。View相對於ViewGroup來說就更簡單了,沒有攔截方法,dispatch基本上是直接呼叫了自身的onTouchEvent,處理起來一點難度都木有呀。

一些沒說到但也很重要的點

上面解釋的東西都很簡單,是從一個ViewGroup+一個View開始的,事件分發的執行者是ViewGroup,子容器也只有一個View,但實際開發中當然沒這麼簡單,不過不要怕,再複雜的情況也能夠拆分成這種模式的,只不過層次多了一些遞迴複雜了一些而已,原理還是一樣的。

順帶補充幾點:

  • 從使用者點選螢幕開始觸發一個系列的點選事件時,事件真正的傳遞流程是:Activity(PhoneWindow)->DecorView->ViewGroup->View,在到達ViewGroup之前還有一個DecorView,事件是從Activity傳過來的,但這些東西其實和ViewGroup的原理是一樣的,Activity能看做一個大的ViewGroup,當它的DecorView包含的所有子View沒有人能夠消耗事件的時候(這樣說有漏洞,大家懂我的意思就行了)最後還是會交給Activity處理。
  • 事件衝突解決可以按照上面的原理在幾個point中進行處理。最容易想到的處理的時機是在onInterceptTouchEvent裡,比如當一個豎直方向滑動的ViewGroup裡巢狀一個橫向滑動的ViewGroup,可以在這裡的ACTION_MOVE裡來判斷後續事件應該傳遞給誰處理,當然,也可以根據上面說的標記位FLAG_DISALLOW_INTERCEPT配合子View的dispatchTouchEvent來控制事件的流向,這都是比較容易想到的,不過看過別的大神,通過分享MotionEvent的方法來控制事件的流向,即在父容器中儲存MotionEvent並在適當的時機傳入子View自定義的事件處理方法來分享事件,也是可行的。
  • 任何View只要拒絕了一系列事件中的ACTION_DOWN(返回false),則後續事件都不會再傳遞過來了。但如果拒絕了其他的事件,後續事件還是可以傳過來的,比如View某次ACTION_MOVE沒處理,這個沒處理的事件最後會被Activity消耗掉(而不是View的父容器),但後續的事件還是會繼續傳給該View。
  • 合理的利用ACTION_CANCEL能夠控制一個系列事件的生命週期,讓事件處理更加靈活。

理解事件分發的機制只要明白上面的原理基本就夠用了,github上很多牛逼的大神寫的各種炫酷的自定義控制元件的事分發根據這些也能夠看明白,當然還有很多擴充套件的東西和更深入的內容由於篇幅的關係在這裡就不羅嗦了,更重要的還是去看原始碼吧。
最後送各位一句經典:紙上得來終覺淺,絕知此事要躬行!

相關文章