版權聲明:本文出自汪磊的博客,轉載請務必注明出處。
在前兩篇我們共同探討了事件傳遞機制《View篇》與《ViewGroup篇》,我們知道View觸摸事件是ViewGroup傳遞過去的,比如一個很簡單的布局最外層是LinearLayout,里面就一個Button,我們點擊Button的時候觸摸事件是由外層LinearLayout傳遞給里面Button的,但是有沒有想過當前觸摸事件是誰傳遞給外層的LinearLayout的呢?帶着這個疑問我們繼續來共同探討一下。
從Demo示例說起
我們還是先寫一個簡單的demo,很簡單,代碼如下:自定義Button:
1 public class MyButton extends Button { 2
3 private final String TAG = "WL"; 4
5 public MyButton(Context context, AttributeSet attrs) { 6 super(context, attrs); 7 } 8
9 @Override 10 public boolean dispatchTouchEvent(MotionEvent ev) { 11 // 12 Log.i(TAG, "MyButton_dispatchTouchEvent_Action:"+ev.getAction()); 13 return super.dispatchTouchEvent(ev); 14 } 15
16 @Override 17 public boolean onTouchEvent(MotionEvent event) { 18 // 19 Log.i(TAG, "MyButton_onTouchEvent_Action:"+event.getAction()); 20 return super.onTouchEvent(event); 21 } 22 }
自定義LinearLayout:
1 public class MyLinearLayout extends LinearLayout { 2
3 private final String TAG = "WL"; 4
5 public MyLinearLayout(Context context, AttributeSet attrs) { 6 super(context, attrs); 7 // 8 } 9
10 @Override 11 public boolean dispatchTouchEvent(MotionEvent ev) { 12 // 13 Log.i(TAG, "MyLinearLayout_dispatchTouchEvent_Action:"+ev.getAction()); 14 return super.dispatchTouchEvent(ev); 15 } 16
17 @Override 18 public boolean onTouchEvent(MotionEvent event) { 19 // 20 Log.i(TAG, "MyLinearLayout_onTouchEvent_Action:"+event.getAction()); 21 return super.onTouchEvent(event); 22 } 23 }
布局文件:
1 <com.wl.activitydispatchtouchevent.MyLinearLayout 2 xmlns:android="http://schemas.android.com/apk/res/android"
3 xmlns:tools="http://schemas.android.com/tools"
4 android:layout_width="match_parent"
5 android:layout_height="match_parent"
6 android:background="#0099cc"
7 android:id="@+id/mylinearlayout"
8 android:gravity="center"
9 tools:context="com.wl.activitydispatchtouchevent.MainActivity" >
10
11 <com.wl.activitydispatchtouchevent.MyButton 12 android:id="@+id/mybutton"
13 android:layout_width="wrap_content"
14 android:layout_height="wrap_content"
15 android:textSize="20sp"
16 android:text="WL_Button" />
17
18 </com.wl.activitydispatchtouchevent.MyLinearLayout>
Activity中代碼:
1 public class MainActivity extends Activity implements OnClickListener, 2 OnTouchListener { 3
4 private final String TAG = "WL"; 5
6 @Override 7 protected void onCreate(Bundle savedInstanceState) { 8 super.onCreate(savedInstanceState); 9 setContentView(R.layout.activity_fullscreen); 10 // 11 findViewById(R.id.mybutton).setOnClickListener(this); 12 findViewById(R.id.mybutton).setOnTouchListener(this); 13 // 14 findViewById(R.id.mylinearlayout).setOnClickListener(this); 15 findViewById(R.id.mylinearlayout).setOnTouchListener(this); 16 } 17
18 @Override 19 public boolean onTouch(View v, MotionEvent event) { 20 // 21 Log.i(TAG, "onTouch___v:" + v + "___action:" + event.getAction()); 22 return false; 23 } 24
25 @Override 26 public void onClick(View v) { 27 // 28 Log.i(TAG, "onClick___v:" + v); 29 } 30
31 @Override 32 public boolean dispatchTouchEvent(MotionEvent ev) { 33 Log.i(TAG, "MainActivity__dispatchTouchEvent__action:" + ev.getAction()); 34 return super.dispatchTouchEvent(ev); 35 } 36
37 @Override 38 public boolean onTouchEvent(MotionEvent event) { 39 Log.i(TAG, "MainActivity___onTouchEvent___action=" + event.getAction()); 40 return super.onTouchEvent(event); 41 } 42 }
怎么樣,很簡單吧。和上一篇講解ViewGroup傳遞機制的Demo幾乎差不多,主要差別就是在Activity中我們重寫了Activity的dispatchTouchEvent與onTouchEvent方法。
我們看一下運行效果,點擊Button,打印信息如下:
1 MainActivity__dispatchTouchEvent__action:0
2 MyLinearLayout_dispatchTouchEvent_Action:0
3 MyButton_dispatchTouchEvent_Action:0
4 onTouch___v:com.wl.activitydispatchtouchevent.MyButton___action:0
5 MyButton_onTouchEvent_Action:0
6 MainActivity__dispatchTouchEvent__action:1
7 MyLinearLayout_dispatchTouchEvent_Action:1
8 MyButton_dispatchTouchEvent_Action:1
9 onTouch___v:com.wl.activitydispatchtouchevent.MyButton___action:1
10 MyButton_onTouchEvent_Action:1
11 onClick___v:com.wl.activitydispatchtouchevent.MyButton
除去與Activity有關的信息,其余信息打印順序相信你應該輕松理解了。我們看到觸摸事件實現傳遞到Activity中的,其次才傳遞到MyLinearLayout,最后傳遞給MyButton。是不是觸摸事件就是Activity先獲取到接下來才繼續向下傳遞的呢?別急着下結論,我們看看Activity中dispatchTouchEvent都做了什么。
Activity事件傳遞機制源碼分析(源碼版本為API23)
Activity中dispatchTouchEvent方法源碼如下:
1 public boolean dispatchTouchEvent(MotionEvent ev) { 2 if (ev.getAction() == MotionEvent.ACTION_DOWN) { 3 onUserInteraction(); 4 } 5 if (getWindow().superDispatchTouchEvent(ev)) { 6 return true; 7 } 8 return onTouchEvent(ev); 9 }
是不是爽歪歪?這么短,我們看2-4行代碼,首先判斷如果是ACTION_DOWN事件則執行onUserInteraction()方法,對於onUserInteraction()方法這里不做具體分析,不是本篇重點。
我們繼續向下分析,5-9代碼,如果if條件成立則直接返回true,不成立則dispatchTouchEvent最終返回值由onTouchEvent決定,那么if判斷就是關鍵了。
5行代碼,getWindow()返回mWindow對象,在Activity的attach方法中進行的初始化,如下:
1 final void attach(Context context, ActivityThread aThread, 2 Instrumentation instr, IBinder token, int ident, 3 Application application, Intent intent, ActivityInfo info, 4 CharSequence title, Activity parent, String id, 5 NonConfigurationInstances lastNonConfigurationInstances, 6 Configuration config, String referrer, IVoiceInteractor voiceInteractor) { 7
8 ........... 9 mWindow = new PhoneWindow(this); 10 mWindow.setCallback(this); 11 ........... 12 }
mWindow其實就是PhoneWindow對象,接下來我們找到PhoneWindow類(源碼目錄:...\sdk\sources\android-23\com\android\internal\policy\)。
PhoneWindow類繼承自Window類,我們先看看父類中superDispatchTouchEvent是怎么處理的。
Window類中superDispatchTouchEvent源碼如下:
1 /**
2 * Used by custom windows, such as Dialog, to pass the touch screen event 3 * further down the view hierarchy. Application developers should 4 * not need to implement or call this. 5 * 6 */
7 public abstract boolean superDispatchTouchEvent(MotionEvent event);
看到了吧,很簡單,父類中就是一個抽象方法, 看注釋就知道此方法主要用來屏幕事件傳遞的,開發者不需要實現或者調用這個方法。
接下來我們看看PhoneWindow類中的superDispatchTouchEvent方法:
1 @Override 2 public boolean superDispatchTouchEvent(MotionEvent event) { 3 return mDecor.superDispatchTouchEvent(event); 4 }
是不是更簡單?直接調用mDecor的superDispatchTouchEvent方法,mDecor是什么玩意呢?這里就直說說了,mDecor是DecorView的實例。
DecorView類是PhoneWindow類的內部類,源碼如下:
1 private final class DecorView extends FrameLayout implements RootViewSurfaceTaker { 2
3 .......... 4 public boolean superDispatchTouchEvent(MotionEvent event) { 5 return super.dispatchTouchEvent(event); 6 } 7
8 .......... 9 }
我勒個去,搞半天DecorView 繼承自FrameLayout,我們知道 FrameLayout繼承自ViewGroup,最終就是調用ViewGroup中的dispatchTouchEvent方法進行事件分發。
但是到這里我們還有一個疑問,以我們Demo為例,通過上述分析事件先傳遞到Activity的dispatchTouchEvent方法,然后調用DecorView 的superDispatchTouchEvent方法最終調用的ViewGroup的dispatchTouchEvent方法,但是跟我們Demo中的MyLinearLayout有什么關系呢?或者說是怎么傳遞到MyLinearLayout的呢?
要解答這個疑問我們就必須熟知我們平時調用Activity中的setContentView方法設置布局的時候我們自己的布局到底是怎么掛載到Activity上的,這篇我們就不進入深入源碼解析了,不是本篇重點,直說一些結論性東西。后續會單獨寫一篇文章專門分析setContentView究竟都做了什么。
我們在調用setContentView設置布局的時候其實都是被放置在id為content的FrameLayout 布局中的,注意id為content的FrameLayout 布局並不是上面講的DecorView,具體層級關系如下:
看到了吧,id為content的FrameLayout 布局是DecorView的子View布局。我們自己的布局最后總會替換掉id為content的FrameLayout 。
到這里你該明白了吧,Activity將觸摸事件經過層層傳遞給DecorView, 而DecorView會調用ViewGroup的dispatchTouchEvent方法將事件傳遞給子View。之后的邏輯就是我們上兩篇所講的內容了。
接下來我們回看Activity中dispatchTouchEvent方法,第5行根據我們上述分析的,如果最終找到子View消耗事件則返回值為true,進而整個方法返回true。如果沒有子View處理當前觸摸事件則返回false,執行Activity中onTouchEvent方法。
我們接下來分析一下Activity中onTouchEvent方法,源碼如下;
1 /**
2 * Called when a touch screen event was not handled by any of the views 3 * under it. This is most useful to process touch events that happen 4 * outside of your window bounds, where there is no view to receive it. 5 * 6 * @param event The touch screen event being processed. 7 * 8 * @return Return true if you have consumed the event, false if you haven't. 9 * The default implementation always returns false. 10 */
11 public boolean onTouchEvent(MotionEvent event) { 12 if (mWindow.shouldCloseOnTouch(this, event)) { 13 finish(); 14 return true; 15 } 16
17 return false; 18 }
邏輯也不復雜,主要就是第12行代碼,調用mWindow的shouldCloseOnTouch方法,如果此方法返回true則整個方法返回true,反之則返回false。
mWindow上面我們分析過就是PhoneWindow的實例,好了我們就去PhoneWindow類中找shouldCloseOnTouch方法看一下吧,然而PhoneWindow中並沒有這個方法,那我們看看父類Window中有沒有這個方法呢,果然這個方法在其父類中找到,源碼如下:
1 public boolean shouldCloseOnTouch(Context context, MotionEvent event) { 2 if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN 3 && isOutOfBounds(context, event) && peekDecorView() != null) { 4 return true; 5 } 6 return false; 7 }
這里主要邏輯也是一個判斷,判斷mCloseOnTouchOutside標記以及是否為ACTION_DOWN事件,然后判斷點擊事件的坐標x,y有沒有超出邊界,最后調用peekDecorView()判斷是否為空。peekDecorView()在Window類中就是一個抽象方法,具體實現在PhoneWindow類中如下:
1 public final View peekDecorView() { 2 return mDecor; 3 }
很簡單,就是返回mDecor,上面我們分析過mDecor就是DecorView的實例,這里我們需要知道我們在Activity中調用setContentView的時候mDecor就會初始化,具體分析會在下一篇文章中,這里只要知道mDecor不為null就可以了。
其余的都不難理解但是mCloseOnTouchOutside 是個什么鬼呢?我們知道Activity設置成Dialog樣式的時候默認點擊外部的時候是會關閉的,同樣我們也可以調用setFinishOnTouchOutside(false)設置為點擊外部時候Activity不關閉,mCloseOnTouchOutside 就是用來記錄這個的,如果我們將Activity設置為Dialog樣式mCloseOnTouchOutside 默認就被設置為true,我們知道大部分情況下Activity是不會設置為Dialog樣式的,所以mCloseOnTouchOutside 默認為false。(關於mCloseOnTouchOutside其實是想從源碼角度分析一下的,但是這部分內容實在和傳遞機制不沾邊,就這部分有一個判斷,所以就不仔細分析了,在下一篇分析setContentView的時候在提一下吧 )
這里我們稍微總結一下:mCloseOnTouchOutside 默認情況下是false,如果Activity樣式設置為Dialog系統默認會將mCloseOnTouchOutside 設置為true,所以Dialog樣式的Activity默認情況下點擊外部會關閉,如果我們調用setFinishOnTouchOutside(false)或者在styles文件中設置了 <item name="android:windowCloseOnTouchOutside">false</item> 那么最終都會將mCloseOnTouchOutside 變量置為false,點擊Activity外部也就不會關閉了。
綜上分析,Window中shouldCloseOnTouch大多數情況下是返回false的,從而Activity中onTouchEvent大多說情況下也是返回false,除非我們進行了特殊設置。這也就是Activity中onTouchEvent注釋是The default implementation always returns false而不是The default implementation returns false,就多了一個always。
好了,到此為止關於安卓事件傳遞機制最重要的部分都已經講解完畢,最最核心的還是要掌握View以及ViewGroup的部分,至於Activity的傳遞大體了解一下流程就可以了。
下一篇我們一起探究一下Activity中setContentView方法究竟做了什么事情。反過來你能更好理解本篇中的某些點。