--- 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) > ![](https://i.imgur.com/FbmUH6H.png) ### 事件分發概念 * 由於 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/) 的混合設計模式,它擁有加強或是弱化抽象,還有遞歸抽象… 等等程式設計特性 ::: > ![](https://i.imgur.com/XU0hiYG.png) * 若該 View 已經攔截點擊事件,則會觸發 onTouch 1. `onTouch` 若沒消費該事件,才會傳遞給 `onTouchEvent` 方法 2. `onTouchEvent` 內才會有 onClick 事件 > ![](https://i.imgur.com/IiFi8Ta.png) ### Activity 接收點擊事件 * Activity (AppCompatActivity)、Windows (PhoneWindow) 之間的關係可以參考另一篇 [**Activity 布局**](https://hackmd.io/-2Uta0yBSbyhNayaev5eyg#AppCompatActivity),下圖表示了 `AppCompatActivity` 如何與 `PhoneWindow` 產生關係 > ![](https://i.imgur.com/i89DAQs.png) * **點擊事件從 ==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 開始** ::: > ![](https://i.imgur.com/vBtHm8o.png) * **Activity & View 關係圖** > ![](https://i.imgur.com/59KfaTv.png) ## 事件處理 透過上面分析,我們就知道點擊 **事件是如何傳遞至 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; } ``` > ![](https://i.imgur.com/UOLS8bz.png) 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/) > ![](https://i.imgur.com/z60XP2V.png) ::: :::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; } ``` > ![](https://i.imgur.com/XAHPQnv.png) :::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; } ``` > ![](https://i.imgur.com/sswPfLY.png) ### 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 處理 > ![](https://i.imgur.com/NzZTMfj.png) * 同樣先來觀察 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`