安卓自定義View進階-事件分發原理

WalterMunz 8年前發布 | 7K 次閱讀 安卓開發 Android開發 移動開發

之前講解了很多與View繪圖相關的知識,你可以在 安卓自定義View教程目錄 中查看到這些文章,如果你理解了這些文章,那么至少2D繪圖部分不是難題了,大部分的需求都能滿足,但是關于View還有很多知識點,例如: 讓繪圖更加炫酷的Paint , 讓View動起來的動畫 , 與用戶交互的觸控事件 等一系列內容。 本次就帶大家簡單的了解一下與交互息息相關的東西-事件分發原理 。

注意:本文中所有源碼分析部分均基于 API23(Android 6.0) 版本,由于安卓系統源碼改變很多,可能與之前版本有所不同,但基本流程都是一致的。

為什么要有事件分發機制?

安卓上面的View是樹形結構的,View可能會重疊在一起,當我們點擊的地方有多個View都可以響應的時候,這個點擊事件應該給誰呢?為了解決這一個問題,就有了事件分發機制。

如下圖,View是一層一層嵌套的,當手指點擊 View1 的時候,下面的 ViewGroupA 、 RootView 等也是能夠響應的,為了確定到底應該是哪個View處理這次點擊事件,就需要事件分發機制來幫忙。

View的結構:

我們的View是樹形結構的,在上一個問題中實例View的結構大致如下:

layout文件:

<com.gcssloop.touchevent.test.RootView
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="300dp"
    android:background="#4E5268"
    android:layout_margin="20dp"
    tools:context="com.gcssloop.touchevent.MainActivity">

    <com.gcssloop.touchevent.test.ViewGroupA
        android:background="#95C3FA"
        android:layout_width="200dp"
        android:layout_height="200dp">

        <com.gcssloop.touchevent.test.View1
            android:background="#BDDA66"
            android:layout_width="130dp"
            android:layout_height="130dp"/>

    </com.gcssloop.touchevent.test.ViewGroupA>

    <com.gcssloop.touchevent.test.View2
        android:layout_alignParentRight="true"
        android:background="#BDDA66"
        android:layout_width="80dp"
        android:layout_height="80dp"/>

</com.gcssloop.touchevent.test.RootView>

View結構:

可以看到在上面的View結構中莫名多出來的兩個東西, PhoneWindow 和 DockerView ,這兩個我們并沒有在Layout文件中定義過,但是為什么會存在呢?

仔細觀察上面的 layout 文件,你會發現一個問題,我在 layout 文件中的最頂層 View(Group) 的大小并不是填滿父窗體的,留下了大量的空白區域,由于我們的手機屏幕不能透明,所以這些空白區域肯定要顯示一些東西,那么應該顯示什么呢?

有過安卓開發經驗的都知道,屏幕上沒有View遮擋的部分會顯示主題的顏色。不僅如此,最上面的一個標題欄也沒有在 layout 文件中,這個標題欄又是顯示在哪里的呢?

你沒有猜錯,這個主題顏色和標題欄等內容就是顯示在 DecorView 中的。

現在知道 DecorView 是干什么的了,那么 PhoneWindow 又有什么作用?

要了解 PhoneWindow 是干啥的,首先要了解啥是 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.

簡單來說,Window是一個抽象類,是所有視圖的最頂層容器,視圖的外觀和行為都歸他管,不論是背景顯示,標題欄還是事件處理都是他管理的范疇,它其實就像是View界的太上皇(雖然能管的事情看似很多,但是沒實權,因為抽象類不能直接使用)。

而 PhoneWindow 作為 Window 的唯一親兒子(唯一實現類),自然就是 View 界的皇帝了,PhoneWindow 的權利可是非常大大,不過對于我們來說用處并不大,因為皇帝平時都是躲在深宮里面的,雖然偶爾用特殊方法能見上一面,但想要完全指揮 PhoneWindow 為你工作是很困難的。

而上面說的 DecorView 是 PhoneWindow 的一個內部類,其職位相當于小太監,就是跟在 PhoneWindow 身邊專業為 PhoneWindow 服務的,除了自己要干活之外,也負責消息的傳遞,PhoneWindow 的指示通過 DecorView 傳遞給下面的 View,而下面 View 的信息也通過 DecorView 回傳給 PhoneWindow。

事件分發、攔截與消費

下表省略了 PhoneWidow 和 DecorView。

√ 表示有該方法。

X 表示沒有該方法。

類型 相關方法 Activity ViewGroup View
事件分發 dispatchTouchEvent
事件攔截 onInterceptTouchEvent X X
事件消費 onTouchEvent

這個三個方法均有一個 boolean(布爾) 類型的返回值,通過返回 true 和 false 來控制事件傳遞的流程。

