[TOC]
# 基礎知識
## Activity、PhoneWindow、DecorView

## 事件MotionEvent
MotionEvent負責處理報告所有類型的設備輸入事件,如手指觸摸、鼠標、觸控筆、軌跡球等。在實際開發中我們主要關注手指觸摸事件即可。
事件|說明
---|---
ACTION_DOWN|手指初次接觸到屏幕時觸發
ACTION_MOVE|手指在屏幕上滑動時觸發,可多次觸發
ACTION_UP|手指離開屏幕時觸發
ACTION_CANCEL|事件被上層攔截時觸發
ACTION_OUTSIDE|手指不在控件區域時觸發
## Input系統
本段出自[http://gityuan.com/2015/09/19/android-touch/](http://gityuan.com/2015/09/19/android-touch/)
當手指觸摸到屏幕時,屏幕硬件一行行不斷地掃描每個像素點,獲取到觸摸事件后,從底層產生中斷上報。再通過native層調用Java層InputEventReceiver中的dispatchInputEvent方法。經過層層調用,交由Activity的dispatchTouchEvent方法來處理。

# Activity 的事件分發
## DecorView的分發
首先從上圖中DecorView的dispatchTouchEvent看起:
```java
// DecorView.java
public boolean dispatchTouchEvent(MotionEvent ev) {
final Window.Callback cb = mWindow.getCallback();
return cb != null && !mWindow.isDestroyed() && mFeatureId < 0
? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
}
```
Activity在創建Window對象時,會為Window對象設置回調接口,也就是Activity自己,這樣Window在收到外界的狀態改變時,就會回調Activity的相應方法。
上面代碼中cb指Window.Callback,Activity實現了此接口,因此接下來調用Activity的dispatchTouchEvent:
```java
// Activty.java
public boolean dispatchTouchEvent(MotionEvent ev) {
// 捕獲用戶正在和設備交互,為了更方便的管理通知欄的通知,暫不重要
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 事件分發給 Window
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
// 當 PhoneWindow 里的所有View都不消費事件時(多用于處理落在Window范圍外部的觸摸事件)
return onTouchEvent(ev);
}
```
主要的代碼在Window的事件分發,一起來看看:
## PhoneWindow 的分發
先來看看 PhoneWindow 的分發:
```java
// PhoneWindow.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
```
可以看到,直接調用自身 DecorView 的 superDispatchTouchEvent 方法:
```java
// DecorView.java
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
```
而 DecorView 又直接調用了父類 ViewGroup 的 dispatchTouchEvent 方法。DecorView是一個FrameLayout,觸摸事件在這一步傳遞給了根 ViewGroup。
## PhoneWindow不處理時,Activity 的處理
當 PhoneWindow 不消費事件,也就是當前 Activity 的所有 View 都不消費事件時,事件會回傳給 Activity,調用 onTouchEvent 方法:
```java
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
```
mWindow 為 PhoneWindow 對象,在 PhoneWindow 代碼中未找到 shouldCloseOnTouch 方法,來看看其父類 Window 中的該方法:
```java
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
if (mCloseOnTouchOutside && event.getAction() == MotionEvent.ACTION_DOWN
&& isOutOfBounds(context, event) && peekDecorView() != null) {
return true;
}
return false;
}
```
當設置了 mCloseOnTouchOutside 為 true,并且為 MotionEvent.ACTION_DOWN 事件,手指在 Activity 界面以外,且當前 Activity 包含子 View 時,會返回 true,當前 Activity 會被 finish。
下面來仔細看看 ViewGroup 的事件分發。
# ViewGroup 的事件分發
## 概述
**分發事件**
方法:dispatchTouchEvent()
結果:返回值為true代表事件被當前View消費了,為false代表沒被消費
1、由于Android視圖結構為樹狀結構,所以觸摸事件都是從rootView開始依次向下進行傳遞,稱之為事件分發。
2、在沒有攔截的情況下,事件最終將會分發給手指觸摸區域的子 View,子 View 重疊時分發給最上層的子 View
**攔截事件**
方法:onInterceptTouchEvent()
1、事件在分發的過程中,可以被中途某個ViewGroup進行攔截,停止向下進行分發傳遞。
2、ViewGroup攔截后,直接調用其onTouchEvent方法看是否需要消費使用。

**使用事件**
方法:onTouchEvent(MotionEvent event)
定義:拿到MotionEvent實例,可以獲取到事件內容并使用
結果:onTouchEvent返回值為true時代表消費了事件,上層View不會再接收事件回傳;返回值為false時代表沒消費事件,可能只是使用了事件的內容,但沒消費,事件會繼續進行回傳。
事件被消費,就意味著事件信息傳遞終止
1、如果事件沒有被攔截,會按視圖層次結構依次往下進行傳遞,直到傳遞到最底層的View,也就是最終命中的View。
2、命中的View拿到事件后,會決定是否消費,如果消費了就沒上層View什么事了。
3、如果命中的View沒有消費事件,事件會回傳,依次調用上層View的onTouchEvent方法,看上層View是否需要使用事件做些什么。

## ViewGroup 的事件分發機制偽代碼:
這一段偽代碼來源于 GcsSloop 大神的[文章](http://www.gcssloop.com/customview/dispatch-touchevent-source),很有嚼勁。理解了這段代碼基本對于 Android 事件分發機制就有了宏觀上的把握了。配合吳小龍同學的這篇 [Android 事件傳遞機制分析](http://wuxiaolong.me/2015/12/19/MotionEvent/)服用效果更佳!
```java
// 返回值代表是否將事件消費掉
public boolean dispatchTouchEvent(MotionEvent event) {
// 默認狀態為未消費過
boolean result = false;
// 如果沒有攔截(ViewGroup一般不會進行攔截,可點擊的ViewGroup除外,下文源碼部分有分析)
if (!onInterceptTouchEvent(event)) {
// 則交給childView
result = child.dispatchTouchEvent(event);
}
// 如果事件沒有被childView消費
if (!result) {
// 則調用自身 onTouchEvent()
result = onTouchEvent(event);
}
// 返回事件消費狀態
return result;
}
```
## ViewGroup 的事件分發機制源碼
```java
public boolean dispatchTouchEvent(MotionEvent ev) {
//...
final boolean intercepted;
//這里首先對是否攔截此事件做了很多判斷
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//...
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = true;
}
//...
// 如果不攔截才會往下走進行分發
if (!canceled && !intercepted) {
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final View[] children = mChildren;
// 查找可以處理本次事件的childView
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(preorderedList, children, childIndex);
//...
// 將事件交給childView
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...
}
}
if (mFirstTouchTarget == null) {
// 此處會調用super.dispatchTouchEvent()
handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);
} else {
//...
}
return handled;
}
```
其中交給childView處理的dispatchTransformedTouchEvent方法源碼如下:
```java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
final boolean handled;
//...
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
//調用childView的dispatchTouchEvent方法
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
```
最后來看看剛剛 onInterceptTouchEvent 方法的默認實現:
```java
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
```
可以看到,會先檢查事件來源。當為鼠標事件,且當前為 ACTION_DOWN,且鼠標左鍵按下時,默認返回 true;大部分情況即非鼠標事件時,onInterceptTouchEvent 都是返回 false。
# View 的事件分發
在事件向下分發的過程中,如果中間沒有ViewGroup攔截事件,事件會一直傳遞到最底層的子View。調用View的dispatchTouchEvent方法。
## 分發順序
View 可以注冊很多事件監聽器,如:單擊事件(onClick)、長按事件(onLongClick)、觸摸事件(onTouch),并且View自身也有 onTouchEvent 方法。這些方法由 dispatchTouchEvent() 方法進行分發。
事件的調度順序為:onTouchListener > onTouchEvent > onLongClickListener > onClickListener
## View 的事件分發機制偽代碼
```java
// 返回值代表是否將事件消費掉
public boolean dispatchTouchEvent(MotionEvent event) {
if(mOnTouchListener.onTouch(this, event)) {
return true;
} else if (onTouchEvent(event)) {
return true;
}
return false;
}
```
onClickListener 和 onLongClickListener 在 onTouchEvent 中調用。
## View 的事件分發機制源碼:
```java
public boolean dispatchTouchEvent(MotionEvent event) {
//...
if (onFilterTouchEventForSecurity(event)) {
ListenerInfo li = mListenerInfo;
// 回調mOnTouchListener接口
if (li != null && li.mOnTouchListener != null&& (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// 調用onTouchEvent方法(在里面會回調onLongClickListener、onClickListener)
if (!result && onTouchEvent(event)) {
result = true;
}
}
//...
return result;
}
public boolean onTouchEvent(MotionEvent event) {
// 判斷View是否可點擊
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
//...
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
//...
// 執行單擊事件
performClick();
break;
case MotionEvent.ACTION_DOWN:
//...
// 檢測長按,并執行長按事件
checkForLongClick(0, x, y);
break;
}
// 只要是可點擊的,就會返回true,也就是當前View就會消費事件
return true;
}
return false;
}
```
## 點擊事件
View的onClick事件需要同時接收到ACTION_DOWN和ACTION_UP才能觸發,如果這兩個事件被分發給了不同的View,點擊事件就不會被觸發。
View只有消費了ACTION_DOWN事件,才能接收到后續的事件,并且后續事件傳遞來時,不會再傳遞給其他View,除非在途中被其他View攔截。如果后續事件確實被攔截,當前View會收到一個ACTION_CANCEL的事件,表示事件結束,不會有后續事件。
一次觸摸流程中產生的事件應被同一 View 消費,全部接收或者全部拒絕。
## 注意要點
* 只要給 View 注冊了 onClickListener、onLongClickListener、OnContextClickListener 其中的任何一個監聽器或者設置了 android:clickable=”true” 就代表這個 View 是可點擊的。
* 可點擊的 View 就會消費事件,不可點擊的 View 不會消費事件
* ViewGroup 和 ChildView 同時注冊了點擊事件監聽器時,事件優先給 ChildView 消費
* 同一次點擊事件只能被一個 View 消費,防止事件混亂
# 總結
對于ViewGroup來說:
* 用戶觸摸事件從rootView依次向下進行傳遞。在沒有攔截的情況下,事件會依次傳遞到最底部子View
* 事件分發過程中,可被中途某個ViewGroup進行攔截,停止向下進行分發傳遞。ViewGroup攔截后直接調用其onTouchEvent看是否需要消費使用,如不消費事件往上進行回傳
* 如果事件沒被攔截順利傳遞給最底層的View,命中的View會決定是否消費,消費了就沒上層View的事情了
* 子View如果不消費事件,事件會進行回傳,每個層級的ViewGroup都可以拿到事件并決定是否消費
* 如果事件一直沒有被消費,最后會回傳給 Activity,如果 Activity 也不需要就被拋棄
對于子View來說:
* 只要注冊了onClickListener、onLongClickListener、onContextClickListener或設置了clickable="true",就代表時可點擊的
* 可點擊的View就會消費事件,不可點擊的View不會消費事件
* 當ChildView重疊時,一般會分配給顯示在最上面的ChildView
# 補充
關于 ViewGroup 對于觸摸事件的分發流程圖,可參考下面這幾幅圖,[來源](https://blog.csdn.net/xyz_lmn/article/details/12517911)
1、都不攔截且都不消費的情況

2、View 消費了事件,不再進行回傳

3、View 消費了 ACTION_DOWN

View 消費了 ACTION_DOWN,但后續的 ACTION_MOVE 和 ACTION_UP 被上層攔截了,會給它個ACTION_CANCEL

4、上層一開始就攔截了事件,此時子 View 對于事件無感

# 參考文檔:
[安卓自定義View進階-事件分發機制詳解](http://www.gcssloop.com/customview/dispatch-touchevent-source)
- 導讀
- Java知識
- Java基本程序設計結構
- 【基礎知識】Java基礎
- 【源碼分析】Okio
- 【源碼分析】深入理解i++和++i
- 【專題分析】JVM與GC
- 【面試清單】Java基本程序設計結構
- 對象與類
- 【基礎知識】對象與類
- 【專題分析】Java類加載過程
- 【面試清單】對象與類
- 泛型
- 【基礎知識】泛型
- 【面試清單】泛型
- 集合
- 【基礎知識】集合
- 【源碼分析】SparseArray
- 【面試清單】集合
- 多線程
- 【基礎知識】多線程
- 【源碼分析】ThreadPoolExecutor源碼分析
- 【專題分析】volatile關鍵字
- 【面試清單】多線程
- Java新特性
- 【專題分析】Lambda表達式
- 【專題分析】注解
- 【面試清單】Java新特性
- Effective Java筆記
- Android知識
- Activity
- 【基礎知識】Activity
- 【專題分析】運行時權限
- 【專題分析】使用Intent打開三方應用
- 【源碼分析】Activity的工作過程
- 【面試清單】Activity
- 架構組件
- 【專題分析】MVC、MVP與MVVM
- 【專題分析】數據綁定
- 【面試清單】架構組件
- 界面
- 【專題分析】自定義View
- 【專題分析】ImageView的ScaleType屬性
- 【專題分析】ConstraintLayout 使用
- 【專題分析】搞懂點九圖
- 【專題分析】Adapter
- 【源碼分析】LayoutInflater
- 【源碼分析】ViewStub
- 【源碼分析】View三大流程
- 【源碼分析】觸摸事件分發機制
- 【源碼分析】按鍵事件分發機制
- 【源碼分析】Android窗口機制
- 【面試清單】界面
- 動畫和過渡
- 【基礎知識】動畫和過渡
- 【面試清單】動畫和過渡
- 圖片和圖形
- 【專題分析】圖片加載
- 【面試清單】圖片和圖形
- 后臺任務
- 應用數據和文件
- 基于網絡的內容
- 多線程與多進程
- 【基礎知識】多線程與多進程
- 【源碼分析】Handler
- 【源碼分析】AsyncTask
- 【專題分析】Service
- 【源碼分析】Parcelable
- 【專題分析】Binder
- 【源碼分析】Messenger
- 【面試清單】多線程與多進程
- 應用優化
- 【專題分析】布局優化
- 【專題分析】繪制優化
- 【專題分析】內存優化
- 【專題分析】啟動優化
- 【專題分析】電池優化
- 【專題分析】包大小優化
- 【面試清單】應用優化
- Android新特性
- 【專題分析】狀態欄、ActionBar和導航欄
- 【專題分析】應用圖標、通知欄適配
- 【專題分析】Android新版本重要變更
- 【專題分析】唯一標識符的最佳做法
- 開源庫源碼分析
- 【源碼分析】BaseRecyclerViewAdapterHelper
- 【源碼分析】ButterKnife
- 【源碼分析】Dagger2
- 【源碼分析】EventBus3(一)
- 【源碼分析】EventBus3(二)
- 【源碼分析】Glide
- 【源碼分析】OkHttp
- 【源碼分析】Retrofit
- 其他知識
- Flutter
- 原生開發與跨平臺開發
- 整體歸納
- 狀態及狀態管理
- 零碎知識點
- 添加Flutter到現有應用
- Git知識
- Git命令
- .gitignore文件
- 設計模式
- 創建型模式
- 結構型模式
- 行為型模式
- RxJava
- 基礎
- Linux知識
- 環境變量
- Linux命令
- ADB命令
- 算法
- 常見數據結構及實現
- 數組
- 排序算法
- 鏈表
- 二叉樹
- 棧和隊列
- 算法時間復雜度
- 常見算法思想
- 其他技術
- 正則表達式
- 編碼格式
- HTTP與HTTPS
- 【面試清單】其他知識
- 開發歸納
- Android零碎問題
- 其他零碎問題
- 開發思路