天天看點

【朝花夕拾】Android自定義View篇之(六)Android事件分發機制(中)從源碼分析事件分發邏輯及經常遇到的一些“詭異”現象

前言

       在上一篇文章【【朝花夕拾】Android自定義View篇之(五)Android事件分發機制(上)Touch三個重要方法的處理邏輯】【下文簡稱(五),請先閱讀完(五)再閱讀本文】,我們通過示例和log來分析了Android的事件分發機制。這些,我們隻是看到了現象,如果要進一步了解事件分發機制,這是不夠的,我們還需要透過現象看本質,去研究研究源碼。本文将從源碼(基于Android API-26)出發,去分析我們上一篇文章中看到的現象,以及其它一些和事件相關的常見問題,如事件是如何傳到View中的?requestDisallowInterceptTouchEvent為什麼失效?view設定了focusable=“false”,為什麼還能觸發點選事件?Touch事件和Click事件誰先誰後?等等!

一、事件的前世今生

       前文中研究事件傳遞是從Activity的dispatchTouchEvent開始的,但是事件的起源肯定不是Activity。因為觸摸事件是觸摸的硬體,是以很明顯事件一定是從底層傳過來的。但是,事件是如何傳遞到View的呢?這一節我們簡單了解一下事件傳遞到Activity的經過。

       首先,我們在ViewInner類的dispatchTouchEvent方法中列印調用棧,看看這個方法的調用流程。

1 //=============ViewInner.java============
2 @Override
3 public boolean dispatchTouchEvent(MotionEvent event) {
4      Log.i("songzheweiwang",Log.getStackTraceString(new Throwable()));
5      return super.dispatchTouchEvent(event);
6 }      

點選ViewInner區域,得到如下log:

1 java.lang.Throwable
 2     at com.example.demos.customviewdemo.ViewInner.dispatchTouchEvent(ViewInner.java:24)
 3     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
 4     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
 5     at com.example.demos.customviewdemo.ViewGroupMiddle.dispatchTouchEvent(ViewGroupMiddle.java:23)
 6     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
 7     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
 8     at com.example.demos.customviewdemo.ViewGroupOuter.dispatchTouchEvent(ViewGroupOuter.java:23)
 9     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
10     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
11     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
12     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
13     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
14     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
15     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
16     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
17     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
18     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
19     at android.view.ViewGroup.dispatchTransformedTouchEvent(ViewGroup.java:3043)
20     at android.view.ViewGroup.dispatchTouchEvent(ViewGroup.java:2727)
21     at com.android.internal.policy.DecorView.superDispatchTouchEvent(DecorView.java:526)
22     at com.android.internal.policy.PhoneWindow.superDispatchTouchEvent(PhoneWindow.java:1830)
23     at android.app.Activity.dispatchTouchEvent(Activity.java:3410)
24     at com.example.demos.customviewdemo.EventDemoActivity.dispatchTouchEvent(EventDemoActivity.java:37)
25     at android.support.v7.view.WindowCallbackWrapper.dispatchTouchEvent(WindowCallbackWrapper.java:68)
26     at com.android.internal.policy.DecorView.dispatchTouchEvent(DecorView.java:475)
27     at android.view.View.dispatchPointerEvent(View.java:12768)
28     at android.view.ViewRootImpl$ViewPostImeInputStage.processPointerEvent(ViewRootImpl.java:5455)
29     at android.view.ViewRootImpl$ViewPostImeInputStage.onProcess(ViewRootImpl.java:5258)
30     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4766)
31     at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4819)
32     at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4785)
33     at android.view.ViewRootImpl$AsyncInputStage.forward(ViewRootImpl.java:4934)
34     at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4793)
35     at android.view.ViewRootImpl$AsyncInputStage.apply(ViewRootImpl.java:4991)
36     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4766)
37     at android.view.ViewRootImpl$InputStage.onDeliverToNext(ViewRootImpl.java:4819)
38     at android.view.ViewRootImpl$InputStage.forward(ViewRootImpl.java:4785)
39     at android.view.ViewRootImpl$InputStage.apply(ViewRootImpl.java:4793)
40     at android.view.ViewRootImpl$InputStage.deliver(ViewRootImpl.java:4766)
41     at android.view.ViewRootImpl.deliverInputEvent(ViewRootImpl.java:7490)
42     at android.view.ViewRootImpl.doProcessInputEvents(ViewRootImpl.java:7448)
43     at android.view.ViewRootImpl.enqueueInputEvent(ViewRootImpl.java:7409)
44     at android.view.ViewRootImpl$WindowInputEventReceiver.onInputEvent(ViewRootImpl.java:7593)
45     at android.view.InputEventReceiver.dispatchInputEvent(InputEventReceiver.java:199)
46     at android.os.MessageQueue.nativePollOnce(Native Method)
47     at android.os.MessageQueue.next(MessageQueue.java:326)
48     at android.os.Looper.loop(Looper.java:165)
49     at android.app.ActivityThread.main(ActivityThread.java:7477)
50     at java.lang.reflect.Method.invoke(Native Method)
51     at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:500)
52     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:865)      

       實際上,Android輸入事件的源頭是位于/dev/input/下的裝置節點,而輸入事件的終點是由WMS管理的某個視窗,最終由視窗中的View處理。最初的輸入事件為核心生成的原始事件,而最終傳遞給視窗的則是KeyEvent(鍵盤)或MotionEvent(滑鼠和觸摸屏)對象。上述log中(從下往上看),用三種顔色标注了3個階段:紅色部分表示底層活動,這裡我們看到了不少熟悉的身影,ZygoteInit,ActivityThread,Native Method等;綠色部分第45行的InputEventReceiver.dispatchInputEvent()方法,事件通過該方法由Native層進入到Java層,并傳遞到Activity中,這裡我們也看到了不少熟悉的身影,ViewRootImpl,DecorView等;從第24行開始,就是我們前面熟悉的,從Acitivty開始調用dispatchTouchEvent一層層來分發事件。可能讀者看到從第20~9行會有疑惑,為什麼中間還有這麼多過程?如果了解Android的View層次結構的話就會知道,在DecorView和開發者定義的布局(如ViewGroupOuter)之間,隔着很多層ViewGroup,也需要一層層傳遞,這一點我在本系列第一篇【【朝花夕拾】Android自定義View篇之(一)View繪制流程】【後面簡稱(一)】的第二節做過講解,不明白的可以先去看看。

       本文的重點是從Activity開始的事件分發,至于前面從底層到Activity的事件流程,咱們這裡做一定的了解即可,有興趣的可以自行研究。

