Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> Android資訊 >> 10分鐘理解 Android View 事件分發機制

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

編輯:Android資訊

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上很多牛逼的大神寫的各種炫酷的自定義控件的事分發根據這些也能夠看明白,當然還有很多擴展的東西和更深入的內容由於篇幅的關系在這裡就不羅嗦了,更重要的還是去看源碼吧。
最後送各位一句經典:紙上得來終覺淺,絕知此事要躬行!

  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved