徹底掌握Android touch事件分發順序

徹底掌握Android touch事件分發順序

Android touch事件的分發主要由幾個方向可以展開深入分析:

  1. touch事件是如何從驅動層傳遞給Framework層的InputManagerService;
  2. WMS是如何通過ViewRootImpl將事件傳遞到目標窗口;
  3. touch事件到達DecorView後,是如何一步步傳遞到內部的子View中。

其中與上層軟件開發息息相關的就是第三條。

思路梳理

在深入分析事件分發源碼之前,需要先弄清楚2個概念。

ViewGroup

ViewGroup是一組View的組合,在其內部有可能包含多個子View,當手指觸摸屏幕上時,手指所在的區域能在ViewGroup的顯示範圍內,也可能在其內部的View控件上。

因此它內部的事件分發的重心是處理當前Group和子View之間的邏輯關係。

  1. 當前Group是否需要攔截touch事件
  2. 是否需要將touch事件繼續分發給子View;
  3. 如何將touch事件分發給子View。

View

View是一個單純的控件,不能被再細分,內部也不會存在子Veiw,所以它的事件分發的重點在於當前View如何去處理touch事件,並根據相應的手勢邏輯進行一系列的效果展示(比如滑動,放大,點擊,長按)。

  1. 是否存在TouchListener;
  2. 是否自己接受處理touch事件(主要邏輯在onTouchEvent方法中)。

事件分發核心 dispatchTouchEvent

整個View之間的事件分發,實際上就是一個大的遞歸函數,而這個遞歸函數就是dispatchTouchEvent方法。在這個遞歸的過程中會適時調用onInterceptTouchEvent來攔截事件,或者調用onTouchEvent方法處理事件。

先從宏觀角度,縱覽整個dispatch的源碼如下:

public boolean dispatchTouchEvent(){
    /**
     * 步驟1:檢查當前ViewGroup是否需要攔截事件
     */
     ...
    /**
     *	步驟2:將事件分發給子View
     */
     ...
    /*
     * 步驟3:根據mFirstTouchTarget,再次分發事件
     */
     ...
}

如代碼中的註釋,dispatch主要分爲3大步驟:

  • 步驟1:判斷當前ViewGroup是否需要攔截此touch事件,如果攔截則此次touch事件不再會傳遞給子View(或者以CANCEL的方式通知子View
  • 步驟2:如果沒有攔截,則將事件分發給子View繼續處理,如果子View將此次事件捕獲,則將mFirstTouchTarget賦值給捕獲touch事件的VIew。
  • 步驟3:根據mFirstTouchTarget重新分發事件。

步驟1的具體代碼如下:

在這裏插入圖片描述

圖中紅框標出了是否需要攔截的條件:

  • 如果事件爲DOWN事件,則調用onInterceptTouchEvent進行攔截判斷
  • 或者 mFirstTouchTarget 不爲 null,代表已經有子 View 捕獲了這個事件,子 View 的 dispatchTouchEvent 返回 true 就是代表捕獲 touch 事件。

如果在上面步驟 1 中,當前 ViewGroup 並沒有對事件進行攔截,則執行步驟 2。

步驟 2 具體代碼如下:

在這裏插入圖片描述

仔細看上述代碼可以看出:

  • 圖中①處表名事件主動分發的前提是事件爲DOWN事件;
  • 圖中②處遍歷所有子View;
  • 圖中③處判斷事件座標是否在子View的座標範圍內,並且子View並沒有處在動畫狀態;
  • 圖中④處調用dispatchTransformedTouchEvent方法將事件分發給子View,如果子View捕獲事件成功,則將mFirstTouchTarget賦值給子View。

步驟3具體代碼如下:

在這裏插入圖片描述

步驟3有2個分支判斷。

  • 分支1:如果此時mFirstTouchTarget爲null,說明在上述的事件分發中並沒有子View對事件進行了捕獲操作。這種情況下,直接調用dispatchTransformedTouchEvent方法,並傳入child爲null,最終會調用super.dispatchTransformedTouchEvent方法。實際上最終會調用自身的onTouchEvent方法,進行處理touch事件。也就是說:如果沒有子View捕獲處理touch事件,ViewGroup會通過自身的onTouchEvent方法進行處理

爲什麼DOWN事件特殊?

所有的touch事件都是從DOWN事件開始的,這是DOWN事件比較特殊的原因之一。另一個原因就是DOWN事件的處理結果會直接影響到後續MOVE,UP事件的邏輯。

在步驟2中,只有DOWN事件會傳遞給子View進行捕獲判斷,一旦子View捕獲成功,後續的MOVE和UP事件是通過遍歷mFirstTouchTarget鏈表,查找之前接受ACTION_DOWN的子View,並將觸摸事件分配給這些子View。也就是說後續的MOVE,UP事件的分發交給誰,取決於它們的起始事件DOWN是由誰捕獲的。

mFirstTouchTarget有什麼作用?

mFirstTouchTarget的部分源碼如下:

在這裏插入圖片描述

可以看出其實mFirstTouchTarget是一個TouchTarget類型的鏈表結構。而這個TouchTarget的作用就是用來記錄捕獲了DOWN事件的View,具體保存在上圖中的child變量。可是爲什麼是鏈表類型的結構呢?因爲Android設備是支持多指操作的,每一個手指的DOWN事件都可以當作一個TouchTarget保存起來。在步驟3中判斷如果mFirstTarget不爲null,則再次將事件分發給相應的TouchTarget。

容易被遺漏的CANCEL事件

在上面步驟3中,繼續向子View分發事件的代碼中,有一段比較有趣的邏輯:
在這裏插入圖片描述

上圖紅框中表名已經有子View捕獲了touch事件,但是藍色框中的intercepted boolean變量又是true。這種情況下,事件主導權會重新回到父視圖ViewGroup中,並傳遞給子View的分發事件中傳入一個cancelChild == true。

看一下dispatchTransformedTouchEvent方法的部分源碼如下:

在這裏插入圖片描述

因爲之前傳入參數cancel爲true,並且child不爲null,最終這個事件會被包裝爲一個ACTION_CANCEL事件傳給child

什麼情況下會觸發這段邏輯呢?

總結一下就是:當父視圖的onInterceptTouchEvent先返回false,然後在子View的dispatchTouchEvent中返回true(表示子View捕獲事件),關鍵步驟就是在接下來的MOVE的過程中,父視圖的onInterceptTouchEvent又返回true,intercepted被重新置爲true,此時上述邏輯就會被觸發,子控件就會收到ACTION_CANCEL的touch事件。

實際上有個很經典的例子可以用來演示這種情況:

當在ScrollView中添加自定義View時,ScrollView默認在DOWN事件中並不會進行攔截,事件會被傳遞給ScrollView內的子控件。只有當手指進行滑動並達到一定的距離之後,onInterceptTouchEvent方法返回true,並觸發ScrollView的滾動效果。當ScrollView進行滾動的瞬間,內部的子View會接收到一個CANCEL事件,並丟失touch焦點。

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