Android事件分發機制三:事件分發工作流程

一隻修仙的猿發表於2021-01-24

前言

很高興遇見你~

本文是事件分發系列的第三篇。

在前兩篇文章中,Android事件分發機制一:事件是如何到達activity的? 分析了事件分發的真正起點:viewRootImpl,Activity只是其中的一個環節;Android事件分發機制二:viewGroup與view對事件的處理 原始碼解析了viewGroup和view是如何分發事件的。

事件分發的核心內容,則為viewGroup和view對事件的分發,也就是第二篇文章。第二篇文章對原始碼的分析較為深入,缺乏一個更高的角度來審視事件分發流程。本文在前面的分析基礎上,對整個事件分發的工作流程進行一個總結,更好地把握事件是如何在不同的物件和方法之間進行傳遞。

回顧

先來回顧一下整體的流程,以便更好地定位我們的知識。

  1. 觸控資訊從手機觸控螢幕時產生,通過IMS和WMS傳送到viewRootImpl
  2. viewRootImpl通過呼叫view的dispatchPointerEvent方法把觸控資訊傳遞給view
  3. view通過呼叫自身的dispatchTouchEvent方法開始了事件分發

圖中的view指的是一個控制元件樹,他可以是一個viewGroup也可以是一個簡單的view。因為viewGroup是繼承自view,所以一個控制元件樹,也可以看做是一個view。

我們今天探討的工作流程,就是從圖中的view呼叫自身的dispatchTouchEvent開始。

主要物件與方法

事件分發的物件

這一部分內容在第二篇有詳細解析,這裡做個簡單的回顧。

當我們手機觸碰螢幕時會產生一系列的MotionEvent物件,根據觸控的情況不同,這些物件的型別也會不同。具體如下:

  • ACTION_DOWN: 表示手指按下螢幕
  • ACTION_MOVE: 手指在螢幕上滑動時,會產生一系列的MOVE事件
  • ACTION_UP: 手指抬起,離開螢幕、
  • ACTION_CANCEL:當出現異常情況事件序列被中斷,會產生該型別事件
  • ACTION_POINTER_DOWN: 當已經有一個手指按下的情況下,另一個手指按下會產生該事件
  • ACTION_POINTER_UP: 多個手指同時按下的情況下,抬起其中一個手指會產生該事件

事件分發的方法

事件分發屬於控制元件系統的一部分,主要的分發物件是viewGroup與view。而其中核心的方法有三個: dispatchTouchEventonInterceptTouchEventonTouchEvent 。那麼在講分發流程之前,先來介紹一下這三個方法。這三個方法屬於view體系的類,其中Window.CallBack介面中包含了 dispatchTouchEventonTouchEvent 方法,Activity和Dialog都實現了Window.CallBack介面,因此都實現了該方法。因這三個方法經常在自定義view中被重寫,以下的分析,如果沒有特殊說明都是在預設方法實現的情況下。

dispatchTouchEvent

該方法是事件分發的核心方法,事件分發的邏輯都是在這個方法中實現。該方法存在於類View中,子類ViewGroup、以及其他的實現類如DecorView都重寫了該方法。

無論是在viewGroup還是view,該方法的主要作用都是處理事件。如果成功處理則返回true,處理失敗則返回false,表示事件沒有被處理。具體到類,在viewGroup相關類中,該方法的主要作用是把事件分發到該viewGroup所擁有的子view,如果子view沒有處理則自己處理;在view的相關類中,該方法的主要作用是消費觸控事件。

onInterceptTouchEvent

該方法只存在於viewGroup中,當一個事件需要被分發到子view時,viewGroup會呼叫此方法檢查是否要進行攔截。如果攔截則自己處理,而如果不攔截才會呼叫子view的 dispatchTouchEvent 方法分發事件。

方法返回true表示攔截事件,返回false表示不攔截。

這個方法預設只對滑鼠的相關操作的一種特殊情況進行了攔截,其他的情況需要具體的實現類去重寫攔截。

onTouchEvent

該方法是消費事件的主要方法,存在於view中,viewGroup預設並沒有重寫該方法。方法返回true表示消費事件,返回false表示不消費事件。

viewGroup分發事件時,如果沒有一個子view消費事件,那麼會呼叫自身的onTouchEvent方法來處理事件。View的dispatchTouchEvent方法中,並不是直接呼叫onTouchEvent方法來消費事件,而是先呼叫onTouchListener判斷是否消費;如果onTouchListener沒有消費事件,才會呼叫onTouchEvent來處理事件。

