Android開發藝術探索知識回顧——第3章 View的事件體系:3、View的事件分發機制、滑動衝突

 

 

3.4 View的事件分發機制

上面幾節介紹了 View 的基礎知識以及 View 的滑動,本節將介紹 View 的一個核心知識點:事件分發機制。事件分發機制不僅僅是核心知識點更是難點,不少初學者甚至中級開發者面對這個問題時都會覺得困惑。

另外,View 的另一大難題滑動衝突,它的解決方法的理論基礎就是事件分發機制,因此掌握好 View 的事件分發機制是十分重要的。本節將深入介紹 View 的事件分發機制,在 3.4.1 節會對事件分發機制進行概括性地介紹,而在 3.4.2 節將結合系統源碼去進一步分析事件分發機制。

 

3.4.1 點擊事件的傳遞規則

在介紹點擊事件的傳遞規則之前,首先我們要明白這裏要分析的對象就是 MotionEvent即點擊事件,關於 MotionEvent 3.1節中已經進行了介紹。

 

何爲點擊事件的事件分發?

所謂點擊事件的事件分發,其實就是對 MotionEvent 事件的分發過程,即當一個 MotionEvent 產生了以後,系統需要把這個事件傳遞給一個具體的 View,而這個傳遞的過程就是分發過程。

 

點擊事件分發過程的三個重要方法

點擊事件的分發過程由三個很重要的方法來共同完成:dispatchTouchEventonlnterceptTouchEvent onTouchEvent。下面我們先介紹一下這幾個方法。

public boolean dispatchTouchEvent(MotionEvent ev)

用來進行事件的分發。如果事件能夠傳遞給當前 View,那麼此方法一定會被調用,返回結果受當前 View 的 onTouchEvent 和下級 View dispatchTouchEvent 方法的影響,表示是否消耗當前事件。

public boolean onInterceptTouchEvent(MotionEvent event)

在上述方法內部調用,用來判斷是否攔截某個事件,如果當前 View 攔截了某個事件,那麼在同一個事件序列當中,此方法不會被再次調用,返回結果表示是否攔截當前事件。

public boolean onTouchEvent(MotionEvent event)

在 dispatchTouchEvent 方法中調用,用來處理點擊事件,返回結果表示是否消耗當前事件,如果不消耗,則在同一個事件序列中,當前View 無法再次接收到事件。

上述三個方法到底有什麼區別呢?它們是什麼關係呢?其實它們的關係可以用如下僞代碼表示:

public boolean dispatchTouchEvent(MotionEvent ev) {
      boolean consume = false;
      if (onInterceptTouchEvent(ev)) {
          consume = onTouchEvent(ev);

      } else {
          consume = child.dispatchTouchEvent(ev);
     }

     return consume;
}

 

通過僞代碼,大致瞭解點擊事件的傳遞規則

上述僞代碼已經將三者的關係表現得淋漓盡致。通過上面的僞代碼,我們也可以大致瞭解點擊事件的傳遞規則:對於一個根 ViewGroup 來說,點擊事件產生後,首先會傳遞給它,這時它的 dispatchTouchEvent 就會被調用,如果這個 ViewGroup onlnterceptTouchEvent 方法返回 true,就表示它要攔截當前事件,接着事件就會交給這個 ViewGroup 處理,即它的 onTouchEvent 方法就會被調用;

如果這個 ViewGroup 的 onlnterceptTouchEvent 方法返回 false,就表示它不攔截當前事件,這時當前事件就會繼續傳遞給它的子元素,接着子元素的 dispatchTouchEvent 方法就會被調用,如此反覆直到事件被最終處理。

 

當 View 設置了 OnTouchListener

當一個 View 需要處理事件時,如果它設置了 OnTouchListener,那麼 OnTouchListener 中的 onTouch 方法會被回調。這時事件如何處理還要看 onTouch 的返回值,如果返回false,則當前 View 的 onTouchEvent 方法會被調用;如果返回true,那麼 onTouchEvent 方法將不 會被調用。

由此可見,給 View 設置的 OnTouchListener,其優先級比 onTouchEvent 要高。 在 onTouchEvent 方法中,如果當前設置的有OnClickListener,那麼它的 onClick 方法會被調用。可以看出,平時我們常用的 OnClickListener,其優先級最低,即處於事件傳遞的尾端。

 

當一個點擊事件產生後,它的傳遞過程遵循的順序

當一個點擊事件產生後,它的傳遞過程遵循如下順序:Activity -> Window -> View,即事件總是先傳遞給 Activity,Activity 再傳遞給Window,最後 Window 再傳遞給頂級 View頂級 View 接收到事件後,就會按照事件分發機制去分發事件。

考慮一種情況,如果一個 View 的 onTouchEvent 返回 false,那麼它的父容器的 onTouchEvent 將會被調用,依此類推。 如果所有的元素都不處理這個事件,那麼這個事件將會最終傳遞給 Activity 處理,即 Activity 的 onTouchEvent 方法會被調用。

