【轉】Android事件分發機制徹底解析,帶你從源碼的角度完全理解(下)

轉載請註明出處:http://blog.csdn.net/guolin_blog/article/details/9153761android

記得在前面的文章中,我帶你們一塊兒從源碼的角度分析了Android中View的事件分發機制,相信閱讀過的朋友對View的事件分發已經有比較深入的理解了。app

還未閱讀過的朋友,請先參考 Android事件分發機制徹底解析,帶你從源碼的角度完全理解(上) 。ide

那麼今天咱們將繼續上次未完成的話題,從源碼的角度分析ViewGruop的事件分發。佈局

首先咱們來探討一下,什麼是ViewGroup?它和普通的View有什麼區別?ui

顧名思義,ViewGroup就是一組View的集合,它包含不少的子View和子VewGroup,是Android中全部佈局的父類或間接父類,像LinearLayout、RelativeLayout等都是繼承自ViewGroup的。但ViewGroup實際上也是一個View,只不過比起View,它多了能夠包含子View和定義佈局參數的功能。ViewGroup繼承結構示意圖以下所示:this

能夠看到,咱們平時項目裏常常用到的各類佈局,全都屬於ViewGroup的子類。spa

簡單介紹完了ViewGroup,咱們如今經過一個Demo來演示一下Android中VewGroup的事件分發流程吧。.net

首先咱們來自定義一個佈局,命名爲MyLayout,繼承自LinearLayout,以下所示:3d

1 public class MyLayout extends LinearLayout {
2 
3     public MyLayout(Context context, AttributeSet attrs) {
4         super(context, attrs);
5     }
6 
7 }

而後,打開主佈局文件activity_main.xml,在其中加入咱們自定義的佈局:rest

 1 <com.example.viewgrouptouchevent.MyLayout xmlns:android="http://schemas.android.com/apk/res/android"
 2     xmlns:tools="http://schemas.android.com/tools"
 3     android:id="@+id/my_layout"
 4     android:layout_width="match_parent"
 5     android:layout_height="match_parent"
 6     android:orientation="vertical" >
 7 
 8     <Button
 9         android:id="@+id/button1"
10         android:layout_width="match_parent"
11         android:layout_height="wrap_content"
12         android:text="Button1" />
13 
14     <Button
15         android:id="@+id/button2"
16         android:layout_width="match_parent"
17         android:layout_height="wrap_content"
18         android:text="Button2" />
19 
20 </com.example.viewgrouptouchevent.MyLayout>

能夠看到,咱們在MyLayout中添加了兩個按鈕,接着在MainActivity中爲這兩個按鈕和MyLayout都註冊了監聽事件:

 1 myLayout.setOnTouchListener(new OnTouchListener() {
 2     @Override
 3     public boolean onTouch(View v, MotionEvent event) {
 4         Log.d("TAG", "myLayout on touch");
 5         return false;
 6     }
 7 });
 8 button1.setOnClickListener(new OnClickListener() {
 9     @Override
10     public void onClick(View v) {
11         Log.d("TAG", "You clicked button1");
12     }
13 });
14 button2.setOnClickListener(new OnClickListener() {
15     @Override
16     public void onClick(View v) {
17         Log.d("TAG", "You clicked button2");
18     }
19 });

咱們在MyLayout的onTouch方法,和Button一、Button2的onClick方法中都打印了一句話。如今運行一下項目,效果圖以下所示:

分別點擊一下Button一、Button2和空白區域,打印結果以下所示:

你會發現,當點擊按鈕的時候,MyLayout註冊的onTouch方法並不會執行,只有點擊空白區域的時候纔會執行該方法。你能夠先理解成Button的onClick方法將事件消費掉了,所以事件不會再繼續向下傳遞。

那就說明Android中的touch事件是先傳遞到View,再傳遞到ViewGroup的?如今下結論還未免過早了,讓咱們再來作一個實驗。

查閱文檔能夠看到,ViewGroup中有一個onInterceptTouchEvent方法,咱們來看一下這個方法的源碼:

 1 /**
 2  * Implement this method to intercept all touch screen motion events.  This
 3  * allows you to watch events as they are dispatched to your children, and
 4  * take ownership of the current gesture at any point.
 5  *
 6  * <p>Using this function takes some care, as it has a fairly complicated
 7  * interaction with {@link View#onTouchEvent(MotionEvent)
 8  * View.onTouchEvent(MotionEvent)}, and using it requires implementing
 9  * that method as well as this one in the correct way.  Events will be
10  * received in the following order:
11  *
12  * <ol>
13  * <li> You will receive the down event here.
14  * <li> The down event will be handled either by a child of this view
15  * group, or given to your own onTouchEvent() method to handle; this means
16  * you should implement onTouchEvent() to return true, so you will
17  * continue to see the rest of the gesture (instead of looking for
18  * a parent view to handle it).  Also, by returning true from
19  * onTouchEvent(), you will not receive any following
20  * events in onInterceptTouchEvent() and all touch processing must
21  * happen in onTouchEvent() like normal.
22  * <li> For as long as you return false from this function, each following
23  * event (up to and including the final up) will be delivered first here
24  * and then to the target's onTouchEvent().
25  * <li> If you return true from here, you will not receive any
26  * following events: the target view will receive the same event but
27  * with the action {@link MotionEvent#ACTION_CANCEL}, and all further
28  * events will be delivered to your onTouchEvent() method and no longer
29  * appear here.
30  * </ol>
31  *
32  * @param ev The motion event being dispatched down the hierarchy.
33  * @return Return true to steal motion events from the children and have
34  * them dispatched to this ViewGroup through onTouchEvent().
35  * The current target will receive an ACTION_CANCEL event, and no further
36  * messages will be delivered here.
37  */
38 public boolean onInterceptTouchEvent(MotionEvent ev) {
39     return false;
40 }