我們為view設定的onClickListener與onLongClickListener都是在View的dispatchTouchEvent方法中,根據具體的觸控情況被呼叫。

重要規則

事件分發有一個很重要的原則:一個觸控點的事件序列只能給一個view消費,除非發生異常情況如被viewGroup攔截 。具體到程式碼實現就是:消費了一個觸控點事件序列的down事件的view,將持續消費該觸控點事件序列接下來的所有的事件 。舉個例子:

當我手指按下螢幕時產生了一個down事件,只有一個view消費了這個down事件,那麼接下來我的手指滑動螢幕產生的move事件會且僅會給這個view消費。而當我手機抬起,再按下時,這時候又會產生新的down事件,那麼這個時候就會再一次去尋找消費down事件的view。所以,事件分發,是以事件序列為單位的

因此下面的工作流程中都是指down事件的分發 ,而不是ACTION_MOVE或ACTION_UP的分發。因為消費了down事件,意味著接下來的move和up事件都會給這個view處理,也就無所謂分發了。但同時注意事件序列是可以被viewGroup的onInterceptTouchEvent中斷的,這些就屬於其他的情況了。

細心的讀者還會發現事件分發中包含了多點觸控。在多點觸控的情況下,ACTION_POINTER_DOWN與ACTION_DOWN的分發規則是不同的,具體可前往第二篇文章瞭解詳細。ACTION_POINTER_DOWN在ACTION_DOWN的分發模型上稍作了一些修改而已,後面會詳細解析,

工作流程模型

工作流程模型,本質上就是不同的控制元件物件,viewGroup和view之間事件分發方法的關係。需要注意的是,這裡討論的是viewGroup和view的預設方法實現,不涉及其他實現類如DecorView的重寫方法。

下面用一段虛擬碼來表示三個事件分發方法之間的關係( 這裡再次強調,這裡的事件分發模型分發的事件型別是ACTION_DOWN且都是預設的方法,沒有經過重寫,這點很重要 ):

public boolean dispatchTouchEvent(MotionEvent event){
    
    // 先判斷是否攔截
    if (onInterceptTouchEvent()){
        // 如果攔截呼叫自身的onTouchEvent方法判斷是否消費事件
        return onTouchEvent(event);
    }
    // 否則呼叫子view的分發方法判斷是否處理事件
    if (childView.dispatchTouchEvent(event)){
        return true;
    }else{
        return onTouchEvent(event);
    }
}

這段程式碼非常好的展示了三個方法之間的關係:在viewGroup收到觸控事件時,會先去呼叫 onInterceptTouchEvent 方法判斷是否攔截,如果攔截則呼叫自己的 onTouchEvent 方法處理事件,否則呼叫子view的 dispatchTouchEvent 方法來分發事件。因為子view也有可能是一個viewGroup,這樣就形成了一個類似遞迴的關係。

這裡我再補上view分發邏輯的簡化虛擬碼:

public boolean dispatchTouchEvent(MotionEvent event){
    // 先判斷是否存在onTouchListener且返回值為true
    if (mOnTouchListener!=null && mOnTouchListener.onTouch(event)){
        // 如果成功消費則返回true
        return true;
    }else{
        // 否則呼叫onTouchEvent消費事件
        return onTouchEvent(event);
    }
}

view與viewGroup不同的是他不需要分發事件,所以也就沒有必要攔截事件。view會先檢查是否有onTouchListener且返回值是否為true,如果是true則直接返回,否則呼叫onTouchEvent方法來處理事件。

基於上述的關係,可以得到下面的工作流程圖:

這裡為了展示類遞迴關係使用了畫了兩個viewGroup,只需看中間一個即可,下面對這個圖進行解析:

  • viewGroup
    1. viewGroup的dispatchTouchEvent方法接收到事件訊息,首先會去呼叫onInterceptTouchEvent判斷是否攔截事件
      • 如果攔截,則呼叫自身的onTouchEvent方法
      • 如果不攔截則呼叫子view的dispatchTouchEvent方法
    2. 子view沒有消費事件,那麼會呼叫viewGroup本身的onTouchEvent
    3. 上面1、2步的處理結果為viewGroup的dispatchTouchEvent方法的處理結果,並返回給上一層的onTouchEvent處理
  • view
    1. view的dispatchTouchEvent預設情況下會呼叫onTouchEvent來處理事件,返回true表示消費事件,返回false表示沒有消費事件
    2. 第1步的結果就是dispatchTouchEvent方法的處理結果,成功消費則返回true,沒有消費則返回false並交給上一層的onTouchEvent處理