PS: 從上表可以看到 Activity 和 View 都是沒有時間攔截機制的,這是因為:

Activity 作為原始的事件分發者,如果 Activity 攔截了事件會導致整個屏幕都無法響應事件,這肯定不是我們想要的效果。

View最為事件傳遞的最末端,要么消費掉事件,要么不處理進行回傳,根本沒必要進行事件攔截。

事件分發流程

前面我們了解到了我們的View是樹形結構的,基于這樣的結構,我們的事件可以進行有序的分發。

事件收集之后最先傳遞給 Activity, 然后依次向下傳遞,大致如下:

Activity -> PhoneWindow -> DecorView -> ViewGroup -> ... -> View

這樣的事件分發機制邏輯非常清晰,可是,你是否注意到一個問題?如果最后分發到View,如果這個View也沒有處理事件怎么辦,就這樣讓事件浪費掉?

當然不會啦,如果沒有任何View消費掉事件,那么這個事件會按照反方向回傳,最終傳回給Activity,如果最后 Activity 也沒有處理,本次事件才會被拋棄:

Activity <- PhoneWindow <- DecorView <- ViewGroup <- ... <- View

看到這里,我不禁微微一皺眉,這個東西咋看起來那么熟悉呢?再仔細一看,這不就是一個非常經典的 責任鏈模式 么? 如果我能處理就攔截下來自己干,如果自己不能處理或者不確定就交給責任鏈中下一個對象,只不過責任鏈一般是單向的,而這個提供了一個反向的反饋機制。

這種設計是非常精巧的,上層View既可以直接攔截該事件,自己處理,也可以先詢問(分發給)子View,如果子View需要就交給子View處理,如果子View不需要還能繼續交給上層View處理。既保證了事件的有序性,又非常的靈活。在我第一次將這個邏輯弄清楚的時候,看著這樣精妙的設計,簡直想歡呼慶賀一下。

其實關于事件傳遞機制,吳小龍的 Android事件傳遞機制分析 一文中的比喻非常有趣,本文也會借鑒一些其中的內容。

先確定幾個角色:

Activity - 公司大老板

RootView - 項目經理

ViewGroupA - 技術小組長

View1 - 碼農小王(公司里唯一的碼農)

View2 - 跑龍套的路人甲,無視即可

PS:由于 PhoneWindow 和 DecorView 我們無法直接操作,以下所有示例均省略了 PhoneWindow 和 DecorView。

1.點擊 View1 區域但沒有任何 View 消費事件

安卓自定義View進階-事件分發原理

當手指在 View1 區域點擊了一下之后,如果所有View都不消耗事件,你就能看到一個完整的事件分發流程,大致如下:

紅色箭頭方向表示事件分發方向。

綠色箭頭方向表示事件回傳方向。

注意: 上圖顯示分發流程僅僅是一個示意流程,并不代表實際情況,如果按照實際情況繪制,會導致流程圖非常復雜和混亂,在糾結了好久之后做了一個艱難的決定,采用這樣一個簡化后的流程。

上面的流程中存在部分不合理內容,請大家選擇性接受。

  1. 事件返回時 dispatchTouchEvent 直接指向了父View的 onTouchEvent 這一部分是不合理的,實際上它僅僅是給了父View的 dispatchTouchEvent 一個 false 返回值,父View根據返回值來調用自身的 onTouchEvent 。

  2. ViewGroup 是根據 onInterceptTouchEvent 的返回值來確定是調用子View的 dispatchTouchEvent 還是自身的 onTouchEvent , 并沒有將調用交給 onInterceptTouchEvent 。

  3. ViewGroup 的事件分發機制偽代碼如下,可以看出事件分發的順序。

public boolean dispatchTouchEvent(MotionEvent ev) {
    boolean result = false;             // 默認狀態為沒有消費過

    if (!onInterceptTouchEvent(ev)) {   // 如果沒有攔截交給子View
        result = child.dispatchTouchEvent(ev);
    }

    if (!result) {                      // 如果事件沒有被消費,詢問自身onTouchEvent
        result = onTouchEvent(ev);
    }

    return result;
}

測試:

情景:老板: 我看公司最近業務不咋地,準備發展一下電商業務,下周之前做個淘寶出來試試怎么樣。

事件順序,老板(MainActivity)要做淘寶,這個事件通過各個部門(ViewGroup)一層一層的往下傳,傳到最底層的時候,碼農小王(View1)發現做不了,于是消息又一層一層的回傳到老板那里。

可以看到整個事件傳遞路線非常有序。從Activity開始,最后回傳給Activity結束(由于我們無法操作Phone Window和DecorView,所以沒有它們的信息)。

