Android事件分發機制、滑動衝突解決

一個暱稱而已T發表於2017-09-22

這裡所說的事件,是MotrionEvent

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


事件分發


在瞭解事件分發前,應該先了解一下Activity的檢視層級,因為這裡只涉及事件的分發,所以做了一個比較簡單的示意圖:
這裡寫圖片描述

Window、Activity、DecorView以及ViewRoot之間的關係


首先需要明確一點的事,事件分發機制實際上應用了責任鏈模式

事件分發的大致流程如下圖所示:

具體的說說明可以參考這篇文章:安卓自定義View進階-事件分發機制原理


接下來只是整理一些筆記(參考《Android開發藝術探索》):


(1)相關方法是說明

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

onInterceptTouchEvent
在dispatchTouchEvent的內部呼叫,用來判斷是是否攔截某個事件,如果當前ViewGroup(View不存在該方法,且在ViewGroup中預設返回不攔截)攔截了某個事件,那麼在用一個事件序列當中,此方法不會被再次呼叫,返回結果表示是否攔截當前事件

(同一事件序列是指從手指接觸螢幕的那一個刻起,到手指離開螢幕的那一刻結束,在這個過程中所產生的一系列事件,
這個時間序列以DOWN事件開始,中間含有數量不定的MOVE事件,最終以UP事件結束)

onTouchEvent
在dispatchTouchEvent的內部呼叫,用來處理點選事件,返回結果表示是否消耗了當前事件如果不消耗,則在同一個事件序列中,當前View無法再次接收到事件
onTouchEvent預設消耗事件,即返回true,除非它是不可點選的,clickable和longClickable同時為false,其中longClickeable都預設為false,clickable則分情況,Button的預設為true,TextView的為false。而View的enable屬性不影響onTouchEvent的預設返回值,自只要clickable和longClickable有一個為true即可)

根據上述描述可以知道:

如果一個View攔截了一個事件序列的某一個事件,則該事件後的事件都會預設被攔截。

可以知道如果一個View不消耗(消耗即返回true,但不代表一定要根據該事件實現某種邏輯,可以就是空邏輯)一個事件序列中的某一事件,那麼接下來的一連串事件也就無法接收到了,因為事件一般是按序列來的,不先消耗前一個事件,則無法繼續後序事件的處理。


(2)事件分發的巨集觀流程

一個事件的大概流程如下:

Activity -> Window(由PhoneWindow實現) -> DecorView(底層容器)-> RootView(通過setConentView設定的)

PhoneWindow(附屬於Activity的,控制頂級View的外觀和行為策略)和DecorView只負責事件的分發,不存在攔截與處理事件的邏輯。如果事件最終沒有被消耗,就會返回給Activity,由其onTouchEvent消耗。


(3)當一個View需要處理事件時,如果它設定了OnTouchListner,那麼OnTouchListener中的onTouch方法會被回撥。這時事件如何處理還需要看onTouch的返回值,如果返回false,則當前View的onTouchEvent方法會被呼叫,否則不會。給View設定的OnTounchListener優先順序大於onTouchEvent。(OnTouchListenr是從外部定義處理TouchEvent的事件的邏輯,onTouchEvent的邏輯則是事先被定義好的)。在onTouchEvent中,如果當前有設定OnClickListener,那麼它的onClick方法會被呼叫(前提是當前View是可點選的,且它收到了down和up事件),OnClickListener的優先順序最低。


(4)正常情況下,一個事件序列只能被一個View攔截且消耗。因為一旦一個元素攔截了某些事件,那麼同一個事件序列的後續事件都會交給它處理,且其onInterceptTouchEvent不會再被呼叫,從而不能在傳遞給其它View處理。但是可以通過特殊手段做到,如一個View將本該自己處理的事件通過onTouchEvent強行傳遞給其它View處理。


(5)如果View不消耗除ACTION_DOWN以外的事件,那麼這個事件序列將會消失,此時父元素的onTouchEvent不會被呼叫(因為父元素認為將事件傳遞給了子元素去處理),但是當前View可以持續收到後續事件,最後消失的事件會回傳給Activity處理。


(6)通過requestDisallowInterceptTouchEvent方法可以在子元素中干預父元素的事件分發過程,但是DOWN事件除外。


(7)事件傳遞給子元素,需要先判斷子元素是否能夠接收到點選事件:子元素是否在播放動畫和點選事件的座標是否落在子元素的區域內。


滑動衝突

滑動衝突的場景可以簡單分為以下三種:

  • 外部滑動方向和內部不一致
  • 外部滑動方向與內部一致
  • 上面兩種情況的巢狀

解決方法

(1)外部攔截法
點選事件都先經過父容器的攔截處理,攔截住需要事件,防止傳遞到子元素。比較符合事件分發的機制,需要重寫父容器的onInterceptTouchEvent方法,在內部做出相應攔截即可。

(2)內部攔截法
父容器不攔截任何事件,所有事件都交給子元素,如果子元素需要就直接消耗掉,否則交由父容器處理。需要配合requsestDisallowTouchEvent方法才能正常工作。除了子元素需要處理以外,父容器也要預設攔截除了DOWN意外的其他事件。

相關文章