可以看到整個工作流程就是一個“U”型結構,在不攔截的情況下,會一層層向下尋找消費事件的view。而如果當前view不處理事件,那麼就一層層向上拋,尋找處理的viewGroup。

上述的工作流程模型並不是完整的,還有其他的特殊情況沒有考慮。下面討論幾種特殊的情況:

事件序列被中斷

我們知道,當一個view接收了down事件之後,該觸控點接下來的事件都會被這個view消費。但是,viewGroup是可以在中途掐斷事件流的,因為每一個需要分發給子view的事件都需要經過攔截方法:onInterceptTouchEvent (當然,這裡不討論子view設定不攔截標誌的情況)。那麼當viewGroup掐斷事件流之後,事件的走向又是如何的呢?參看下圖:(注意,這裡不討論多指操作的情況,僅討論單指操作的move或up事件被viewGroup攔截的情況

  1. 當viewGroup攔截子view的move或up事件之後,會將當前事件改為cancel事件併傳送給子view
  2. 如果當前事件序列還未結束,那些接下來的事件都會交給viewGroup的ouTouchEvent處理
  3. 此時不管是viewGroup還是view的onTouchEvent返回了false,那麼將導致整個控制元件樹的dispatchTouchEvent方法返回false
    • 秉承著一個事件序列只能給一個view消費的原則,如果一個view消耗了down事件卻在接下來的move或up事件返回了false,那麼此事件不會給上層的viewGroup處理,而是直接返回false。
多點觸控情況

上面討論的所有情況,都是不包含多點觸控情況的。多點觸控的情況,在原有的事件分發流程上,新增了一些特殊情況。這裡就不再畫圖,而是把一些特殊情況描述一下,讀者瞭解一下就可以了。

預設情況下,viewGroup是支援多點觸控的分發,但view是不支援多點觸控的,需要自己去重寫 dispatchTouchEvent 方法來支援多點觸控。

多點觸控的分發規則如下:

viewGroup在已有view接受了其他觸點的down事件的情況下,另一個手指按下產生ACTION_POINTER_DOWN事件傳遞給viewGroup:

  1. viewGroup會按照ACTION_DOWN的方式去分發ACTION_POINTER_DOWN事件
    • 如果子view消費該事件,那麼和單點觸控的流程一致
    • 如果子view未消費該事件,那麼會交給上一個最後接收down事件的view去處理
  2. viewGroup兩個view接收了不同的down事件,那麼攔截其中一個view的事件序列,viewGroup不會消費攔截的事件序列。換句話說,viewGroup和其中的view不能同時接收觸控事件。

Activity的事件分發

細心的讀者會發現,上述的工作流程並不涉及Activity。我們印象中的事件分發都是 Activity -> Window -> ViewGroup ,那麼這是怎麼回事?這一切,都是DecorView “惹的禍” 。

DecorView重寫viewGroup的 dispatchTouchEvent 方法,當接收到觸控事件後,DecorView會首先把觸控物件傳遞給內部的callBack物件。沒錯,這個callBack物件就是Activity。加入Activity這個環節之後,分發的流程如下圖所示:

整體上和前面的流程沒有多大的不同,Activity繼承了Window.CallBack介面,所以也有dispatchTouchEvent和onTouchEvent方法。對上圖做個簡單的分析:

  1. activity接收到觸控事件之後,會直接把觸控事件分發給viewGroup
  2. 如果viewGroup的dispatchTouchEvent方法返回false,那麼會呼叫Activity的onTouchEvent來處理事件
  3. 第1、2步的處理結果就是activity的dispatchTouchEvent方法的處理結果,並返回給上層

上面的流程不僅適用於Activity,同樣適用於Dialog等使用DecorView和callback模式的控制元件系統。

最後

到這裡,事件分發的主要內容也就講解完了。結合前兩篇文章,相信讀者對於事件分發有更高的認知。

紙上得來終覺淺,絕知此事要躬行。學了知識之後最重要的就是實踐。下一篇文章將簡單分析一下如何利用學習到的事件分發知識運用到實際開發中。

原創不易,你的點贊是我創作最大的動力,感謝閱讀 ~

全文到此,原創不易,覺得有幫助可以點贊收藏評論轉發。
筆者才疏學淺,有任何想法歡迎評論區交流指正。
如需轉載請評論區或私信交流。

另外歡迎光臨筆者的個人部落格:傳送門

相關文章