# 事件分發機制原理
之前講解了很多與View繪圖相關的知識,你可以在 [安卓自定義View教程目錄](http://www.gcssloop.com/customview/CustomViewIndex) 中查看到這些文章,如果你理解了這些文章,那么至少2D繪圖部分不是難題了,大部分的需求都能滿足,但是關于View還有很多知識點,例如: `讓繪圖更加炫酷的Paint`,`讓View動起來的動畫`,`與用戶交互的觸控事件` 等一系列內容。**本次就帶大家簡單的了解一下與交互息息相關的東西-事件分發原理**。
> 本次魔法小火車的終點站是事件分發,請各位魔法師帶好裝備,準備登車啟程。
**注意:本文中所有源碼分析部分均基于 API23(Android 6.0) 版本,由于安卓系統源碼改變很多,可能與之前版本有所不同,但基本流程都是一致的。**
## 為什么要有事件分發機制?
**安卓上面的View是樹形結構的,View可能會重疊在一起,當我們點擊的地方有多個View都可以響應的時候,這個點擊事件應該給誰呢?為了解決這一個問題,就有了事件分發機制。**
如下圖,View是一層一層嵌套的,當手指點擊 `View1` 的時候,下面的`ViewGroupA`、 `RootView` 等也是能夠響應的,為了確定到底應該是哪個View處理這次點擊事件,就需要事件分發機制來幫忙。

## View的結構:
我們的View是樹形結構的,在上一個問題中實例View的結構大致如下:
**layout文件:**
```XML
<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` 和 `DecorView` ,這兩個我們并沒有在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
```
**看到這里,我不禁微微一皺眉,這個東西咋看起來那么熟悉呢?再仔細一看,這不就是一個非常經典的[責任鏈模式](https://zh.wikipedia.org/wiki/%E8%B4%A3%E4%BB%BB%E9%93%BE%E6%A8%A1%E5%BC%8F)嗎,** 如果我能處理就攔截下來自己干,如果自己不能處理或者不確定就交給責任鏈中下一個對象。
**這種設計是非常精巧的,上層View既可以直接攔截該事件,自己處理,也可以先詢問(分發給)子View,如果子View需要就交給子View處理,如果子View不需要還能繼續交給上層View處理。既保證了事件的有序性,又非常的靈活。在我第一次將這個邏輯弄清楚的時候,看著這樣精妙的設計,簡直想歡呼慶賀一下。**
其實關于事件傳遞機制,吳小龍的 [Android事件傳遞機制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/) 一文中的比喻非常有趣,本文也會借鑒一些其中的內容。
先確定幾個角色:
> Activity - 公司大老板
>
> RootView - 項目經理
>
> ViewGroupA - 技術小組長
>
> View1 - 碼農小王(公司里唯一的碼農)
>
> View2 - 跑龍套的路人甲,無視即可
**PS:由于 PhoneWindow 和 DecorView 我們無法直接操作,以下所有示例均省略了 PhoneWindow 和 DecorView。**
### 1.點擊 View1 區域但沒有任何 View 消費事件

當手指在 `View1` 區域點擊了一下之后,如果所有View都不消耗事件,你就能看到一個完整的事件分發流程,大致如下:
> 紅色箭頭方向表示事件分發方向。
>
> 綠色箭頭方向表示事件回傳方向。

> **注意: 上圖顯示分發流程僅僅是一個示意流程,并不代表實際情況,如果按照實際情況繪制,會導致流程圖非常復雜和混亂,在糾結了好久之后做了一個艱難的決定,采用這樣一個簡化后的流程。**
>
> 上面的流程中存在部分不合理內容,請大家選擇性接受。
>
> 1. 事件返回時 `dispatchTouchEvent` 直接指向了父View的 `onTouchEvent` 這一部分是不合理的,實際上它僅僅是給了父View的 `dispatchTouchEvent` 一個 false 返回值,父View根據返回值來調用自身的 `onTouchEvent`。
> 2. ViewGroup 是根據 `onInterceptTouchEvent` 的返回值來確定是調用子View的 `dispatchTouchEvent` 還是自身的 `onTouchEvent`, 并沒有將調用交給 `onInterceptTouchEvent`。
> 3. ViewGroup 的事件分發機制偽代碼如下,可以看出調用的順序。
>
> ```java
> 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 這么簡單都做不了,你們都是干啥的(憤怒).
```
### 2.點擊 View1 區域且事件被 View1 消費
如果事件被View1消費掉了則事件會回傳告訴上層View這個事件已經被我解決了,上層View就無需再響應了。

> 注意:這張圖中的事件回傳路徑才是正確的路徑。
**測試:**
> **情景:老板: 我覺得咱們這個app按鈕不好看,做的有光澤一點,要讓人有一種想點的欲望。**
>
> **事件順序,老板(MainActivity)要做改界面,這個事件通過各個部門(ViewGroup)一層一層的往下傳,傳到最底層的時候,碼農小王(View1)就在按鈕上添加了一道光(為啥是小王呢?因為公司沒有設計師)。**
>
> 可以看出,事件一旦被消費就意味著消息傳遞的結束,上層View知道了事件已經被消費掉,就不再處理了。
```
MainActivity?[老板]: dispatchTouchEvent 把按鈕做的好看一點,要有光澤,給人一種點擊的欲望.
RootView?????[經理]: dispatchTouchEvent 技術部,老板說按鈕不好看,要加一道光.
RootView?????[經理]: onInterceptTouchEvent
ViewGroupA???[組長]: dispatchTouchEvent 給按鈕加上一道光.
ViewGroupA???[組長]: onInterceptTouchEvent
View1????????[碼農]: dispatchTouchEvent 加一道光.
View1????????[碼農]: onTouchEvent 做好了.
```
> 加一道光:
>
> 
### 3.點擊 View1 區域但事件被 ViewGroupA 攔截
> 上層的View有權攔截事件,不傳遞給下層View,例如 ListView 滑動的時候,就不會將事件傳遞給下層的子 View。

> 注意:可以看到,如果上層攔截了事件,下層View將接收不到事件信息。
**測試:**
> **情景:老板: 報告一下項目進度。**
>
> **事件順序,老板(MainActivity)要知道項目進度,這個事件通過各個部門(ViewGroup)一層一層的往下傳,傳到技術組組長(ViewGroupA)的時候,組長(ViewGroupA)上報任務即可。無需告知碼農小王(View1)。**
```
MainActivity?[老板]: dispatchTouchEvent 現在項目做到什么程度了?
RootView?????[經理]: dispatchTouchEvent 技術部,你們的app快做完了么?
RootView?????[經理]: onInterceptTouchEvent
ViewGroupA???[組長]: dispatchTouchEvent 項目進度?
ViewGroupA???[組長]: onInterceptTouchEvent
ViewGroupA???[組長]: onTouchEvent 正在測試,明天就測試完了
```
### 其他情況
事件分發機制設計到到情形非常多,這里就不一一列舉了,記住以下幾條原則就行了。
* 1.如果事件被消費,就意味著事件信息傳遞終止。
* 2.如果事件一直沒有被消費,最后會傳給Activity,如果Activity也不需要就被拋棄。
* 3.判斷事件是否被消費是根據返回值,而不是根據你是否使用了事件。
#### [文中測試用的源碼下載](https://raw.githubusercontent.com/GcsSloop/AndroidNote/master/CustomView/Demo/dispatchTouchEventDemo.zip)
## 總結
View的事件分發機制實際上就是一個非常經典的責任鏈模式,如果你了解責任鏈模式,那么事件分發對你來說并不是什么難題,如果你不了解責任鏈模式,剛好借此機會學習一下啦。
> **責任鏈模式:**
>
> 當有多個對象均可以處理同一請求的時候,將這些對象串聯成一條鏈,并沿著這條鏈傳遞改請求,直到有對象處理它為止。
Android 中事件分發機制原理雖然非常簡單,但由于實際場景非常復雜,一旦具體到某個場景中變得很麻煩,而本文僅僅是帶你簡單的了解一下事件分發機制,更詳細的內容和具體的一些特殊情形處理會在后續文章中進行講解。
由于個人水平有限,文章中可能會出現錯誤,如果你覺得哪一部分有錯誤,或者發現了錯別字等內容,歡迎在評論區告訴我,另外,據說關注[作者微博](http://weibo.com/GcsSloop)不僅能第一時間收到新文章消息,還能變帥哦。
## 參考資料
[Activity](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/app/Activity.java)<br/>
[PhoneWindow](https://android.googlesource.com/platform/frameworks/base/+/696cba573e651b0e4f18a4718627c8ccecb3bda0/policy/src/com/android/internal/policy/impl/PhoneWindow.java)<br/>
[ViewGroup](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java)<br/>
[View](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/View.java)<br/>
[Android事件傳遞機制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/)<br/>
[Android ViewGroup/View 事件分發機制詳解](http://anany.me/2015/11/08/touchevent/)<br/>
[ Android事件分發機制完全解析,帶你從源碼的角度徹底理解(上)](http://blog.csdn.net/guolin_blog/article/details/9097463)<br/>
[ Android事件分發機制完全解析,帶你從源碼的角度徹底理解(下)](http://blog.csdn.net/sinyu890807/article/details/9153747)<br/>
[更簡單的學習Android事件分發](http://www.idtkm.com/customview/customview11/)<br/>
《安卓開發藝術探索》
## About Me
### 作者微博: <a href="http://weibo.com/GcsSloop" target="_blank">@GcsSloop</a>
<a href="http://www.gcssloop.com/info/about" target="_blank"><img src="http://ww4.sinaimg.cn/large/005Xtdi2gw1f1qn89ihu3j315o0dwwjc.jpg" width="300" style="display:inline;" /></a>
- 0-發現
- AndroidInterview-Q-A
- Android能讓你少走彎路的干貨整理
- LearningNotes
- temp
- temp11
- 部分地址
- 0-待辦任務
- 待補充列表
- 0-未分類
- AndroidView事件分發與滑動沖突處理
- Spannable
- 事件分發機制詳解
- 1-Java
- 1-Java-01基礎
- 未歸檔
- 你應該知道的JDK知識
- 集合框架
- 1-Java-04合集
- Java之旅0
- Java之旅
- JAVA之旅01
- JAVA之旅02
- JAVA之旅03
- JAVA之旅04
- JAVA之旅05
- JAVA之旅06
- JAVA之旅07
- JAVA之旅08
- JAVA之旅09
- java之旅1
- JAVA之旅10
- JAVA之旅11
- JAVA之旅12
- JAVA之旅13
- JAVA之旅14
- JAVA之旅15
- JAVA之旅16
- JAVA之旅17
- JAVA之旅18
- JAVA之旅19
- java之旅2
- JAVA之旅20
- JAVA之旅21
- JAVA之旅22
- JAVA之旅23
- JAVA之旅24
- JAVA之旅25
- JAVA之旅26
- JAVA之旅27
- JAVA之旅28
- JAVA之旅29
- java之旅3
- JAVA之旅30
- JAVA之旅31
- JAVA之旅32
- JAVA之旅33
- JAVA之旅34
- JAVA之旅35
- 1-Java-05辨析
- HashMapArrayMap
- Java8新特性
- Java8接口默認方法
- 圖解HashMap(1)
- 圖解HashMap(2)
- 2-Android
- 2-Android-1-基礎
- View繪制流程
- 事件分發
- AndroidView的事件分發機制和滑動沖突解決
- 自定義View基礎
- 1-安卓自定義View基礎-坐標系
- 2-安卓自定義View基礎-角度弧度
- 3-安卓自定義View基礎-顏色
- 自定義View進階
- 1-安卓自定義View進階-分類和流程
- 10-安卓自定義View進階-Matrix詳解
- 11-安卓自定義View進階-MatrixCamera
- 12-安卓自定義View進階-事件分發機制原理
- 13-安卓自定義View進階-事件分發機制詳解
- 14-安卓自定義View進階-MotionEvent詳解
- 15-安卓自定義View進階-特殊形狀控件事件處理方案
- 16-安卓自定義View進階-多點觸控詳解
- 17-安卓自定義View進階-手勢檢測GestureDetector
- 2-安卓自定義View進階-繪制基本圖形
- 3-安卓自定義View進階-畫布操作
- 4-安卓自定義View進階-圖片文字
- 5-安卓自定義View進階-Path基本操作
- 6-安卓自定義View進階-貝塞爾曲線
- 7-安卓自定義View進階-Path完結篇偽
- 8-安卓自定義View進階-Path玩出花樣PathMeasure
- 9-安卓自定義View進階-Matrix原理
- 通用類介紹
- Application
- 2-Android-2-使用
- 2-Android-02控件
- ViewGroup
- ConstraintLayout
- CoordinatorLayout
- 2-Android-03三方使用
- Dagger2
- Dagger2圖文完全教程
- Dagger2最清晰的使用教程
- Dagger2讓你愛不釋手-終結篇
- Dagger2讓你愛不釋手-重點概念講解、融合篇
- dagger2讓你愛不釋手:基礎依賴注入框架篇
- 閱讀筆記
- Glide
- Google推薦的圖片加載庫Glide:最新版使用指南(含新特性)
- rxjava
- 這可能是最好的RxJava2.x入門教程完結版
- 這可能是最好的RxJava2.x入門教程(一)
- 這可能是最好的RxJava2.x入門教程(三)
- 這可能是最好的RxJava2.x入門教程(二)
- 這可能是最好的RxJava2.x入門教程(五)
- 這可能是最好的RxJava2.x入門教程(四)
- 2-Android-3-優化
- 優化概況
- 各種優化
- Android端秒開優化
- apk大小優化
- 內存分析
- 混淆
- 2-Android-4-工具
- adb命令
- 一鍵分析Android的BugReport
- 版本控制
- git
- git章節簡述
- 2-Android-5-源碼
- HandlerThread 源碼分析
- IntentService的使用和源碼分析
- 2-Android-9-辨析
- LRU算法
- 什么是Bitmap
- 常見圖片壓縮方式
- 3-Kotlin
- Kotlin使用筆記1-草稿
- Kotlin使用筆記2
- kotlin特性草稿
- Kotlin草稿-Delegation
- Kotlin草稿-Field
- Kotlin草稿-object
- 4-JavaScript
- 5-Python
- 6-Other
- Git
- Gradle
- Android中ProGuard配置和總結
- gradle使用筆記
- Nexus私服搭建
- 編譯提速最佳實踐
- 7-設計模式與架構
- 組件化
- 組件化探索(OKR)
- 1-參考列表
- 2-1-組件化概述
- 2-2-gradle配置
- 2-3-代碼編寫
- 2-4-常見問題
- 2-9-值得一讀
- 8-數據結構與算法
- 0臨時文件
- 漢諾塔
- 8-數據-1數據結構
- HashMap
- HashMap、Hashtable、HashSet 和 ConcurrentHashMap 的比較
- 遲到一年HashMap解讀
- 8-數據-2算法
- 1個就夠了
- Java常用排序算法(必須掌握的8大排序算法)
- 常用排序算法總結(性能+代碼)
- 必須知道的八大種排序算法(java實現)
- 9-職業
- 閱讀
- 書單
- 面試
- 面試-01-java
- Java面試題全集駱昊(上)
- Java面試題全集駱昊(下)
- Java面試題全集駱昊(中)
- 面試-02-android
- 40道Android面試題
- 面試-03-開源源碼
- Android圖片加載框架最全解析(二),從源碼的角度理解Glide的執行流程
- 面試-07-設計模式
- 面試-08-算法
- 面試-09-其他
- SUMMARY
- 版權說明
- temp111