二、事件從Activity到View體系

       在上一篇文章的代碼示例中,我們的Boss——EventDemoActivity類中有如下代碼

1 //=============Boss:EventDemoActivity.java============
 2  @Override
 3  public boolean dispatchTouchEvent(MotionEvent ev) {
 4      Log.i("songzheweiwang", "[EventDemoActivity-->dispatchTouchEvent]ev=" + EventUtil.parseAction(ev.getAction()));
 5      return super.dispatchTouchEvent(ev);
 6  }
 7 
 8  @Override
 9  public boolean onTouchEvent(MotionEvent event) {
10      Log.i("songzheweiwang", "[EventDemoActivity-->onTouchEvent]event=" + EventUtil.parseAction(event.getAction()));
11      return super.onTouchEvent(event);
12  }      

      Activity是事件的起點,dispatchTouchEvent又是整個邏輯中最早分發事件的地方。但是Activity并不是View體系中的一員,那它是怎樣把事件分發到View體系中的呢?追蹤super.dispatchTouchEvent方法,進入到Activity.java的方法中:

1 //==========================Activity.java=======================
 2 /**
 3  * Called to process touch screen events.  You can override this to
 4  * intercept all touch screen events before they are dispatched to the
 5  * window.  Be sure to call this implementation for touch screen events
 6  * that should be handled normally.
 7  *
 8  * @param ev The touch screen event.
 9  *
10  * @return boolean Return true if this event was consumed.
11  */
12 public boolean dispatchTouchEvent(MotionEvent ev) {
13     if (ev.getAction() == MotionEvent.ACTION_DOWN) {
14         onUserInteraction();
15     }
16     if (getWindow().superDispatchTouchEvent(ev)) {
17         return true;
18     }
19     return onTouchEvent(ev);
20 }      

       如注釋中所說,在事件分發到window之前,可以重寫這個方法來攔截所有的螢幕事件,如果事件被消費了,這個方法會傳回true。這個方法咱們隻關注第15~18行。第15行中如果getWindow().superDispatchTouchEvent值為true,表示後續的View體系把事件消費了,那麼執行第16行,直接傳回true了,後面該Activity的onTouchEvent方法就不再執行了。如果其值為false,則表示事件沒有被消費,那該Activity就調用自己的onTouchEvent來消費該事件。這一點,在上一篇文章中多份log都展現了這個結論。當然我們還是需要繼續看看getWindow().superDispatchTouchEvent是如何實作的。

       其主要代碼如下:

1 //=================Activity.java=================
 2 ......
 3 private Window mWindow;
 4 ......
 5 public Window getWindow() {
 6    return mWindow;
 7 }
 8 ......
 9 
10 //================Window.java================
11 /**
12  * ......
13  * <p>The only existing implementation of this abstract class is
14  * android.view.PhoneWindow, which you should instantiate when needing a
15  * Window.
16  */
17 public abstract class Window {
18   ......
19     public abstract boolean superDispatchTouchEvent(MotionEvent event);
20   ......
21 }      

superDispatchTouchEvent在Window.java中是個抽象方法,需要在實作類中來實作。如注釋所說,PhoneWindow是該抽象類唯一存在的實作類,是以我們追蹤到PhoneWindow中。