這個過程其實也很好理解,我們可以換一種思路,假如點擊事件是一個難題,這個難題最終被上級領導分給了一個程序員去處理(這是事件分發過程),結果這個程序員搞不定onTouchEvent 返回了 false),現在該怎麼辦呢?難題必須要解決,那隻能交給水平更高的上級解決(上級的 onTouchEvent 被調用),如果上級再搞不定,那隻能交給上級的上級去解決,就這樣將難題一層層地向上拋,這是公司內部一種很常見的處理問題的過程。從這個角度來看,View 的事件傳遞過程還是很貼近現實的,畢竟程序員也生活在現實中。

 

關於事件傳遞機制的一些結論

關於事件傳遞的機制,這裏給出一些結論,根據這些結論可以更好地理解整個傳遞機制,如下所示。

1)同一個事件序列是指:從手指接觸屏幕的那一刻起,到手指離開屏幕的那一刻結束,在這個過程中所產生的一系列事件,這個事件序列以 down 事件開始,中間含有數量不定的 move 事件,最終以 up 事件結束。

(2)正常情況下,一個事件序列只能被一個 View 攔截且消耗。這一條的原因可以參考(3)因爲一旦一個元素攔截了某此事件,那麼同一個事件序列內的所有事件都會直接交給它處理,因此同一個事件序列中的事件不能分別由兩個 View 同時處理,但是通過特殊手段可以做到,比如一個 View 將本該自己處理的事件通過 onTouchEvent 強行傳遞給其他 View 處理。

(3)某個 View 一旦決定攔截,那麼這一個事件序列都只能由它來處理 ( 如果事件序列能夠傳遞給它的話),並且它的onlnterceptTouchEvent 不會再被調用。這條也很好理解,就是說當一個 View 決定攔截一個事件後,那麼系統會把同一個事件序列內的其他方法都直接交給它來處理,因此就不用再調用這個 View 的 onlnterceptTouchEvent 去詢問它是否要攔截了。

(4)某個 View 一旦開始處理事件,如果它不消耗 ACTION_DOWN 事件 ( onTouchEvent 返回了 false),那麼同一事件序列中的其他事件都不會再交給它來處理,並且事件將重新交由它的父元素去處理,即父元素的 onTouchEvent 會被調用。意思就是事件一旦交給一個 View處理,那麼它就必須消耗掉,否則同一事件序列中剩下的事件就不再交給它來處理了,這就好比上級交給程序員一件事,如果這件事沒有處理好,短期內上級就不敢再把事情交給這個程序員做了,二者是類似的道理。

(5)如果 View 不消耗除 ACTION_DOWN 以外的其他事件,那麼這個點擊事件會消失,此時父元素的 onTouchEvent 並不會被調用,並且當前 View 可以持續收到後續的事件,最終這些消失的點擊事件會傳遞給 Activity 處理。

(6)ViewGroup 默認不攔截任何事件。Android 源碼中 ViewGroup onlnterceptTouchEvent 方法默認返回 false。

(7)View 沒有 onlnterceptTouchEvent 方法,一旦有點擊事件傳遞給它,那麼它的 onTouchEvent 方法就會被調用。

(8)View 的 onTouchEvent 默認都會消耗事件 ( 返回 true ),除非它是不可點擊的 ( clickable longClickable 同時爲 false )。 View longClickable 屬性默認都爲 false,clickable 屬性要分情況,比如 Button 的 clickable 屬性默認爲 true,而 TextView 的 clickable 屬性默認爲 false。

(9)View 的 enable 屬性不影響 onTouchEvent 的默認返回值。哪怕一個 View 是 disable 狀態的,只要它的 clickable 或者 longClickable有一個爲 true,那麼它的 onTouchEvent 就返回 true。

(10)onClick 會發生的前提是當前 View 是可點擊的,並且它收到了 down 和 up 的事件。

(11)事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,然後再由父元素分發給子 View,通過requestDisallowInterceptTouchEvent 方法可以在子元素中干預父元素的事件分發過程,但是 ACTION_DOWN 事件除外。

 

3.4.2 事件分發的源碼解析

上一節分析了 View 的事件分發機制,本節將會從源碼的角度去進一步分析、證實上面的結論

1、Activity對點擊事件的分發過程

點擊事件用 MotionEvent 來表示,當一個點擊操作發生時,事件最先傳遞給當前 Activity,由 Activity 的 dispatchTouchEvent 來進行事件派發,具體的工作是由 Activity 內部的 Window 來完成的

Window 會將事件傳遞給 decor view,decor view 一般就是當前界面的底層容器 ( 即 setContentView 所設置的 View 的父容器 ),通過Activity.getWindow.getDecorView() 可以獲得。我們先從 Activity dispatchTouchEvent 開始分析。

 

源碼:Activity#dispatchTouchEvent

 public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

