Android ViewGroup事件分發機制

發表於2015-06-01

上一篇已經完整的解析了Android View的事件分發機制,今天給大家程式碼ViewGroup事件分發的原始碼解析~~凡是自定義ViewGroup實現各種滑動效果的,不可避免的會出現很多事件的衝突,對ViewGroup事件分發機制的瞭解,也有益於大家瞭解衝突產生的原因,以及對衝突進行處理~

1、案例

首先我們接著上一篇的程式碼,在程式碼中新增一個自定義的LinearLayout:

繼承LinearLayout,然後複寫了與事件分發機制有關的程式碼,新增上了日誌的列印~

然後看我們的佈局檔案:

MyLinearLayout中包含一個MyButton,MyButton都上篇部落格中已經出現過,這裡就不再貼程式碼了,不清楚可以去檢視~

然後MainActivity就是直接載入佈局,沒有任何程式碼~~~

直接執行我們的程式碼,然後點選我們的Button,依然是有意的MOVE一下,不然不會觸發MOVE事件,看一下日誌的輸出:

可以看到大體的事件流程為:

MyLinearLayout的dispatchTouchEvent -> MyLinearLayout的onInterceptTouchEvent -> MyButton的dispatchTouchEvent ->Mybutton的onTouchEvent

可以看出,在View上觸發事件,最先捕獲到事件的為View所在的ViewGroup,然後才會到View自身~

下面我們按照日誌的輸出,進入原始碼~

2、原始碼分析

ViewGroup – dispatchTouchEvent

1、ViewGroup – dispatchTouchEvent – ACTION_DOWN

首先是ViewGroup的dispatchTouchEvent方法:

程式碼比較長,決定分段貼出,首先貼出的是ACTION_DOWN事件相關的程式碼。

16行:進入ACTION_DOWN的處理

17-23行:將mMotionTarget置為null

26行:進行判斷:if(disallowIntercept || !onInterceptTouchEvent(ev))

兩種可能會進入IF程式碼段

1、當前不允許攔截,即disallowIntercept =true,

2、當前允許攔截但是不攔截,即disallowIntercept =false,但是onInterceptTouchEvent(ev)返回false ;

注:disallowIntercept 可以通過viewGroup.requestDisallowInterceptTouchEvent(boolean);進行設定,後面會詳細說;而onInterceptTouchEvent(ev)可以進行復寫。

36-57行:開始遍歷所有的子View

41行:進行判斷當前的x,y座標是否落在子View身上,如果在,47行,執行child.dispatchTouchEvent(ev),就進入了View的dispatchTouchEvent程式碼中了,如果不瞭解請參考:Android View的事件分發機制,當child.dispatchTouchEvent(ev)返回true,則為mMotionTarget=child;然後return true;

ViewGroup的ACTION_DOWN分析結束,總結一下:

ViewGroup實現捕獲到DOWN事件,如果程式碼中不做TOUCH事件攔截,則開始查詢當前x,y是否在某個子View的區域內,如果在,則把事件分發下去。

按照日誌,接下來到達ACTION_MOVE

2、ViewGroup – dispatchTouchEvent – ACTION_MOVE

首先我們原始碼進行刪減,只留下MOVE相關的程式碼:

18行:把ACTION_DOWN時賦值的mMotionTarget,付給target ;

23行:if (!disallowIntercept && onInterceptTouchEvent(ev)) 當前允許攔截且攔截了,才進入IF體,當然了預設是不會攔截的~這裡執行了onInterceptTouchEvent(ev)

28-30行:把座標系統轉化為子View的座標系統

32行:直接return target.dispatchTouchEvent(ev);

可以看到,正常流程下,ACTION_MOVE在檢測完是否攔截以後,直接呼叫了子View.dispatchTouchEvent,事件分發下去;

最後就是ACTION_UP了

3、ViewGroup – dispatchTouchEvent – ACTION_UP

17行:判斷當前是否是ACTION_UP

21,28行:分別重置攔截標誌位以及將DOWN賦值的mMotionTarget置為null,都UP了,當然置為null,下一次DOWN還會再賦值的~

最後,修改座標系統,然後呼叫target.dispatchTouchEvent(ev);

正常情況下,即我們上例整個程式碼的流程我們已經走完了:

1、ACTION_DOWN中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則找到包含當前x,y座標的子View,賦值給mMotionTarget,然後呼叫 mMotionTarget.dispatchTouchEvent