1 //=============PhoneWindow.java==========
 2 ......
 3 // This is the top-level view of the window, containing the window decor.
 4 private DecorView mDecor;
 5 ......
 6 @Override
 7 public boolean superDispatchTouchEvent(MotionEvent event) {
 8     return mDecor.superDispatchTouchEvent(event);
 9 }
10 
11 //===========DecorView.java==========
12 ......
13 public boolean superDispatchTouchEvent(MotionEvent event) {
14     return super.dispatchTouchEvent(event);
15 }
16 ....
17 
18 //========ViewGroup.java=======
19 @Override
20 public boolean dispatchTouchEvent(MotionEvent ev) {
21 ......
22 }      

在本系列第一篇文章【(一)】中,對DecorView做過講解,如果有看過這篇文章,那麼此時看到如上代碼,就不會陌生了。DecorView是整個View體系的根view,它是一個ViewGroup,是以事件流程就進入到ViewGroup中的dispatchTouchEvent中了。通過上述流程,Activity就通過dispatchTouchEvent将事件分發到View體系中來了。

       如果View體系沒有消費掉事件,就會調用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     return false;
17 }      

       這裡注釋說得非常明确了:當它下面的所有view都沒有處理掉螢幕觸摸事件時,Activity中的這個方法會調用。當觸摸事件發生在window邊界以外的地方時,這個方法尤為有用,因為這些地方,沒有view來接收這個事件。【(五)】的第三點中有個模型圖,當觸摸白色的Boss區域時,下面三個區域中的view接收不到觸摸事件,是以最後隻有Activity中的onTouchEvent響應了,【(五)】的第六節中log可以明确這一點。如果您消費了這個事件,傳回true;消費不了,則傳回false,預設的實作,總是傳回false。這一點好了解,Boss嘛,隻安排底下的員工幹活,自己絕不會動手,就算他們幹不了,把事件傳回到Boss這裡,自己也不會處理的。這裡可以改動EventDemoActivity.java中重寫的onTouchEvent方法來驗證一下:

1 @Override
2 public boolean onTouchEvent(MotionEvent event) {
3     boolean b = super.onTouchEvent(event);
4     Log.i("songzheweiwang", "[EventDemoActivity-->onTouchEvent]event=" + EventUtil.parseAction(event.getAction())+";b="+b);
5     return b;
6 }      

最終發現,列印的log中,b值總是false。

       到這裡,Activity中的兩個重要方法就講完了,後面再來看看事件從Activity分發到View體系後,事件流程是如何在其中一層層分發和處理的。

三、葉子View的事件處理邏輯

       控件分為兩種:一種是容器類控件,即繼承自ViewGroup,如LinearLayout等;一種是葉子View,View的派生類,如TextView,Button等。與之對應,處理事件分發也分為兩種情形:一種是ViewGroup處理事件分發;另一種是View處理事件邏輯。ViewGroup會遞歸周遊子View,如果是葉子View也會調用到View中的dispatchTouchEvent方法。是以,這裡咱們先從葉子View處理事件開始分析。

  1、葉子View處理事件常見的若幹場景

    (1)onTouch、onTouchEvent和onClick方法執行順序問題

       我們工作中經常需要為控件設定監聽Touch事件和Click事件。這裡對【(五)】中的執行個體代碼做一點改動,為ViewInner控件添加觸摸事件和點選事件,如下所示:

1 //===========EventDemoActivity========== 
 2 ViewInner mViewInner = findViewById(R.id.viewInner);
 3 mViewInner.setOnClickListener(new View.OnClickListener() {
 4     @Override
 5     public void onClick(View v) {
 6         Log.i("songzheweiwang-2", "[EventDemoActivity-->onclick]");
 7     }
 8 });
 9 mViewInner.setOnTouchListener(new View.OnTouchListener() {
10     @Override
11     public boolean onTouch(View v, MotionEvent event) {
12         Log.i("songzheweiwang-2", "[EventDemoActivity-->onTouch]event="+EventUtil.parseAction(event.getAction()));
13         return false;
14     }
15 });      

點選ViewInner控件後得到如下log:

1 06-14 01:19:59.246 25518-25518/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_DOWN
2 06-14 01:19:59.246 25518-25518/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
3 06-14 01:19:59.277 25518-25518/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_UP
4 06-14 01:19:59.277 25518-25518/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP
5 06-14 01:19:59.278 25518-25518/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onclick]      

我們發現,從執行順序上看,onTouch > onTouchEvent > onClick,并且onTouch方法執行了兩次,onClick在ACTION_UP之後才被執行。

    (2)onTouch傳回true的場景

       另外,我們發現,oTouch是一個boolean型方法,預設傳回的是false。如果将其傳回值該為true,結果又怎樣呢?