若是不看源碼你還真可能被這注釋嚇到了,這麼長的英文註釋看得頭都大了。但是源碼居然如此簡單!只有一行代碼,返回了一個false!

 

好吧,既然是布爾型的返回,那麼只有兩種可能,咱們在MyLayout中重寫這個方法,而後返回一個true試試,代碼以下所示:

 1 public class MyLayout extends LinearLayout {
 2 
 3     public MyLayout(Context context, AttributeSet attrs) {
 4         super(context, attrs);
 5     }
 6     
 7     @Override
 8     public boolean onInterceptTouchEvent(MotionEvent ev) {
 9         return true;
10     }
11     
12 }

如今再次運行項目,而後分別Button一、Button2和空白區域,打印結果以下所示:

你會發現,無論你點擊哪裏,永遠都只會觸發MyLayout的touch事件了,按鈕的點擊事件徹底被屏蔽掉了!這是爲何呢?若是Android中的touch事件是先傳遞到View,再傳遞到ViewGroup的,那麼MyLayout又怎麼可能屏蔽掉Button的點擊事件呢?

看來只有經過閱讀源碼,搞清楚Android中ViewGroup的事件分發機制,才能解決咱們心中的疑惑了,不過這裏我想先跟你透露一句,Android中touch事件的傳遞,絕對是先傳遞到ViewGroup,再傳遞到View的。記得在Android事件分發機制徹底解析,帶你從源碼的角度完全理解(上) 中我有說明過,只要你觸摸了任何控件,就必定會調用該控件的dispatchTouchEvent方法。這個說法沒錯,只不過還不完整而已。實際狀況是,當你點擊了某個控件,首先會去調用該控件所在佈局的dispatchTouchEvent方法,而後在佈局的dispatchTouchEvent方法中找到被點擊的相應控件,再去調用該控件的dispatchTouchEvent方法。若是咱們點擊了MyLayout中的按鈕,會先去調用MyLayout的dispatchTouchEvent方法,但是你會發現MyLayout中並無這個方法。那就再到它的父類LinearLayout中找一找,發現也沒有這個方法。那隻好繼續再找LinearLayout的父類ViewGroup,你終於在ViewGroup中看到了這個方法,按鈕的dispatchTouchEvent方法就是在這裏調用的。修改後的示意圖以下所示:

