---
title: 'View 事件分發 / 滑動衝突'
disqus: kyleAlien
---
View 事件分發 / 滑動衝突
===
## Overview of Content
:::success
* 如果喜歡讀更好看一點的網頁版本,可以到我新做的網站 [**DevTech Ascendancy Hub**](https://devtechascendancy.com/)
本篇文章對應的是 [**Android View 事件分發:處理滑動衝突 | 內部、外部事件攔截**](https://devtechascendancy.com/android-view-event_conflict-interception/)
:::
* 以下的 Android 源碼會採用 [**Android-10**](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/view/View.java) 的源碼
[TOC]
## 點擊事件傳入
點擊事件是從 **Activity(起點) 透過一層層傳遞至 View(終點) 中**,下圖是一個示意圖,至於若對建構 View 有興趣可以參考 [**LayoutInflater 分析**](https://hackmd.io/-2Uta0yBSbyhNayaev5eyg#LayoutInflater-%E5%8A%A0%E8%BC%89)
最終傳入 View 中讓其處理點擊事件 (從最外部 Acitivty -> Window -> View)
> 
### 事件分發概念
* 由於 View 的分發實作細節比較多,但我們要關注的 "**分發**",主要是由 3 個方法來完成
| 分發重點方法 | 功能 | 返回意義 |
| -------- | -------- | -------- |
| dispatchTouchEvent : boolean | 由上層 View 被觸發,傳遞至目標 View | 返回結果會由 `onTouchEvent`、子 View 的 `dispatchTouchEvent` 影響 (true: 被處理) |
| onInterceptTouchEvent : boolean | 在當前 View 中,用來判斷是否攔截某個事件 | 返回結果表示該事件是否被攔截 (true: 被攔截) |
| onTouchEvent : boolean | 當前 View 已經攔截,開始處理是建 | 返回結果代表該事件是否被消耗 (true: 被處理) |
* 以下是 View 事件分發的偽程式,可以很好的描述出 View 事件分發概念
```java=
// View 偽程式
public boolean dispatchTouchEvent(MotionEvent e) {
bool isEventConsume = false;
if(onInterceptTouchEvent(e)) { // 是否攔截分發
isEventConsume = onTouchEvent(e);
} else {
isEventConsume = child.dispatchTouchEvent(e);
}
return isEventConsume;
}
```
1. 可以看出分發順序 `dispatchTouchEvent` -> `onInterceptTouchEvent`,在依照是否消耗來決定之後的走向
2. `dispatchTouchEvent` 的遞迴調用(若事件沒有被消耗,就會往子成員 `child` 繼續呼叫),直到找到消耗事件的 View
:::info
Android View 是一個 [**組件模式**](https://devtechascendancy.com/object-oriented_programming_composite/)、[**裝飾模式**](https://devtechascendancy.com/object-oriented_design_decorate/) 的混合設計模式,它擁有加強或是弱化抽象,還有遞歸抽象… 等等程式設計特性
:::
> 
* 若該 View 已經攔截點擊事件,則會觸發 onTouch
1. `onTouch` 若沒消費該事件,才會傳遞給 `onTouchEvent` 方法
2. `onTouchEvent` 內才會有 onClick 事件
> 
### Activity 接收點擊事件
* Activity (AppCompatActivity)、Windows (PhoneWindow) 之間的關係可以參考另一篇 [**Activity 布局**](https://hackmd.io/-2Uta0yBSbyhNayaev5eyg#AppCompatActivity),下圖表示了 `AppCompatActivity` 如何與 `PhoneWindow` 產生關係
> 
* **點擊事件從 ==Activity#dispatchTouchEvent== 開始分析**,一路會分析進入 `PhoneWindow`、`DecorWindow`,最後到達 DecorView#ViewGroup (最頂層 ViewGroup) 中的 **dispatchTouchEvent 方法**
```java=
/**
* Activity.java
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
// 這裡的 getWindow 就是 PhoneWindow 類
// @ 分析 superDispatchTouchEvent
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
// -------------------------------------------------------------
/**
* PhoneWindow.java
*/
private DecorView mDecor;
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
// -------------------------------------------------------------
/**
* DecorView.java
*
* DecorView 繼承於 FrameLayout,而 FrameLayout 並沒有 Overidde
* dispatchTouchEvent 方法,所以必須往它的父類尋找
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
// -------------------------------------------------------------
/**
* ViewGroup.java
*
* ViewGroup 繼承於 View
* ViewGroup 有重寫 dispatchTouchEvent,所以不用繼續往 View 去
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略
}
```
:::info
* 從這裡可以看出 **事件分發會由 PhoneWindow 中的 DecorView 開始**
:::
> 
* **Activity & View 關係圖**
> 
## 事件處理
透過上面分析,我們就知道點擊 **事件是如何傳遞至 DecorView#ViewGroup 中**,這裡我們會再分 ViewGroup 點擊事件來分析
- **先了解幾個 MotionEvent**
| MotionEvent 事件 | 動作 | 其他 |
| -------- | -------- | -------- |
| ACTION_DOWN | 手指下壓 | 又份為攔截、不攔截 |
| ACTION_UP | 手指抬起 | 事件結束 |
| ACTION_MOVE | 在螢幕滑動 | 會被 **多次觸發** |
| ACTION_CANCEL | 事件取消 | **事件被上層攔截時候觸發** |
### [ViewGroup](https://cs.android.com/android/platform/superproject/+/master:frameworks/base/core/java/android/view/ViewGroup.java) 處理 `ACTION_DOWN`:ChildView 攔截
* **==Down 事件只會觸發一次== (單點觸控,多點觸控就不只一次)**,攔截就是該 ViewGroup **自己處理事件**,不會對 ChildView 分發
* 每個點擊事件都是以 Action Down 開始,**細節請注意以下的++註解++**,而它主要做的事情有 (這裡會先列出主要的處理項目)
1. 清除先前的事件:注意 **resetTouchState 方法,它會在 ViewGroup#`dispatchTouchEvent` 事件是 ACTION_DOWN 時執行**
> ViewGroup 中有一個 member 是 **TouchTarget**:它用來串接該點擊事件
```java=
// ViewGroup.java
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 取得目前行為
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 在 Action Down 時才執行 reset
// @ 追蹤 cancelAndClearTouchTargets 方法
cancelAndClearTouchTargets(ev);
// @ 追蹤 resetTouchState 方法
resetTouchState();
}
}
... 省略部分
return handled;
}
private void resetTouchState() {
// @ 查看 clearTouchTargets 方法
clearTouchTargets();
resetCancelNextUpFlag(this);
// 清除 FLAG_DISALLOW_INTERCEPT、允許 ViewGroup 攔截
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
// 清除第一個點擊 View 串列的所有事件
private void clearTouchTargets() {
// TouchTarget 是單向鏈表
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
// 回收 TouchTarget 方便覆用
target.recycle();
target = next;
} while (target != null);
// @ 將成員 mFirstTouchTarget 至為 null
mFirstTouchTarget = null;
}
}
```
:::info
* 我們來試運算,使用上面的 `~&` 公式,是否能清除 `FLAG_DISALLOW_INTERCEPT` 這個 Flag 值
```shell=
## 假設 mGroupFlags 為 0x8A (0b1000 1010)
## FLAG_DISALLOW_INTERCEPT 為 0x80 (這是假設值 0b1000 0000)
## 首先反向
~FLAG_DISALLOW_INTERCEPT
## Ans: 0b0111 1111
## 做 And 運算
mGroupFlags & (0b0111 1111)
## (0b1000 1010) & (0b0111 1111)
Ans: 0b0000 1010
```
:::
2. 是否會呼叫 ViewGroup#**onInterceptTouchEvent** 方法:會有兩個條件,再加上一個 FLAG 判斷:
1. 目前是 ACTION_DOWN 事件
2. 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 mFirstTouchTarget 賦值)
3. 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 `ACTION_DOWN` 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法)
:::success
* **FLAG_DISALLOW_INTERCEPT 判斷**:
可以透過 ViewGroup#requestDisallowInterceptTouchEvent 方法控制,**通常是 ChildView 在要求 Parent 不要攔截使用**
同時如果 ChildView 如果使用 requestDisallowInterceptTouchEvent 時也要同時禁止 ViewGroup 對 `ACTION_DOWN` 的攔截 (**請看第一點**)
:::
```java=
// ViewGroup.java
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 取得目前行為
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action down 時 ...初始化 FLAG_DISALLOW_INTERCEPT
final boolean intercepted;
// 這裡有兩個判斷 決定是否呼叫 ViewGroup 自己的 onInterceptTouchEvent 方法
// 1. 目前是 ACTION_DOWN
// 2. 已經有子 View 處理這個點擊事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判斷是否禁止 攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 呼叫自身的 onInterceptTouchEvent 方法
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
... 省略部分
return handled;
}
```
> 
3. 若 ViewGroup 自身沒有攔截,就會 ++**遞迴 ChildView**++:並一一執行 **ViewGroup#dispatchTransformedTouchEvent** 分發給每個 ChildView 處理
- 若 ChildView 消耗事件則返回 true,則跳出迴圈
- 否則往下一個 ChildView 詢問
```java=
// ViewGroup
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 取得目前行為
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Action down 時 ...初始化 FLAG_DISALLOW_INTERCEPT
final boolean intercepted;
// 判斷自身 ViewGroup 是否可以攔截事件
// 目前假設 ViewGroup 不攔截
if (!canceled && !intercepted) {
...
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
// 重新排列在該 ViewGroup 中的 ChildView 們的順序
final ArrayList<View> preorderedList =
buildTouchDispatchChildList();
...
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
...
// 是否動畫中 canReceivePointerEvents
// 點擊是否在 child 元素內 isTransformedTouchPointInView
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 獲取點擊中的 View
// 新的 View 事件才會返回 非 null
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Give it the new pointer in addition to the ones it is handling.
// Child View 已經接收到點擊
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// @ 重點在 dispatchTransformedTouchEvent 方法
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// ChildView 處理事件成功
... 省略部分
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// @ addTouchTarget 方法
// 內會賦予 mFirstTouchTarget 值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 跳出迴圈
break;
}
}
}
}
}
}
... 省略部分
return handled;
}
```
:::success
* 從這裡可以看出來 **View 是一個多元樹**,並且在這裡使用 [**責任鏈 OOP 設計**](https://devtechascendancy.com/object-oriented_design_chain_framework/)
> 
:::
:::info
* **按照螢幕上的 Z 軸**,重新排列 ViewGroup 中 ChildView 的順序
```java=
// ViewGroup
public ArrayList<View> buildTouchDispatchChildList() {
return buildOrderedChildList();
}
ArrayList<View> buildOrderedChildList() {
// 全部 ChildView 的數量
final int childrenCount = mChildrenCount;
...
final boolean customOrder = isChildrenDrawingOrderEnabled();
for (int i = 0; i < childrenCount; i++) {
// add next child (in child order) to end of list
final int childIndex = getAndVerifyPreorderedIndex(childrenCount, i, customOrder);
final View nextChild = mChildren[childIndex];
final float currentZ = nextChild.getZ();
// insert ahead of any Views with greater Z
int insertIndex = i;
while (insertIndex > 0 && mPreSortedChildren.get(insertIndex - 1).getZ() > currentZ) {
insertIndex--;
}
// Z 軸越大則擺放至越前面
mPreSortedChildren.add(insertIndex, nextChild);
}
return mPreSortedChildren;
}
```
:::
* [**ViewGroup**](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java)#**dispatchTransformedTouchEvent** 方法:**ViewGroup 會透過該方法把事件傳遞給 ChildView**
> 內部其實就會針對 ChildView 執行 **dispatchTouchEvent 方法**
1. 若是傳入的 child 是 null 就呼叫 ViewGroup 父類的 dispatchTouchEvent
2. 不是 null 則呼叫指定 View 的 dispatchTouchEvent (**當前情況就是有傳入 View 物件,所以會轉跳到指定 View#dispatchTouchEvent 方法**)
```java=
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event,
boolean cancel,
View child,
int desiredPointerIdBits) {
final boolean handled;
... 省略部分
// 重點在個判斷
if (child == null) {
// 如果 Child view 為 null 則回傳給 ViewGroup 的 Parent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
... 目前走這裡
handled = child.dispatchTouchEvent(transformedEvent);
}
return handled;
}
```
* ViewGroup#**addTouchTarget**:賦予 `mFirstTouchTarget` 成員
```java=
// ViewGroup.java
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
// 取覆用的 TouchTarget 物件
final TouchTarget target = TouchTarget
.obtain(child, pointerIdBits);
// 串接上一個事件的 View
target.next = mFirstTouchTarget;
// 第一個 處理點擊事件 的 View
mFirstTouchTarget = target;
return target;
}
```
> 
:::success
一開始 target.next 為 null,通過 TouchTarget#obtain 獲得對象 (相當於 new 的功能),並將 next 只給前一個 View
**最後再將剛剛獲得的對象賦予 `mFirstTouchTarget`** 並返回
:::
4. 在 ChildView 接收並攔截事件後 (透過 `dispatchTouchEvent` 方法),**會賦予該 ViewGroup#mFirstTouchTarget 值**、並跳出迴圈
* ChildView 處理完事件後,返回到 ViewGroup 繼續處理
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
if (onFilterTouchEventForSecurity(ev)) {
... 省略上面分析 (ChildView 分發,跳出 for 迴圈)
// 目前情況, ChildView 已經處理事件,所以 mFirstTouchTarget 不為 null
if (mFirstTouchTarget == null) {
// 下一小節在分析
} else {
... 目前判斷走這
TouchTarget predecessor = null;
// 尋找目前的點擊事件相對的 ChildView
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 分發事件
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
}
...
return handled;
}
```
:::info
到這裡就分析完 ViewGroup 不攔截事件,ChildView 透過 **View#dispatchTouchEvent** 收到事件
:::
### [ViewGroup](https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/view/ViewGroup.java) 處理 `ACTION_DOWN`:ChildView 不攔截
:::success
ChildView 不攔截代表 **ViewGroup 要自己決定要不要處理該事件**
:::
* ViewGroup ChildView 攔截事件:處理方式相同 (上一小節),這裡主要看看不同的部分,該事件由 ViewGroup 自己處理 (dispatchTransformedTouchEvent 方法)
```java=
// ViewGroup.java
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// 取得目前行為
final int action = ev.getAction();
// 與 Mask 進行 and 操作,取得真正的 Action
final int actionMasked = action & MotionEvent.ACTION_MASK;
if (actionMasked == MotionEvent.ACTION_DOWN) {
// 在 Action Down 時才執行 reset
cancelAndClearTouchTargets(ev);
resetTouchState();
}
}
... 省略部分
return handled;
}
```
1. 清除先前的事件:注意 **resetTouchState 方法,它會在 ViewGroup#dispatchTouchEvent 事件是 `ACTION_DOWN` 時執行**
> 同上,請參考上一小節
2. 是否會呼叫 ViewGroup#**onInterceptTouchEvent** 方法:會有兩個條件,再加上一個 FLAG 判斷
1. 目前是 `ACTION_DOWN` 事件
2. 已經有 ChildView 處理這個點擊事件 (如果有子 View 處理事件就會給 `mFirstTouchTarget` 賦值)
3. 目前 ViewGroup 是否被禁止攔截 (一般 ViewGroup 接收到 `ACTION_DOWN` 時,如果沒有禁止攔截的話,就會執行 onInterceptTouchEvent 方法)
> 同上,請參考上一小節
3. 若 ViewGroup 自身沒有攔截,就會遞迴 ChildView:並一一執行 **ViewGroup#dispatchTransformedTouchEvent** 分發給每個 ChildView 處理
```java=
// ViewGroup
// 從該 ViewGroup 開始,往下串接點擊的目標
private TouchTarget mFirstTouchTarget;
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
...
if (!canceled && !intercepted) {
// 這邊會循環該 ViewGroup 中的所有的 ChildView,看有沒有 View 處理事件
//
// 目前狀況是都沒有 ChildView 要處理這個事件
}
}
...
}
```
4. ChildView 接收但 **全部都不攔截事件**,**ViewGroup#mFirstTouchTarget 為 null**
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
if (onFilterTouchEventForSecurity(ev)) {
... 省略上面分析 (ChildView 分發)
// 目前情況,沒有 ChildView 處理事件
if (mFirstTouchTarget == null) {
// 注意傳入的第三個參數 null
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
}
...
return handled;
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
... 省略部分
if (child == null) {
// 回到 ViewGroup 的 Parent dispatchTouchEvent
handled = super.dispatchTouchEvent(transformedEvent);
} else {
...
}
...
return handled;
}
```
:::info
這裡可以看出 **ViewGroup 預設不處理事件**,直接將事件返回到上一層 View
:::
### [View](https://cs.android.com/android/platform/superproject/+/android-10.0.0_r1:frameworks/base/core/java/android/view/View.java) 收到事件:處理 `ACTION_UP`
:::success
假設 ViewGroup 不攔截事件,最終會呼叫目標 View#`dispatchTouchEvent` 方法,**讓 View 處理事件**
:::
* **View 首先分發給 onTouch 若是沒有處理則分發到 onTouchEvent**,順序如下
1. 判斷 `onTouch` 接口:**最先執行 onTouch 接口,若是已經處理,就 onTouchEvent 就不會接收到事件**
2. 判斷 `onTouchEvent` 接口:若 onTouch 沒有處理這個事件,就會輪到該 View 的 onTouchEvent 處理事件
```java=
// View.java
public boolean dispatchTouchEvent(MotionEvent event) {
... 省略部分
boolean result = false;
... 省略部分
if (onFilterTouchEventForSecurity(event)) {
...
// 最一開始事件分發到 onTouch
ListenerInfo li = mListenerInfo;
if (li != null && li.mOnTouchListener != null
// 判斷該 View 是 enable
&& (mViewFlags & ENABLED_MASK) == ENABLED
&& li.mOnTouchListener.onTouch(this, event)) {
result = true;
}
// @ 分析 onTouchEvent
if (!result && onTouchEvent(event)) {
result = true;
}
}
... 省略
return result;
}
```
* 在 View#**onTouchEvent** 方法中,可以看到 onClick、onLongClick 接口的呼叫
```java=
// View.java
// PerformClick 代表該 View 的點擊事件
private PerformClick mPerformClick;
public boolean onTouchEvent(MotionEvent event) {
final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();
// 只要設定 CLICKABLE、LONG_CLICKABLE、CONTEXT_CLICKABLE 其中一個,
// 那該 View 就是可點擊的
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
// 點擊事件的代理 TouchDelegate
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
... 省略部分
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
...
// 長按任務 mHasPerformedLongPress
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// 移除長按 Callback
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
// @ 分析 performClickInternal
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
}
}
}
```
* 這邊可以看到 View 的點擊事件是 **透過 Handler 傳送 Click 點擊任務 (Runnable),這樣就不會造成點擊事件堵塞**
```java=
// View.java
public boolean post(Runnable action) {
final AttachInfo attachInfo = mAttachInfo;
if (attachInfo != null) {
// 將 Click 任務放入 Handler
return attachInfo.mHandler.post(action);
}
getRunQueue().post(action);
return true;
}
private boolean performClickInternal() {
notifyAutofillManagerOnClick();
// @ 分析 performClick
return performClick();
}
public boolean performClick() {
notifyAutofillManagerOnClick();
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
// 執行使用者的 OnClick 接口
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
```
> 
### ViewGroup:ACTION_DOWN 總結
1. 判斷事件是否被 ViewGroup 攔截:
* ViewGroup 攔截事件 -> 直接走 `3`
* ViewGroup 不攔截事件 -> 先走 `2` 再走 `3`
2. ViewGroup 不攔截,並有其中一個 ChildView 攔截事件並處理
* ViewGroup#newTouchTarget 被賦予值
* 分發到自身的 View (`super.dispatchTouchEvent` 呼叫自己的父類)
```java=
// ViewGroup.java
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
... 省略部分
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
// @ addTouchTarget 方法內會賦予 mFirstTouchTarget 值
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
// 跳出迴圈
break;
}
```
3. 事件分發、處理:有兩種情況
* 所有 ChildView 不攔截這個事件,**所以 mFirstTouchTarget 為 null**,相當於是最後一個 View
> 因為沒有任何 ChildView 處理,所以 **ViewGroup#View 自己處理 Down 事件**
```java=
// 分發程式
if (mFirstTouchTarget == null) {
// 第三個參數為 null
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
```
* 有 ChildView 攔截,**所以 mFirstTouchTarget 不為 null**,並且 while 只循環一次,因為 **ChildView 已處理 (分發時處理)**
```java=
// 分發程式
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
}
```
### ACTION_MOVE:View 收到 MOVE
* **Move 事件仍會先走 DecorView 這個 ViewGroup**,在執行 `ACTION_MOVE` 事件時先需要知道幾件事情
1. **ViewGroup 不會清理點擊的 Flag**
2. TouchTarget 類型的 **`mFirstTouchTarget` 元素 ++不為空++** (因為已經有 ChildView 元素處理)
* ACTION_MOVE 事件的判斷流程圖如下,它主要做的事情是
1. ViewGroup 是否攔截事件 (以下預設不攔截事件)
2. 不攔截、**==Move 不分發事件==**
3. 分發 or 處理
> 
* 同樣先來觀察 ViewGroup#dispatchTouchEvent 方法,並從這裡開始分析
1. 不會進入 reset 環節
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 不清理 Flag,不會進入 reset 環節
if (actionMasked == MotionEvent.ACTION_DOWN) {
... 省略
}
}
```
2. 判斷當前 ViewGroup 是否有禁止攔截 `FLAG_DISALLOW_INTERCEPT`,如果沒有的話就調用自身的 `onInterceptTouchEvent` 方法 檢查是否攔截
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
// Check for interception.
// 由於 mFirstTouchTarget 不為空,所以會進入
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判斷 ViewGroup 是否禁止攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 預設 ViewGroup 不攔截
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
}
```
3. 不是 `ACTION_DOWN` 事件,所以不會進入最初的 `ACTION_DOWN` 事件分發環節
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
// 注意這兩個區域變數,下面會用到
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
// 進入
if (!canceled && !intercepted) {
// 2. 不是 Down 事件,所以不進入
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 省略... 由於不進入,所以不會對 ChildView 分發
}
}
... 省略部分
}
```
4. `ACTION_MOVE` 的重點在事件分發 (請看註解)
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
// mFirstTouchTarget 不為空 (因為已經是 View 處理事件)
if (mFirstTouchTarget == null) {
...
} else {
// ++重點在這++TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
// alreadyDispatchedToNewTouchTarget 是 false
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// @ 分析 dispatchTransformedTouchEvent 分發給 ChildView
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
...
}
predecessor = target;
target = next;
}
}
}
```
* 接下來透過 dispatchTransformedTouchEvent 方法:分發 `ACTION_MOVE` 事件給 ChildView 的 dispatchTouchEvent
```java=
// ViewGroup.java
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits)
...省略部分
if (child == null) {
...
} else {
// 加設有 ChildView 則會進入這裡
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
}
```
### ACTION_MOVE:ViewGroup 攔截 MOVE
* 先了解目前情況:已經有 ChildView 處理 ACTION_DOWN 事件,而我們自訂一個 ViewGroup 並複寫 ViewGroup#**onInterceptTouchEvent** 方法,**==在 `ACTION_VIEW` 時返回 true 攔截==**
```java=
// 自定義 ViewGroup.java
public class MyViewGroup extends View {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_MOVE) {
// ViewGroup 自己消耗事件
return true;
}
return super.onInterceptTouchEvent(ev);
}
}
```
* **ACTION_MOVE 事件是多次發生**
1. 第一個 ACTION_MOVE (++從 ParentView 進來++)的目的是:^1.^ 取消 ChildView 事件(**事件改為 ACTION_CANCEL**)、^2.^ 將 **ViewGroup#mFirstTouchTarget 置為空**,這時 ParentView 是不處理事件的
結果:這時 **ChildView 會收到 ACTION_CANCEL 事件** (事件被取消,之後都會由 ViewGroup 處理事件)
```java=
/**
* ViewGroup.java
*
* 由於複寫 onInterceptTouchEvent 方法會讓 Move 事件為 ture,所以 cancelChild 也為 ture,
* 傳入 dispatchTransformedTouchEvent 方法的 cancel 參數為 ture,
*
* 這時事件就會被改為 ACTION_CANCEL,下面的 ChildView 就會接收到 ACTION_CANCEL 事件
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//... 省略
if (mFirstTouchTarget == null) {
...
} else {
TouchTarget predecessor = null;
TouchTarget target = mFirstTouchTarget;
while (target != null) {
final TouchTarget next = target.next;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// ACTION_MOVE 第一次進入
// ^1^ 重點 intercepted 是 true,所以 cancelChild = true
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// ^2^ 重點,mFirstTouchTarget = next,next 為 null
// 所以 mFirstTouchTarget = null
if (cancelChild) {
if (predecessor == null) {
// next 為空,所以 mFirstTouchTarget 置為空 (next 為空)
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
final int oldAction = event.getAction();
// cancel 為 true 會進入
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
// 改變事件為 ACTION_CANCEL
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
...
} else {
// 往下分發 ChildView 的事件就為 cancel
// 事件被上層攔截時觸發
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
... 省略部分
return handled;
}
```
2. 第二個 ACTION_MOVE(從 ParentView 進來),由於 mFirstTouchTarget 為空,所以 ParentView 不會分發
結果:**ParentView 自己處理事件**
```java=
/**
* ViewGroup.java
*/
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
// 在第一個 ACTION_MOVE 時 mFirstTouchTarget 被置為空,所以不會進入
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// ...省略,因為 mFirstTouchTarget = null
} else {
intercepted = true;
}
// 由於 intercepted = true 也就是攔截,就不會進入
if (!canceled && !intercepted) {
//... 分發部份
}
// 進入
if (mFirstTouchTarget == null) {
// ChildView 為空 (第三個參數)
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
...
}
...
}
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
... 省略部分
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
transformedEvent.offsetLocation(offsetX, offsetY);
if (! child.hasIdentityMatrix()) {
transformedEvent.transform(child.getInverseMatrix());
}
handled = child.dispatchTouchEvent(transformedEvent);
}
// Done.
transformedEvent.recycle();
return handled;
}
```
## 事件衝突
為何會發生衝突 ? 因為**事件只會有一個**,若是響應的元件不是自己要的 View 原件,這時就可以稱之為事件衝突
> Ex: ViewPager 包裹 RecyclerView
* 分為兩種方法處理
1. **內部攔截法**:需要 ChildView#dispatchTouchEvent 處理 & 需要配合改動父容器 (ParentView) 的 onInterceptTouchEvent 攔截
2. **外部攔截法** (較常使用):只需要 ParentView 處理
### 內部攔截:ChildView 處理
* **在 ChildView 中使用 `requestDisallowInterceptTouchEvent` 方法,控制 `FLAG_DISALLOW_INTERCEPT` 元素,讓 ParentView 不會往下分發事件** (先介紹這裡的坑~)
* 內部攔截作法 - 2 個重點步驟 (都必須)
> 以下假設,ChildView 只處理垂直滑動、水平滑動給 ParentView 處理
1. ChildView 在適當時機透過 `requestDisallowInterceptTouchEvent` 方法,要求 ParentView 不要攔截事件
```java=
// 自訂 View
public class MyView extends View {
int dispatchX, dispatchY;
@Override
public boolean dispatchTouchEvent(MotionEvent event) {
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 要求 ParentView 不攔截事件
getParent().requestDisallowInterceptTouchEvent(true);
break;
case MotionEvent.ACTION_UP:
break;
case MotionEvent.ACTION_MOVE:
int deltaX = x - dispatchX;
int deltaY = y - dispatchY;
// 水平時,事件交由 ViewGroup 處理
if(Math.abs(deltaX) > Math.abs(deltaY)) {
getParent().requestDisallowInterceptTouchEvent(false);
}
break;
}
dispatchX = x;
dispatchY = y;
return super.dispatchTouchEvent(event);
}
}
```
2. 重寫 ParentView 的 `onInterceptTouchEvent` 方法,設定在 ACTION_DOWN 時不攔截事件,這樣事件才能傳入 ChildView
```java=
// 自訂 ViewGroup
public class MyViewGroup extends ViewGroup {
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
if(ev.getAction() == MotionEvent.ACTION_DOWN) {
// ACTION_DOWN 時強制 ViewGroup 不攔截事件
return false;
}
return super.onInterceptTouchEvent(ev);
}
}
```
* `requestDisallowInterceptTouchEvent` 細節說明: 為啥 requestDisallowInterceptTouchEvent 無法控制 ACTION_DOWN 事件
:::warning
* 發生原因
ViewGroup#`ACTION_DOWN` 事件,會清除 ViewGroup 的禁止攔截事件,所以就算 ChildView 有要求 ViewGroup 不攔截事件,但事件都無法傳入 ChildView 就沒有用了
:::
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
// @ 查看 resetTouchState
resetTouchState();
}
// Check for interception.
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) { // mFirstTouchTarget != null 滿足
// 控制 FLAG_DISALLOW_INTERCEPT
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
// 如果為 ture 則 ParentView 就不會往下分發
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
// 省略其他...
}
}
// 這個方法可以控制 Flag
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
```
1. 從上面可看出 ViewGroup 時 ACTION_DOWN 使用 **resetTouchState() 方法**,這裡會清理 `FLAG_DISALLOW_INTERCEPT` 這個 Flag,導致事件 ACTION_DOWN 一定會被分發
```java=
// ViewGroup.java
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
... 省略部分
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 清理後 FLAG_DISALLOW_INTERCEPT 為空,結果就是為 false
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 判斷 ViewGroup#onInterceptTouchEvent 是否擷取
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
// ++不擷取++ 注意 intercepted 變數
intercepted = false;
}
}
...
}
```
2. 解坑:**ViewGroup 中複寫 `onInterceptTouchEvent`,讓 ViewGroup 在 ACTION_DOWN 事件時返回 false(不擷取),這樣 ACTION_DOWN 就會分發,==這樣才能往下分發到 ChildView==**,而其他的動作則會攔截
:::success
原因是因為 ViewGroup#`resetTouchState` 會清除 `FLAG_DISALLOW_INTERCEPT`,導致上面的設定失效
:::
```java=
/**
* 自定義 ViewGroup 內重新 Override
*/
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
// ACTION_DOWN 事件時,就不擷取,分發到 ChildView
if(ev.getAction() == ACTION_DOWN) {
return false;
}
return true;
}
```
### 外部攔截:ParentView 處理
* 由 ParentView 處理事件攔截比較簡單,只需要覆寫 ParentView#**onInterceptTouchEvent** 決定哪個時間(條件)攔截事件
> 以下假設 水平滑動,就攔截事件給 ParentView#onTouchEvent 處理 !
```java=
// 自定義的 ViewGroup.java
public class MyExternalViewGroup extends ViewGroup {
int interceptX, interceptY;
@Override
public boolean onInterceptTouchEvent(MotionEvent event) {
boolean intercept = false;
boolean defaultIntercept = super.onInterceptTouchEvent(event);
int x = (int) event.getX();
int y = (int) event.getY();
switch(event.getAction()) {
case MotionEvent.ACTION_MOVE:
int deltaX = x - interceptX;
int deltaY = y - interceptY;
// 水平滑動,就攔截 !
if(Math.abs(deltaX) > Math.abs(deltaY)) {
intercept = true;
} else {
intercept = false;
}
break;
case MotionEvent.ACTION_UP:
case MotionEvent.ACTION_DOWN:
// 不攔截事件
intercept = false;
break;
default:
intercept = defaultIntercept;
break;
}
interceptX = x;
interceptY = y;
return intercept;
}
}
```
## Appendix & FAQ
:::info
:::
###### tags: `Android Framework`