1 06-14 01:59:00.487 25760-25760/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_DOWN
2 06-14 01:59:00.492 25760-25760/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_UP      

列印log發現,onTouchEvent和onClick方法都沒有執行。

    (3)控件設定clickable=“false”的場景

       在【(五)】中我們講過,如果控件的clickable屬性為false,那麼它是無法消費觸摸事件的。這裡加上Touch和Click事件後,情形如何呢?

1 06-14 02:15:50.279 26161-26161/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_DOWN
2 06-14 02:15:50.279 26161-26161/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
3 06-14 02:15:50.328 26161-26161/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onTouch]event=ACTION_UP
4 06-14 02:15:50.328 26161-26161/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP
5 06-14 02:15:50.330 26161-26161/com.example.demos I/songzheweiwang-2: [EventDemoActivity-->onclick]      

這裡發現和clickable為true時列印的log一樣,也就是說,此時clickable=“false”失效了,這是什麼原因呢?

    (4)控件設定setEnable(false)時的場景

       預設情況下,控件都是enable的,如果給控件執行setEnable(false),結果會怎樣呢?

1 06-14 02:19:56.369 26359-26359/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
2 06-14 02:19:56.401 26359-26359/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP      

發現onTouch和onClick都沒有執行。為什麼?

       上面4種場景是工作中經常會碰到的與事件相關的場景,下面我們從源碼的角度來分析其原因。

2、View.dispatchTouchEvent方法

       View類中用于處理事件分發邏輯的兩個主要方法是dispatchTouchEvent和onTouchEvent,前者是事件分發到該控件時最先執行的方法,先分析這個方法。

1 //=========================View.java==================
 2 /**
 3  * Pass the touch screen motion event down to the target view, or this
 4  * view if it is the target.
 5  * ......
 6  * @return True if the event was handled by the view, false otherwise.
 7  */
 8 public boolean dispatchTouchEvent(MotionEvent event) {
 9     ......
10     boolean result = false;
11     ......
12     if (onFilterTouchEventForSecurity(event)) {
13         ......
14         //noinspection SimplifiableIfStatement
15         ListenerInfo li = mListenerInfo;
16         if (li != null && li.mOnTouchListener != null
17                 && (mViewFlags & ENABLED_MASK) == ENABLED
18                 && li.mOnTouchListener.onTouch(this, event)) {
19             result = true;
20         }
21         if (!result && onTouchEvent(event)) {
22             result = true;
23         }
24     }
25     ......
26     return result;
27 }      

先看注釋:将螢幕動作事件傳遞給目标view,如果自己就是這個目标則将事件傳給自己。如果這個事件被該目标view處理了,會傳回true,否則會傳回false。當ViewInner被觸摸時,會執行到這裡。

       第11行有個if語句判斷,它的作用是為了安全判斷目前TouchEvent是否被過濾掉了,比如該控件不在最頂部,被其它控件遮住了等情形。如果被過濾了,該方法就會傳回false,事件沒有被消費。我們前面的幾種場景下,該函數傳回的都是true。第16~18行中有多個判斷條件,我們來看看這些條件:

1 //============View.java===========
 2 static class ListenerInfo {
 3     ......
 4     private OnTouchListener mOnTouchListener;
 5     ......
 6 }
 7 
 8 /**
 9  * Register a callback to be invoked when a touch event is sent to this view.
10  * @param l the touch listener to attach to this view
11  */
12 public void setOnTouchListener(OnTouchListener l) {
13     getListenerInfo().mOnTouchListener = l;
14 }      

       前面在EventDemoActivity類中ViewInner調用了該方法,并設定了OnTouchListener,是以li.mOnTouchListener的值這裡不是null。(mViewFlags & ENABLED_MASK) == ENABLED表示該view是enable的,預設情況下控件都是enable;li.mOnTouchListener.onTouch(this, event),就是我們EventDemoActivity中ViewInner的onTouch方法的傳回值,預設是傳回falsle。是以會跳出該if判斷,進入到第21行。由于前面的if條件為false,是以這裡result的值還是false,後面那個條件就開始執行onTouchEvent方法了。如果事件被處理了,onTouchEvent會傳回true,傳回的result值也就為true了,這個事件就完美處理掉了;如果沒有處理掉,onTouchEvent就傳回false,那傳回值result自然就是false了,事件沒有被處理掉。是以此時,onTouchEvent方法都是執行了的。如果在onTouch方法中傳回了true,那麼第16行的判斷條件就是true了,此時result被指派為true,那麼第21行的onTouchEvent方法就沒有機會執行了,這就是咱們前面說的第(2)種場景“onTouch傳回true的場景”了。這種情形下,dispatchTouchEvent傳回true,事件也被處理掉了。這裡給一個結論:當控件是enable的時候,如果onTouch事件傳回true,onTouchEvent和onClick方法均不會執行,且事件仍會被消費。下面,我們再看看onTouchEvent方法的源碼。

  3、View.onTouchEvent方法

       方法很長,這裡選取比較關鍵的代碼進行分析。

