以前講解了不少與View繪圖相關的知識,你能夠在 安卓自定義View教程目錄 中查看到這些文章,若是你理解了這些文章,那麼至少2D繪圖部分不是難題了,大部分的需求都能知足,可是關於View還有不少知識點,例如: 讓繪圖更加炫酷的Paint
,讓View動起來的動畫
,與用戶交互的觸控事件
等一系列內容。本次就帶你們簡單的瞭解一下與交互息息相關的東西-事件分發原理。css
本次魔法小火車的終點站是事件分發,請各位魔法師帶好裝備,準備登車啓程。html
注意:本文中全部源碼分析部分均基於 API23(Android 6.0) 版本,因爲安卓系統源碼改變不少,可能與以前版本有所不一樣,但基本流程都是一致的。java
安卓上面的View是樹形結構的,View可能會重疊在一塊兒,當咱們點擊的地方有多個View均可以響應的時候,這個點擊事件應該給誰呢?爲了解決這一個問題,就有了事件分發機制。android
以下圖,View是一層一層嵌套的,當手指點擊 View1
的時候,下面的ViewGroupA
、 RootView
等也是可以響應的,爲了肯定到底應該是哪一個View處理此次點擊事件,就須要事件分發機制來幫忙。git
咱們的View是樹形結構的,在上一個問題中實例View的結構大體以下:github
layout文件:app
<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結構:ide
能夠看到在上面的View結構中莫名多出來的兩個東西,PhoneWindow
和 DecorView
,這兩個咱們並無在Layout文件中定義過,可是爲何會存在呢?oop
仔細觀察上面的 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。
當手指在 View1
區域點擊了一下以後,若是全部View都不消耗事件,你就能看到一個完整的事件分發流程,大體以下:
紅色箭頭方向表示事件分發方向。
綠色箭頭方向表示事件回傳方向。
注意: 上圖顯示分發流程僅僅是一個示意流程,並不表明實際狀況,若是按照實際狀況繪製,會致使流程圖很是複雜和混亂,在糾結了很久以後作了一個艱難的決定,採用這樣一個簡化後的流程。
上面的流程中存在部分不合理內容,請你們選擇性接受。
- 事件返回時
dispatchTouchEvent
直接指向了父View的onTouchEvent
這一部分是不合理的,實際上它僅僅是給了父View的dispatchTouchEvent
一個 false 返回值,父View根據返回值來調用自身的onTouchEvent
。- ViewGroup 是根據
onInterceptTouchEvent
的返回值來肯定是調用子View的dispatchTouchEvent
仍是自身的onTouchEvent
, 並無將調用交給onInterceptTouchEvent
。- 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 [碼農]: dispatchTouchEvent 作淘寶??? View1 [碼農]: onTouchEvent 這個真心作不了啊. ViewGroupA [組長]: onTouchEvent 小王說作不了. RootView [經理]: onTouchEvent 報告老闆, 技術部說作不了. MainActivity [老闆]: onTouchEvent 這麼簡單都作不了,大家都是幹啥的(憤怒).
若是事件被View1消費掉了則事件會回傳告訴上層View這個事件已經被我解決了,上層View就無需再響應了。
注意:這張圖中的事件回傳路徑纔是正確的路徑。
測試:
情景:老闆: 我以爲我們這個app按鈕很差看,作的有光澤一點,要讓人有一種想點的慾望。
事件順序,老闆(MainActivity)要作改界面,這個事件經過各個部門(ViewGroup)一層一層的往下傳,傳到最底層的時候,碼農小王(View1)就在按鈕上添加了一道光(爲啥是小王呢?由於公司沒有設計師)。
能夠看出,事件一旦被消費就意味着消息傳遞的結束,上層View知道了事件已經被消費掉,就再也不處理了。
MainActivity [老闆]: dispatchTouchEvent 把按鈕作的好看一點,要有光澤,給人一種點擊的慾望. RootView [經理]: dispatchTouchEvent 技術部,老闆說按鈕很差看,要加一道光. RootView [經理]: onInterceptTouchEvent ViewGroupA [組長]: dispatchTouchEvent 給按鈕加上一道光. ViewGroupA [組長]: onInterceptTouchEvent View1 [碼農]: dispatchTouchEvent 加一道光. View1 [碼農]: onTouchEvent 作好了.
加一道光:
上層的View有權攔截事件,不傳遞給下層View,例如 ListView 滑動的時候,就不會將事件傳遞給下層的子 View。
注意:能夠看到,若是上層攔截了事件,下層View將接收不到事件信息。
測試:
情景:老闆: 報告一下項目進度。
事件順序,老闆(MainActivity)要知道項目進度,這個事件經過各個部門(ViewGroup)一層一層的往下傳,傳到技術組組長(ViewGroupA)的時候,組長(ViewGroupA)上報任務便可。無需告知碼農小王(View1)。
MainActivity [老闆]: dispatchTouchEvent 如今項目作到什麼程度了? RootView [經理]: dispatchTouchEvent 技術部,大家的app快作完了麼? RootView [經理]: onInterceptTouchEvent ViewGroupA [組長]: dispatchTouchEvent 項目進度? ViewGroupA [組長]: onInterceptTouchEvent ViewGroupA [組長]: onTouchEvent 正在測試,明天就測試完了
事件分發機制設計到到情形很是多,這裏就不一一列舉了,記住如下幾條原則就好了。
View的事件分發機制實際上就是一個很是經典的責任鏈模式,若是你瞭解責任鏈模式,那麼事件分發對你來講並非什麼難題,若是你不瞭解責任鏈模式,恰好藉此機會學習一下啦。
責任鏈模式:
當有多個對象都可以處理同一請求的時候,將這些對象串聯成一條鏈,並沿着這條鏈傳遞改請求,直到有對象處理它爲止。
Android 中事件分發機制原理雖然很是簡單,但因爲實際場景很是複雜,一旦具體到某個場景中變得很麻煩,而本文僅僅是帶你簡單的瞭解一下事件分發機制,更詳細的內容和具體的一些特殊情形處理會在後續文章中進行講解。
因爲我的水平有限,文章中可能會出現錯誤,若是你以爲哪一部分有錯誤,或者發現了錯別字等內容,歡迎在評論區告訴我,另外,聽說關注做者微博不只能第一時間收到新文章消息,還能變帥哦。
Activity
PhoneWindow
ViewGroup
View
Android事件傳遞機制分析
Android ViewGroup/View 事件分發機制詳解
Android事件分發機制徹底解析,帶你從源碼的角度完全理解(上)
Android事件分發機制徹底解析,帶你從源碼的角度完全理解(下)
更簡單的學習Android事件分發 《安卓開發藝術探索》