那還等什麼?快去看一看ViewGroup中的dispatchTouchEvent方法的源碼吧!代碼以下所示:

 1 public boolean dispatchTouchEvent(MotionEvent ev) {
 2     final int action = ev.getAction();
 3     final float xf = ev.getX();
 4     final float yf = ev.getY();
 5     final float scrolledXFloat = xf + mScrollX;
 6     final float scrolledYFloat = yf + mScrollY;
 7     final Rect frame = mTempRect;
 8     boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
 9     if (action == MotionEvent.ACTION_DOWN) {
10         if (mMotionTarget != null) {
11             mMotionTarget = null;
12         }
13         if (disallowIntercept || !onInterceptTouchEvent(ev)) {
14             ev.setAction(MotionEvent.ACTION_DOWN);
15             final int scrolledXInt = (int) scrolledXFloat;
16             final int scrolledYInt = (int) scrolledYFloat;
17             final View[] children = mChildren;
18             final int count = mChildrenCount;
19             for (int i = count - 1; i >= 0; i--) {
20                 final View child = children[i];
21                 if ((child.mViewFlags & VISIBILITY_MASK) == VISIBLE
22                         || child.getAnimation() != null) {
23                     child.getHitRect(frame);
24                     if (frame.contains(scrolledXInt, scrolledYInt)) {
25                         final float xc = scrolledXFloat - child.mLeft;
26                         final float yc = scrolledYFloat - child.mTop;
27                         ev.setLocation(xc, yc);
28                         child.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
29                         if (child.dispatchTouchEvent(ev))  {
30                             mMotionTarget = child;
31                             return true;
32                         }
33                     }
34                 }
35             }
36         }
37     }
38     boolean isUpOrCancel = (action == MotionEvent.ACTION_UP) ||
39             (action == MotionEvent.ACTION_CANCEL);
40     if (isUpOrCancel) {
41         mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
42     }
43     final View target = mMotionTarget;
44     if (target == null) {
45         ev.setLocation(xf, yf);
46         if ((mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
47             ev.setAction(MotionEvent.ACTION_CANCEL);
48             mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
49         }
50         return super.dispatchTouchEvent(ev);
51     }
52     if (!disallowIntercept && onInterceptTouchEvent(ev)) {
53         final float xc = scrolledXFloat - (float) target.mLeft;
54         final float yc = scrolledYFloat - (float) target.mTop;
55         mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
56         ev.setAction(MotionEvent.ACTION_CANCEL);
57         ev.setLocation(xc, yc);
58         if (!target.dispatchTouchEvent(ev)) {
59         }
60         mMotionTarget = null;
61         return true;
62     }
63     if (isUpOrCancel) {
64         mMotionTarget = null;
65     }
66     final float xc = scrolledXFloat - (float) target.mLeft;
67     final float yc = scrolledYFloat - (float) target.mTop;
68     ev.setLocation(xc, yc);
69     if ((target.mPrivateFlags & CANCEL_NEXT_UP_EVENT) != 0) {
70         ev.setAction(MotionEvent.ACTION_CANCEL);
71         target.mPrivateFlags &= ~CANCEL_NEXT_UP_EVENT;
72         mMotionTarget = null;
73     }
74     return target.dispatchTouchEvent(ev);
75 }

這個方法代碼比較長,咱們只挑重點看。首先在第13行能夠看到一個條件判斷,若是disallowIntercept和!onInterceptTouchEvent(ev)二者有一個爲true,就會進入到這個條件判斷中。disallowIntercept是指是否禁用掉事件攔截的功能,默認是false,也能夠經過調用requestDisallowInterceptTouchEvent方法對這個值進行修改。那麼當第一個值爲false的時候就會徹底依賴第二個值來決定是否能夠進入到條件判斷的內部,第二個值是什麼呢?居然就是對onInterceptTouchEvent方法的返回值取反!也就是說若是咱們在onInterceptTouchEvent方法中返回false,就會讓第二個值爲true,從而進入到條件判斷的內部,若是咱們在onInterceptTouchEvent方法中返回true,就會讓第二個值爲false,從而跳出了這個條件判斷。

 

這個時候你就能夠思考一下了,因爲咱們剛剛在MyLayout中重寫了onInterceptTouchEvent方法,讓這個方法返回true,致使全部按鈕的點擊事件都被屏蔽了,那咱們就徹底有理由相信,按鈕點擊事件的處理就是在第13行條件判斷的內部進行的!

那咱們重點來看下條件判斷的內部是怎麼實現的。在第19行經過一個for循環,遍歷了當前ViewGroup下的全部子View,而後在第24行判斷當前遍歷的View是否是正在點擊的View,若是是的話就會進入到該條件判斷的內部,而後在第29行調用了該View的dispatchTouchEvent,以後的流程就和 Android事件分發機制徹底解析,帶你從源碼的角度完全理解(上) 中講解的是同樣的了。咱們也所以證明了,按鈕點擊事件的處理確實就是在這裏進行的。

而後須要注意一下,調用子View的dispatchTouchEvent後是有返回值的。咱們已經知道,若是一個控件是可點擊的,那麼點擊該控件時,dispatchTouchEvent的返回值一定是true。所以會致使第29行的條件判斷成立,因而在第31行給ViewGroup的dispatchTouchEvent方法直接返回了true。這樣就致使後面的代碼沒法執行到了,也是印證了咱們前面的Demo打印的結果,若是按鈕的點擊事件獲得執行,就會把MyLayout的touch事件攔截掉。

那若是咱們點擊的不是按鈕,而是空白區域呢?這種狀況就必定不會在第31行返回true了,而是會繼續執行後面的代碼。那咱們繼續日後看,在第44行,若是target等於null,就會進入到該條件判斷內部,這裏通常狀況下target都會是null,所以會在第50行調用super.dispatchTouchEvent(ev)。這句代碼會調用到哪裏呢?固然是View中的dispatchTouchEvent方法了,由於ViewGroup的父類就是View。以後的處理邏輯又和前面所說的是同樣的了,也所以MyLayout中註冊的onTouch方法會獲得執行。以後的代碼在通常狀況下是走不到的了,咱們也就再也不繼續往下分析。

再看一下整個ViewGroup事件分發過程的流程圖吧,相信能夠幫助你們更好地去理解:

如今整個ViewGroup的事件分發流程的分析也就到此結束了,咱們最後再來簡單梳理一下吧。

1. Android事件分發是先傳遞到ViewGroup,再由ViewGroup傳遞到View的。

2. 在ViewGroup中能夠經過onInterceptTouchEvent方法對事件傳遞進行攔截,onInterceptTouchEvent方法返回true表明不容許事件繼續向子View傳遞,返回false表明不對事件進行攔截,默認返回false。

3. 子View中若是將傳遞的事件消費掉,ViewGroup中將沒法接收到任何事件。

好了,Android事件分發機制徹底解析到此所有結束,結合上下兩篇,相信你們對事件分發的理解已經很是深入了。

相關文章
相關標籤/搜索