1 /**
 2  * Implement this method to handle touch screen motion events.
 3  * ......
 4  * @return True if the event was handled, false otherwise.
 5  */
 6 public boolean onTouchEvent(MotionEvent event) {
 7     ......
 8     final int action = event.getAction();
 9     final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
10             || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
11             || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
12     if ((viewFlags & ENABLED_MASK) == DISABLED) {
13         if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
14             setPressed(false);
15         }
16         mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
17         // A disabled view that is clickable still consumes the touch
18         // events, it just doesn't respond to them.
19         return clickable;
20     }
21     ......
22     if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
23         switch (action) {
24             case MotionEvent.ACTION_UP:
25                 ......
26                 // Use a Runnable and post this rather than calling
27                 // performClick directly. This lets other visual state
28                 // of the view update before click actions start.
29                 if (mPerformClick == null) {
30                     mPerformClick = new PerformClick();
31                 }
32                 if (!post(mPerformClick)) {
33                     performClick();
34                 }
35                 ......
36                 break;
37             case MotionEvent.ACTION_DOWN:
38                 ......
39                 break;
40             case MotionEvent.ACTION_CANCEL:
41                 ......
42                 break;
43             case MotionEvent.ACTION_MOVE:
44                 ......
45                 break;
46         }
47         return true;
48     }
49     return false;
50 }      

 看看注釋:實作該方法來處理螢幕觸摸事件,如果處理了就傳回true,否則傳回false。這裡需要對比一下dispatchTouchEvent的注釋,它的關鍵字是“pass”,而onTouchEvent的關鍵字是“handle”,這裡可以差別兩者的作用。

       第9行中的clickable值,前說過,有的控件預設是true,有的則是false。控件的flag為可以一般點選和長按,都表示是可點選的。第12行中,如果控件為disable的,即調用了setEnable(false)時,這裡就return了,該方法後面的流程就結束了。如果該控件是可點選的,即clickable的值為true,那onTouchEvent傳回的就是true,事件仍然被消費了;反之,控件不可點選,那傳回的就是false,事件沒有被消費,傳遞到上一級的onTouchEvent方法中,這一點我們在【(五)】中有講過。第17、18行的注釋也明确說明了:一個可以點選的disable的控件,仍然可以消費觸摸事件,隻不過它不能回應這些控件。

       第22行的第二個條件,tooltip的說明如下:

1 //=========================View.java====================
2 1 /**
3 2  * <p>Indicates this view can display a tooltip on hover or long press.</p>
4 3  * {@hide}
5 4  */
6 5 static final int TOOLTIP = 0x40000000;      

這是一種比較特殊的用途,這裡不繼續深究,我們平時的使用以及這裡demo中沒有使用該功能。是以咱們這裡隻考慮第一個條件。

       能夠走到第22行,說明該控件是enable的。如果該控件是不可點選的,那麼這個if語句就跳過去了,傳回false,此時前面的dispatchTouchEvent方法傳回的也一定是false了,說明該控件沒有消費掉事件。相反,如果是可點選的,可以看到,後面一定是會傳回true的,那麼事件就能被消費了。到這裡,結合第19行和dispatchTouchEvent方法,我們可以得到一個結論:如果控件是clickable且沒有被過濾的,那麼這個控件一定是可以消費掉事件的。在ACTION_UP的時候,會走到第29行。這裡我們需要關注一下PerformClick類,其源碼如下:

1 private final class PerformClick implements Runnable {
2     @Override
3     public void run() {
4         performClick();
5     }
6 }      

而第32行的post方法,本質上就是一個Handler發的post,是以第32、33行是一定要執行performClick()方法的。我們着重看一看這個方法:

1 //====================View.java================
 2 /**
 3  * Call this view's OnClickListener, if it is defined.  Performs all normal
 4  * actions associated with clicking: reporting accessibility event, playing
 5  * a sound, etc.
 6  *
 7  * @return True there was an assigned OnClickListener that was called, false
 8  *         otherwise is returned.
 9  */
10 public boolean performClick() {
11     final boolean result;
12     final ListenerInfo li = mListenerInfo;
13     if (li != null && li.mOnClickListener != null) {
14         playSoundEffect(SoundEffectConstants.CLICK);
15         li.mOnClickListener.onClick(this);
16         result = true;
17     } else {
18         result = false;
19     }
20     ......  
21     return result;
22 }      

這個方法就是用來執行點選相關的工作的,第13行播放點選音效,第14行響應點選事件等。