現在分析上面的代碼。首先事件開始交給 Activity 所附屬的 Window 進行分發,如果返回 true,整個事件循環就結束了,返回 false 意味着事件沒人處理,所有 View onTouchEvent 都返回了 false,那麼 Activity onTouchEvent 就會被調用。

 

Window 是如何將事件傳遞給 ViewGroup 

接下來看 Window 是如何將事件傳遞給 ViewGroup 的。通過源碼我們知道,Window是個抽象類,而Window的 superDispatchTouchEvent方法也是個抽象方法,因此我們必須找到 Window 的實現類才行。

 

源碼:Window#superDispatchTouchEvent

public abstract boolean superDispatchTouchEvent(MotionEvent event);

 

Window 的唯一實現是 PhoneWindow

那麼到底 Window 的實現類是什麼呢?其實是 PhoneWindow,這一點從 Window 的源碼中也可以看出來,在 Window 的說明中,有這麼一段話:

Abstract base class for a top-level window look and behavior policy. 
An instance of this class should be used as the top-level view added to the window manager. 
It provides standard UI policies such as a background, title area, default key processing, etc.

The only existing implementation of this abstract class is android.
policy.PhoneWindow, which you should instantiate when needing a Window. 
Eventually that class will be refactored and a factory method added for 
creating Window instances without knowing about a particular implementation.

上面這段話的大概意思是:Window 類可以控制頂級 View 的外觀和行爲策略,它的唯一實現位於 android.policy.PhoneWindow 中,當你要實例化這個 Window 類的時候,你並不知道它的細節,因爲這個類會被重構,只有一個工廠方法可以使用。儘管這看起來有點模糊,不過我們可以看一下 android.policy.PhoneWindow 這個類,儘管實例化的時候此類會被重構,僅是重構而已,功能是類似的。

由於 Window 的唯一實現是 PhoneWindow,因此接下來看一下 PhoneWindow 是如何處理點擊事件的,如下所示。

源碼:PhoneWindow#superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event){
        return mDecor.superDispatchTouchEvent(event);
   }

 

PhoneWindow 傳遞給了 DecorView,DecorView 是什麼

到這裏邏輯就很清晰了,PhoneWindow 將事件直接傳遞給了 DecorView,這個 DecorView 是什麼呢?請看下面:

private final class DecorView extends FrameLayout implements RootViewSurfaceTaker {

    // This is the top-level view of the window, containing the window decor.
    private DecorView mDecor;

    @Override
    public final View getDecorView(){
        if(mDecor == null){
            installDesor():
        }
        return mDecor;
    }
}

我們知道,通過  ((ViewGroup)getWindow().getDecorView().findViewById(android.R.id.content)).getChildAt(0)  這種方式就可以獲取Activity 所設置的 View,這個 mDecor 顯然就是 getWindow().getDecorView() 返回的 View,而我們通過 setContentView 設置的 View 是它的一個子 View

目前事件傳遞到了 DecorView 這裏,由於 DecorView 繼承自 FrameLayout 且是父 View,所以最終事件會傳遞給 View。換句話來說,事件肯定會傳遞到 View,不然應用如何響應點擊事件呢?

不過這不是我們的重點,重點是事件到了 View 以後應該如何傳遞,這對我們更有用。從這裏開始,事件已經傳遞到頂級 View 了,即在Activity 中通過 setContentView 所設置的View另外頂級 View 也叫根 View,頂級 View 一般來說都是 ViewGroup。

 

2、頂級View對點擊事件的分發過程

關於點擊事件如何在 View 中進行分發,上一節已經做了詳細的介紹,這裏再大致回顧一下。點擊事件達到頂級 View ( —般是一個ViewGroup ) 以後,會調用 ViewGroup dispatchTouchEvent 方法,然後的邏輯是這樣的:

如果頂級 ViewGroup 攔截事件,即 onlnterceptTouchEvent 返回 true,則事件由ViewGroup處理。這時如果 ViewGroup mOnTouchListener 被設置,則 onTouch 會被調用,否則 onTouchEvent 會被調用。也就是說,如果都提供的話,onTouch 會屏蔽掉 onTouchEvent。 

在 onTouchEvent 中,如果設置了 mOnClickListener,則 onClick 會被調用。如果頂級 ViewGroup 不攔截事件,則事件會傳遞給它所在的點擊事件鏈上的子 View,這時子 View 的 dispatchTouchEvent 會被調用。到此爲止,事件已經從頂級 View 傳遞給了下一層 View,接下來的傳遞過程和頂級 View 是一致的,如此循環,完成整個事件的分發。

首先看 ViewGroup 對點擊事件的分發過程,其主要實現在 ViewGroup 的 dispatchTouchEvent 方法中,這個方法比較長,這裏分段說明。先看下面一段,很顯然,它描述的是當前 View 是否攔截點擊事情這個邏輯。

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章