2、ACTION_MOVE中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接呼叫mMotionTarget.dispatchTouchEvent(ev)

3、ACTION_UP中,ViewGroup捕獲到事件,然後判斷是否攔截,如果沒有攔截,則直接呼叫mMotionTarget.dispatchTouchEvent(ev)

當然了在分發之前都會修改下座標系統,把當前的x,y分別減去child.left 和 child.top ,然後傳給child;

3、關於攔截

1、如何攔截

上面的總結都是基於:如果沒有攔截;那麼如何攔截呢?

複寫ViewGroup的onInterceptTouchEvent方法:

預設是不攔截的,即返回false;如果你需要攔截,只要return true就行了,這要該事件就不會往子View傳遞了,並且如果你在DOWN retrun true ,則DOWN,MOVE,UP子View都不會捕獲事件;如果你在MOVE return true , 則子View在MOVE和UP都不會捕獲事件。

原因很簡單,當onInterceptTouchEvent(ev) return true的時候,會把mMotionTarget 置為null ;

2、如何不被攔截

如果ViewGroup的onInterceptTouchEvent(ev) 當ACTION_MOVE時return true ,即攔截了子View的MOVE以及UP事件;

此時子View希望依然能夠響應MOVE和UP時該咋辦呢?

Android給我們提供了一個方法:requestDisallowInterceptTouchEvent(boolean) 用於設定是否允許攔截,我們在子View的dispatchTouchEvent中直接這麼寫:

getParent().requestDisallowInterceptTouchEvent(true);  這樣即使ViewGroup在MOVE的時候return true,子View依然可以捕獲到MOVE以及UP事件。

從原始碼也可以解釋:

ViewGroup MOVE和UP攔截的原始碼是這樣的:

當我們把disallowIntercept設定為true時,!disallowIntercept直接為false,於是攔截的方法體就被跳過了~

注:如果ViewGroup在onInterceptTouchEvent(ev)  ACTION_DOWN裡面直接return true了,那麼子View是木有辦法的捕獲事件的~~~

4、如果沒有找到合適的子View

我們的例項,直接點選ViewGroup內的按鈕,當然直接很順利的走完整個流程;

但是有兩種特殊情況

1、ACTION_DOWN的時候,子View.dispatchTouchEvent(ev)返回的為false ; 

如果你仔細看了,你會注意到ViewGroup的dispatchTouchEvent(ev)的ACTION_DOWN程式碼是這樣的

只有在child.dispatchTouchEvent(ev)返回true了,才會認為找到了能夠處理當前事件的View,即mMotionTarget = child;

但是如果返回false,那麼mMotionTarget 依然是null

mMotionTarget 為null會咋樣呢?

其實ViewGroup也是View的子類,如果沒有找到能夠處理該事件的子View,或者乾脆就沒有子View;

那麼,它作為一個View,就相當於View的事件轉發了~~直接super.dispatchTouchEvent(ev);

原始碼是這樣的:

我們沒有一個能夠處理該事件的目標元素,意味著我們需要自己處理~~~就相當於傳統的View~

2、那麼什麼時候子View.dispatchTouchEvent(ev)返回的為true

如果你仔細看了上篇部落格,你會發現只要子View支援點選或者長按事件一定返回true~~

原始碼是這樣的:

5、總結

關於程式碼流程上面已經總結過了~

1、如果ViewGroup找到了能夠處理該事件的View,則直接交給子View處理,自己的onTouchEvent不會被觸發;

2、可以通過複寫onInterceptTouchEvent(ev)方法,攔截子View的事件(即return true),把事件交給自己處理,則會執行自己對應的onTouchEvent方法

3、子View可以通過呼叫getParent().requestDisallowInterceptTouchEvent(true);  阻止ViewGroup對其MOVE或者UP事件進行攔截;

好了,那麼實際應用中能解決哪些問題呢?

比如你需要寫一個類似slidingmenu的左側隱藏menu,主Activity上有個Button、ListView或者任何可以響應點選的View,你在當前View上死命的滑動,選單欄也出不來;因為MOVE事件被子View處理了~ 你需要這麼做:在ViewGroup的dispatchTouchEvent中判斷使用者是不是想顯示選單,如果是,則在onInterceptTouchEvent(ev)攔截子View的事件;自己進行處理,這樣自己的onTouchEvent就可以順利展現出選單欄了~~

相關文章