1 //===============View.java=============
 2 static class ListenerInfo {
 3     ......
 4     public OnClickListener mOnClickListener;
 5     ......
 6 }
 7 ......
 8 /**
 9  * Register a callback to be invoked when this view is clicked. If this view is not
10  * clickable, it becomes clickable.
11  *
12  * @param l The callback that will run
13  *
14  * @see #setClickable(boolean)
15  */
16 public void setOnClickListener(@Nullable OnClickListener l) {
17     if (!isClickable()) {
18         setClickable(true);
19     }
20     getListenerInfo().mOnClickListener = l;
21 }      

我們在EventDemoActivity中ViewInner通過該方法設定了OnClickListener,是以EventDemoActivity中的onClick方法在這裡就被調用了,此時點選事件就産生了。我們可以看到,點選事件是發生在onTouchEvent方法的ACTION_UP事件中的。這裡結合前面講的onTouch方法,就可以解釋場景(1)“onTouch、onTouchEvent和onClick方法執行順序問題”了:onTouch > onTouchEvent > onClick,并且要在ACTION_UP後才會有onClick。在setOnClickListener方法中第17、18行,我們還可以看到,如果這個控件本身不是clickable的,也會先讓它變成clickable的。是以,隻要控件調用了setOnClickListener方法,那麼之前設定的clickable=“false”就會失效。這裡就解釋了場景(3)“控件設定clickable=“false”的場景”。此時,我們傳回去看看onTouchEvent方法的第19行,就會發現,即使控件是enable的,由于我們設定了點選事件監聽,是以這裡是傳回true的,事件仍然被消費了,再結合前面dispatchTouchEvent的第17行,就能夠解釋場景(4)“控件設定setEnable(false)時的場景”下,沒有執行onTouch和onClick方法,但onTouchEvent方法仍然消費了事件。

        這裡對關鍵的源碼進行了詳細的講解,且對前面提到的幾種常見的現象,從源碼角度進行了解釋,希望通過這些來加深對View事件分發的認識和了解。關于View中事件的分發,要講的就是這些了。

四、ViewGroup事件分發處了解析

       ViewGroup中處理事件邏輯的方法有兩個,分别是dispatchTouchEvent和onInterceptTouchEvent。當一個ViewGroup控件收到上一級傳遞來的事件時,也是先經過dispatchTouchEvent處理。咱們先看看該方法的源碼,該方法很長,這裡僅截取了其中關鍵流程的代碼:

1 //=========================ViewGroup.java========================
 2 @Override
 3 public boolean dispatchTouchEvent(MotionEvent ev) {
 4     ......
 5     boolean handled = false;
 6     //該判斷條件在View的dispatchTouchEvent部分講過,這個方法用于判斷控件是否被遮擋。
 7     if (onFilterTouchEventForSecurity(ev)) {
 8         final int action = ev.getAction();
 9         final int actionMasked = action & MotionEvent.ACTION_MASK;
10         // Handle an initial down.
11         if (actionMasked == MotionEvent.ACTION_DOWN) {
12             // Throw away all previous state when starting a new touch gesture.
13             // The framework may have dropped the up or cancel event for the previous gesture
14             // due to an app switch, ANR, or some other state change.
15             /**當開始一個新的觸摸時,這兩句會清除掉以前的狀态,
16              * 清除FLAG_DISALLOW_INTERCEPT設定,
17              * 代碼中還會執行mFirstTouchTarget = null
18              */
19             cancelAndClearTouchTargets(ev);
20             resetTouchState();
21         }
22         // Check for interception.
23         final boolean intercepted;
24         /**事件由子View去處理時mFirstTouchTarget會指派并指向子View。
25          * mFirstTouchTarget != null表示事件由子View來處理;
26          * mFirstTouchTarget = null表示事件由自己處理
27          */
28         if (actionMasked == MotionEvent.ACTION_DOWN
29                 || mFirstTouchTarget != null) {
30             /**FLAG_DISALLOW_INTERCEPT是子View通過requestDisallowInterceptTouchEvent(true)來設定的
31              * disallowIntercept等同于表示是否調用了該方法。
32              */
33             final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
34             if (!disallowIntercept) {
35                 //子View通過onInterceptTouchEvent來判斷是否需要攔截。
36                 intercepted = onInterceptTouchEvent(ev);
37                 ev.setAction(action); // restore action in case it was changed
38             } else {
39                 intercepted = false;
40             }
41         } else {
42             // There are no touch targets and this action is not an initial down
43             // so this view group continues to intercept touches.
44             //沒有找到處理事件的子View,并且這個action不是ACTION_DOWN,是以該View開始攔截觸摸事件。
45             intercepted = true;
46         }
47         ......
48         //如果事件沒有取消且沒有被攔截
49         if (!canceled && !intercepted) {
50             ......
51             //尋找能夠接收該事件的View
52             for (int i = childrenCount - 1; i >= 0; i--) {
53                 ......
54                 final View child = getAndVerifyPreorderedView(
55                         preorderedList, children, childIndex);
56                 ......
57                 /**這兩個判斷條件分别為1、View可見并且沒有播放動畫;
58                  * 2、點選事件的坐标在View的範圍内。
59                  * 如果其中一個條件不滿足,就進行下一次循環。
60                  */
61                 if (!canViewReceivePointerEvents(child)
62                         || !isTransformedTouchPointInView(x, y, child, null)) {
63                     ev.setTargetAccessibilityFocus(false);
64                     continue;
65                 }
66                 ......
67                 //true表示找到了子View來處理事件,false表示沒有子View來處理。
68                 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign))
69                     // Child wants to receive touch within its bounds.
70                     ......
71                     //addTouchTarget方法中,會給mFirstTouchTarget指派。
72                     newTouchTarget = addTouchTarget(child, idBitsToAssign);
73                     ......
74                     break;
75                 }
76                ......
77             }
78         }
79         //沒有找到可以處理事件的子View,需要自身來處理。
80         if (mFirstTouchTarget == null) {
81             // No touch targets so treat this as an ordinary view.
82             handled = dispatchTransformedTouchEvent(ev, canceled, null,
83                     TouchTarget.ALL_POINTER_IDS);
84         } else {
85             ......
86             //這裡有一部分源碼暫時沒有讀懂
87         }
88         ......
89     }
90     ......
91     return handled;
92 }      