MainActivity [老板]: dispatchTouchEvent     經理,我準備發展一下電商業務,下周之前做一個淘寶出來.
RootView     [經理]: dispatchTouchEvent     呼叫技術部,老板要做淘寶,下周上線.
RootView     [經理]: onInterceptTouchEvent  (老板可能瘋了,但又不是我做.)
ViewGroupA   [組長]: dispatchTouchEvent     老板要做淘寶,下周上線?
ViewGroupA   [組長]: onInterceptTouchEvent  (看著不太靠譜,先問問小王怎么看)
View1        [碼農]: onInterceptTouchEvent  做淘寶???
View1        [碼農]: onTouchEvent           這個真心做不了啊.
ViewGroupA   [組長]: onTouchEvent           小王說做不了.
RootView     [經理]: onTouchEvent           報告老板, 技術部說做不了.
MainActivity [老板]: onTouchEvent           這么簡單都做不了,你們都是干啥的(憤怒).

2.點擊 View1 區域且事件被 View1 消費

如果事件被View1消費掉了則事件會回傳告訴上層View這個事件已經被我解決了,上層View就無需再響應了。

注意:這張圖中的事件回傳路徑才是正確的路徑。

測試:

情景:老板: 我覺得咱們這個app按鈕不好看,做的有光澤一點,要讓人有一種想點的欲望。

事件順序,老板(MainActivity)要做改界面,這個事件通過各個部門(ViewGroup)一層一層的往下傳,傳到最底層的時候,碼農小王(View1)發現很容易,就在按鈕上添加了一道光。

可以看出,事件一旦被消費就意味著消息傳遞的結束,上層View知道了事件已經被消費掉,就不再處理了。

MainActivity [老板]: dispatchTouchEvent     把按鈕做的好看一點,要有光澤,給人一種點擊的欲望.
RootView     [經理]: dispatchTouchEvent     技術部,老板說按鈕不好看,要加一道光.
RootView     [經理]: onInterceptTouchEvent  
ViewGroupA   [組長]: dispatchTouchEvent     給按鈕加上一道光.
ViewGroupA   [組長]: onInterceptTouchEvent  
View1        [碼農]: onInterceptTouchEvent  加一道光.
View1        [碼農]: onTouchEvent           做好了.

加一道光:

3.點擊 View1 區域但事件被 ViewGroupA 攔截

上層的View有權攔截事件,不傳遞給下層View,例如 ListView 滑動的時候,就不會將事件傳遞給下層的子 View。

注意:可以看到,如果上層攔截了事件,下層View將接收不到事件信息。

測試:

情景:老板: 報告一下項目進度。

事件順序,老板(MainActivity)要知道項目進度,這個事件通過各個部門(ViewGroup)一層一層的往下傳,傳到技術組組長的時候,組長上報任務即可。

MainActivity [老板]: dispatchTouchEvent     現在項目做到什么程度了?
RootView     [經理]: dispatchTouchEvent     技術部,你們的app快做完了么?
RootView     [經理]: onInterceptTouchEvent  
ViewGroupA   [組長]: dispatchTouchEvent     項目進度?
ViewGroupA   [組長]: onInterceptTouchEvent  
ViewGroupA   [組長]: onTouchEvent           正在測試,明天就測試完了

其他情形

事件分發機制設計到到情形非常多,這里就不一一列舉了,記住以下幾條原則就行了。

  • 1.如果事件被消費,就意味著事件信息傳遞終止。
  • 2.如果事件一直沒有被消費,最后會傳給Activity,如果Activity也不需要就被拋棄。
  • 3.判斷事件是否被消費是根據返回值,而不是根據你是否使用了事件。

總結

View的事件分發機制實際上就是一個非常經典的責任鏈模式,如果你了解責任鏈模式,那么事件分發對你來說并不是什么難題,如果你不了解責任鏈模式,剛好借此機會學習一下啦。

責任鏈模式:

當有多個對象均可以處理同一請求的時候,將這些對象串聯成一條鏈,并沿著這條鏈傳遞改請求,直到有對象處理它為止。

Android 中事件分發機制原理雖然非常簡單,但由于實際運用場景非常復雜,一旦具體到某個場景中變得很麻煩,而本文僅僅是帶你簡單的了解一下事件分發機制,更詳細的內容和具體的一些特殊情形處理會在后續文章中進行講解。

最后,由于個人水平有限,文章中可能會出現錯誤,如果你覺得哪一部分有錯誤,或者發現了錯別字等內容,歡迎在評論區告訴我,另外,據說關注作者微博不僅能第一時間收到新文章消息,還能變帥哦。

參考資料

Activity

PhoneWindow

ViewGroup

View

Android事件傳遞機制分析

Android ViewGroup/View 事件分發機制詳解

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

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

更簡單的學習Android事件分發

《安卓開發藝術探索》

 

 

 

來自:http://www.gcssloop.com/customview/dispatch-touchevent-theory

 

 本文由用戶 WalterMunz 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!