代碼中梳理出了關鍵的流程,并對幾處要點做了一些注釋說明,咱們這裡再解讀一下這段代碼:

    (1)第11行中,當一個新的事件開始時,即ACTION_DOWN觸發時,會進行初始化,清理掉之前的一些狀态,這一點很容易了解。

    (2)第24~27行對字段mFirstTouchTarget進行了簡單的說明,這裡不再贅述了,後面幾個關鍵點需要用到這個字段來進行判斷。

    (3)第28行,如果是ACTION_DOWN事件或者其它事件下找到了處理事件的子View時,會進入到該方法中。

    (4)第30~33行,主要是FLAG_DISALLOW_INTERCEPT相關。這裡需要簡單介紹一下requestDisallowInterceptTouchEvent(boolean disallowIntercept)方法,該方法用于子View請求父View,不讓父View攔截,即讓父View的onInterceptTouchEvent方法傳回true時,失效。在沒有調用該方法時,預設其參數為false,即requestDisallowInterceptTouchEvent(false)效果等同于沒有調用該方法。FLAG_DISALLOW_INTERCEPT的标志位就是通過該方法來進行設定的。該方法對除ACTION_DOWN之外的事件有效,對ACTION_DOWN事件無效,在第12~20行中講過,ACTION_DOWN時,會清除之前狀态,FLAG_DISALLOW_INTERCEPT标志位也被恢複。這裡簡單示範一下該方法的使用:

       首先在ViewGroupMiddle類中修改onInterceptTouchEvent為以下代碼,ViewInner中先不做修改,且為了做對比,要讓ViewInner添加clickable=“true”,能夠消費事件:

1 //===========ViewGroupMiddle.java===========
 2 @Override
 3 public boolean onInterceptTouchEvent(MotionEvent ev) {
 4     Log.i("songzheweiwang-2", "[ViewGroupMiddle-->onInterceptTouchEvent]ev=" + EventUtil.parseAction(ev.getAction()));
 5     switch (ev.getAction()) {
 6         case MotionEvent.ACTION_DOWN:
 7             return false;
 8         default:
 9             return true;
10     }
11 }      

檢視log為:

06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_DOWN
06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->onInterceptTouchEvent]ev=ACTION_DOWN
06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_DOWN
06-12 00:21:27.800 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_UP
06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->onInterceptTouchEvent]ev=ACTION_UP
06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_CANCEL
06-12 00:21:27.832 6906-6906/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_CANCEL      

ACTION_DOWN沒有被攔截,正常傳遞到了ViewInner中,而其他事件卻被攔截了。這裡還可以看到,一個完整的事件,如果隻有ACTION_DOWN,卻沒有ACTION_UP,最終會觸發ACTION_CANCEL。

       現在在ViewInner類的onTouchEvent方法中調用getParent().requestDisallowInterceptTouchEvent(true)方法,如下所示:

1 //=============ViewInner.java=============
2 @Override
3 public boolean onTouchEvent(MotionEvent event) {
4     Log.i("songzheweiwang-2","[ViewInner-->onTouchEvent]event="+EventUtil.parseAction(event.getAction()));
5     getParent().requestDisallowInterceptTouchEvent(true);
6    return super.onTouchEvent(event);
7 }      

之是以要在ViewGroup類中不攔截ACTION_DOWN事件,是因為如果這裡攔截了,ViewInner中的方法就不會執行了,第5行的代碼也自然起不到效果。得到的log如下:

1 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_DOWN
2 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->onInterceptTouchEvent]ev=ACTION_DOWN
3 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_DOWN
4 06-12 00:37:24.505 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_DOWN
5 06-12 00:37:24.524 7509-7509/com.example.demos I/songzheweiwang-2: [ViewGroupMiddle-->dispatchTouchEvent]ev=ACTION_UP
6 06-12 00:37:24.524 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->dispatchTouchEvent]event=ACTION_UP
7 06-12 00:37:24.524 7509-7509/com.example.demos I/songzheweiwang-2: [ViewInner-->onTouchEvent]event=ACTION_UP      

可以看到,ACTION_UP事件在ViewInner中正常執行了。是以在第33行的disallowIntercept變量值,在調用該方法且目前事件不是ACTION_DOWN時(因為會清除掉該方法的效果),其值為true,反之則為false。

        該方法在日常解決事件沖突時經常會用到,是以這裡花了一點篇幅來講解了一下該方法。

    (5)第36行中,調用了onInterceptTouchEvent方法。這個方法我們在【(五)】中提到無數遍了,這裡總算看到了它的身影。自然,這裡一定要去看看其源碼。

1 //=================ViewGroup.java=============
 2 public boolean onInterceptTouchEvent(MotionEvent ev) {
 3     if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
 4             && ev.getAction() == MotionEvent.ACTION_DOWN
 5             && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
 6             && isOnScrollbarThumb(ev.getX(), ev.getY())) {
 7         return true;
 8     }
 9     return false;
10 }      

What?這麼簡單?if語句看起來是和滑鼠有關系的,貌似和咱們的示例及平時使用沒有半毛錢的關系,是以除去這個判斷語句,其實就隻有第8行的代碼了。這裡,在【(五)】中講過,對于重寫的onInterceptTouchEvent方法,傳回false和傳回預設的super.onInterceptTouchEvent,結果是一樣的。

    (6)第28~46行就确定了是否需要對事件進行攔截了。這裡也可以明确看到,onInterceptTouchEvent方法,不一定每次都會被調用了。這裡我們在【(五)】中也多次看到這種情形。

    (7)第49行中,我們關注後面這個條件,在沒有被攔截的情況下才會進入這個if語句内部。它的主要作用就是遞歸周遊其子view,找到可以接收該事件的view。這裡重點需要關注的是第68行的dispatchTransformedTouchEvent方法。這個方法我們在第一節的log中就多次看到過,在ViewGroup中它和dispatchTouchEvent方法總是成對出現的。它的源碼如下:

1 //===============ViewGroup.java============== 
 2 private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
 3             View child, int desiredPointerIdBits) {
 4      final boolean handled;
 5      ......
 6      //transformedEvent是對event的加工處理
 7      if (child == null) {
 8             handled = super.dispatchTouchEvent(transformedEvent);
 9         } else {
10            ......
11             handled = child.dispatchTouchEvent(transformedEvent);
12      }
13      return handle;
14 }      

這裡傳入的child參數是有值的,不是null,是以會調用第10行,進行遞歸。當周遊到葉子View的時候,就跳轉到View類中的diapatchTouchEvent方法中了,我們前面已經講解過了。是以,如果找到了處理事件的view了,handle就是true,該方法傳回true,就會跳出for循環,不再需要便利了,否則繼續便利。這裡面有個addTouchTarget方法,其中會對mFirstTouchTarge進行指派,說明找到了處理事件的子View,如果沒找到,這個值會一直為null。

    (8)第79~83行,既然前面沒有找到可以處理該事件的子view,那麼就隻能自己來接手該事件了。第82行,第三個參數傳入的是nul,是以該方法體中就會執行第7行。ViewGroup的父類是View,是以會走到View類中,走View處理事件的邏輯了,這裡也需要注意,不要混淆父類和父控件了。而如果子View能夠處理該事件,那後面目前ViewGroup控件就和這個事件沒啥關系了。現在,我們也就能夠解釋【(五)】中的很多現象了,如:ViewInner無法消費掉事件時,ViewGroupMiddle會回調自身的onTouchEvent方法;如果ViewInner消費了目前的事件,那麼本次事件就不會再傳回到上級控件了。

    (9)最後該方法的傳回值仍然是,如果傳回true,那麼表示事件被消費了,否則傳回false。

       ViewGroup類中重點需要關注的方法就講完了,其實主要就是dispatchTouchEvent方法的邏輯。現在我們應該可以了解,在【(五)】在各種現象和結論了吧。

五、參考文章

       【Android View 事件分發機制】

       【一文讀懂Android View事件分發機制】

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

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

結語

       Android的View事件傳遞機制的源碼分析,到這裡就講完了,不知道本文中提到的一些場景,是不是也曾經困擾過讀者呢?當然,筆者這裡隻是抓取了主要的關鍵流程和方法進行了講解,真實的源碼邏輯遠遠比這要複雜,因為平時會碰到各種不同的情況,比如長按,tip功能的邏輯處理等。總的來說,本文還隻不過是登堂入室的開始而已,其中如果有不妥或者不正确的地方,歡迎來拍磚。