close

本文原作者: BennuC,原文發布於:BennuCTech

前言




Android 的事件分發機制也是老生常談了,本文從細節入手解讀一下整個機制中的幾個重要部分。

Android 中 touch 事件一定是從 ACTION_DOWN 開始,所以 ACTION_DOWN 的處理至關重要,我們先來看看 ACTION_DOWN 這個事件相關的細節。


dispatchTouchEvent



說到 Android 事件分發,一定繞不開 dispatchTouchEvent 函數,View 和 ViewGroup 的該函數有很大的不同。

我們來看看 ViewGroup 的 dispatchTouchEvent 函數,它的部分源碼如下:
@Overridepublic boolean dispatchTouchEvent(MotionEvent ev) { ... if (onFilterTouchEventForSecurity(ev)) { ... boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus() ? findChildWithAccessibilityFocus() : null; if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { final int actionIndex = ev.getActionIndex(); // always 0 for down final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex) : TouchTarget.ALL_POINTER_IDS; removePointersFromTouchTargets(idBitsToAssign); final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ... for (int i = childrenCount - 1; i >= 0; i--) { ... if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } ... if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ... newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } // The accessibility focus didn't handle the event, so clear // the flag and do a normal dispatch to all children. ev.setTargetAccessibilityFocus(false); } if (preorderedList != null) preorderedList.clear(); } ... } } // Dispatch to touch targets. if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. 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; } ... } predecessor = target; target = next; } } ... } if (!handled && mInputEventConsistencyVerifier != null) { mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1); } return handled;}
可以看到整個分發有幾個關鍵因素:intercepted、canceled、mFirstTouchTarget、alreadyDispatchedToNewTouchTarget。

intercepted、canceled 比較好理解,重點來說說後面兩個因素是如何影響整個分發的。

ACTION_DOWN



一個完整的事件應該包含 ACTION_DOWN、ACTION_MOVE、ACTION_UP。其中 ACTION_DOWN 是開始也是關鍵。

從上面 dispatchTouchEvent 源碼中可以看到首先單獨對 ACTION_DOWN 事件進行了處理,對所有 child 進行遍歷,是從後向前遍歷的,所以在處理上面的也就是最後添加的 view 會先得到事件

for (int i = childrenCount - 1; i >= 0; i--) {

對於每個 child,會先判斷事件是不是發生在它的區域內,不是則不處理:

if (!child.canReceivePointerEvents() || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue;}

如果在區域內,則繼續執行,下面 dispatchTransformedTouchEvent 這個函數就是下發事件的,我們來看下部分源碼:

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { ... if (newPointerIdBits == oldPointerIdBits) { if (child == null || child.hasIdentityMatrix()) { if (child == null) { handled = super.dispatchTouchEvent(event); } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop; event.offsetLocation(offsetX, offsetY); handled = child.dispatchTouchEvent(event); event.offsetLocation(-offsetX, -offsetY); } return handled; } transformedEvent = MotionEvent.obtain(event); } else { transformedEvent = event.split(newPointerIdBits); } // Perform any necessary transformations and dispatch. 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;}
有不少邏輯在裡面,但是仔細觀察可以發現,不論哪個條件,執行的代碼都比較類似,如下:
if (child == null) { handled = super.dispatchTouchEvent(event);} else { ... handled = child.dispatchTouchEvent(event); ...}
當 child 不為 null 的時候,執行 child 的 dispatchTouchEvent;為 null 時執行父類的 dispatchTouchEvent,即 View 的 dispatchTouchEvent 函數,這個函數裡會執行 onTouchEvent 等。所以在 ViewGroup 是沒有 onTouchEvent 等函數的代碼。

由於這時 child 不為 null,所以執行了 child 的 dispatchTouchEvent 函數.

回到之前的 ACTION_DOWN 流程中,根據 dispatchTransformedTouchEvent 返回值進行不同的處理:

返回 ture

如果返回 true,即有一個 child 消費了 ACTION_DOWN 事件,可以看到後續執行了 addTouchTarget 函數,同時將 alreadyDispatchedToNewTouchTarget 置為 true。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ... newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break;}

addTouchTarget 函數源碼如下:

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; //初始mFirstTouchTarget為null,所以這裡next是null mFirstTouchTarget = target; return target;}

關鍵的一點是對 mFirstTouchTarget 進行了賦值。所以說 true 的處理是為 mFirstTouchTarget 賦值,將 alreadyDispatchedToNewTouchTarget 置為 true 最後的 break 則跳出循環,不再遍歷其他 child。

返回 false

如果返回 false,即沒有任何一個 child 消費 ACTION_DOWN 事件,直接跳過 if 代碼,這樣 mFirstTouchTarget 為 null。


mFirstTouchTarget



那麼 mFirstTouchTarget、alreadyDispatchedToNewTouchTarget 這兩個屬性在分發過程中的作用是什麼?我們分別來說:
1、mFirstTouchTarget為null

當 mFirstTouchTarget 為 null,進入 if 語句執行dispatchTransformedTouchEvent(ev, canceled, null,TouchTarget.ALL_POINTER_IDS)

if (mFirstTouchTarget == null) { // No touch targets so treat this as an ordinary view. handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS);}

由於 child 是 null,在 dispatchTransformedTouchEvent 代碼中可以看到不再給任何 child 分發,而是調用了 super.dispatchTouchEvent,即 ViewGroup 自己處理

這樣 ACTION_DOWN 事件分發完了。其他事件分發時由於不再走 ACTION_DOWN 的處理過程,所以 mFirstTouchTarget 會一直為 null,所以其他事件也不再向下分發了,直接 ViewGroup 自己處理

2、mFirstTouchTarget 不為 null

當 mFirstTouchTarget 不為 null,進入 else 語句中,會執行一個 while 循環

else { // Dispatch to touch targets, excluding the new touch target if we already // dispatched to it. Cancel touch targets if necessary. 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; } ... } predecessor = target; target = next; }}
這時由於 alreadyDispatchedToNewTouchTarget 為 true,所以直接給 handled 賦值 true 並不做任何處理。因為之前代碼中 child 對 ACTION_DOWN 事件已經響應,所以這裡的 alreadyDispatchedToNewTouchTarget 是為了防止重複分發 ACTION_DOWN 事件。

這樣 ACTION_DOWN 事件分發完成後,分發其他事件時,alreadyDispatchedToNewTouchTarget 被重新賦值 false,由於不再走 ACTION_DOWN 的處理過程,所以 alreadyDispatchedToNewTouchTarget 就一直是 false 了,而 mFirstTouchTarget 會一直保持不變。在這個 while 循環中則會執行 else 語句,通過執行 dispatchTransformedTouchEvent 將事件直接分發給 mFirstTouchTarget 對應的 child,即之前消費 ACTION_DOWN 事件的 child。

ACTION_DOWN 總結



這樣我們得到幾個結論:

1、ViewGroup 分發事件 down 的時候,會遍歷自己的子 view,從前面的到後面的

for (int i = childrenCount - 1; i >= 0; i--) {

然後判斷子 view 的區域是否包含事件,如果包含則進行處理。

所以同級分發時,即兩個同級的 view 疊加在一起時,先分發給前面的 view。

2、如果所有的 child 都不消費 ACTION_DOWN 事件,那麼實際上 child 並不是收不到任何事件,而是 ACTION_DOWN 會分發給所有有效範圍內的 child,但是其他事件就不再分發了。

3、如果有一個 child 消費了 ACTION_DOWN 事件,那麼後續的事件會直接分發給這個 child,不再經過其他 child。但是注意,在分發 ACTION_DOWN 事件時,排在這 child 前面的 child 還是會分發到 ACTION_DOWN 事件,但是也僅僅是 ACTION_DOWN 事件。

所以整個 Touch 事件分發過程中,ACTION_DOWN 是至關重要的,我們通常考慮的返回值或繼續分發的問題,實際上都是討論 ACTION_DOWN 這個事件的,基本上 ACTION_DOWN 事件分發確定了,後續事件的分發就基本確定下來了。但是注意在後續的事件中,依然需要判斷 InterceptTouchEvent。


攔截機制



我們知道在事件分發過程中是存在一個攔截機制的

onInterceptTouchEvent

當它返回 true 則不向下分發事件,否則向下分發。

但是在這個過程中,還有一個參與者:requestDisallowInterceptTouchEvent,這個函數直接影響事件的攔截。我們今天就來說一說這個這個函數是如何影響事件分發的。


源碼分析



我們先看看這個函數的源碼

@Overridepublic 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); }}
可以看到它改變了一個開關 FLAG_DISALLOW_INTERCEPT,同時調用其 parent 的函數。

那麼這個開關有什麼用?

在 ViewGroup 的 dispatchTouchEvent 函數開頭有這樣一段代碼:
final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action); // restore action in case it was changed } else { intercepted = false; }} else { // There are no touch targets and this action is not an initial down // so this view group continues to intercept touches. intercepted = true;}
先來看判斷邏輯,當是 down 事件或者 mFirstTouchTarget 不為空,則進入一個代碼段;否則攔截設置為 true。

我們知道 down 事件分發過程中,如果有子 view 消費事件,則賦值給 mFirstTouchTarget,後續事件會直接分發給 mFirstTouchTarget

這裡也可以看出,如果有子 View 消費了 down 事件,即 mFirstTouchTarget 不為空,所以後續事件還會檢查攔截。

所以上面就可以理解了,如果 down 事件中沒有子 view 消費事件,那麼後續事件的攔截都為 true。所以後續事件不會再遍歷子 View。

下面再看 if 代碼段

一開始就使用了 FLAG_DISALLOW_INTERCEPT 開關,即 disallowIntercept

當 disallowIntercept 為 true,則不攔截;否則判斷 onInterceptTouchEvent

所以簡單來說 requestDisallowInterceptTouchEvent 設置為 true 可以跳過 onInterceptTouchEvent,不攔截事件。

而且因為 requestDisallowInterceptTouchEvent 又調用了 parent 的函數,所以所有層次的父 view 都不再攔截。

所以 requestDisallowInterceptTouchEvent 的功能是讓這個 view 及上面的所有父 view 都放開攔截,即使 onInterceptTouchEvent 為 true。

所以我們一般如下使用
view.getParent().requestDisallowInterceptTouchEvent(true);

這樣 view 的所有層次的父 view 都不會攔截事件了。


擴展思考



下面讓我們再深入想想。上面這種的情況是在 touch 事件發生前設置 onInterceptTouchEvent,也是我們一般的用法。但是如果事件發生過程中調用這個函數呢?

比如在 view 的 onTouch 的某個事件中使用

getParent().requestDisallowInterceptTouchEvent(true)

當事件開始分發時,down 事件進入父 view 的 dispatchTouchEvent 時,這是子 view 還未得到事件,所以沒有設置 requestDisallowInterceptTouchEvent。

這時如果父 view 的 onInterceptTouchEvent 返回 true,即攔截的話,事件則不會分發給子 view 了,所以 requestDisallowInterceptTouchEvent 永遠不會執行,子 view 則無法得到事件。

但是如果父 view 的 onInterceptTouchEvent 返回 false,即不攔截的話,事件就可以分發到子 view,requestDisallowInterceptTouchEvent 執行,之後的事件都會跳過父 view 的 onInterceptTouchEvent 的判斷

例如父 view 的 onInterceptTouchEvent 代碼如下

public boolean onInterceptTouchEvent(MotionEvent ev) { switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: return false; case MotionEvent.ACTION_MOVE: return true; case MotionEvent.ACTION_UP: return true; default: break; } return false; }

down 事件不進行攔截,但是攔截了 move 和 up 事件。

如果子 view 的 onTouch 的 down 事件中使用

getParent().requestDisallowInterceptTouchEvent(true)

這樣 down 事件分發到了子 view,執行了 requestDisallowInterceptTouchEvent,同時返回了 true。隨後 move 或 up 事件分發到父 view 時,因為被設置了 FLAG_DISALLOW_INTERCEPT 標籤,所以就會跳過 onInterceptTouchEvent。

所以 onInterceptTouchEvent 中 move 和 up 的返回值設置就無效了,因為根本就不再執行這個函數了。


攔截總結



通過上面的分析可以知道 requestDisallowInterceptTouchEvent 會讓父 view 放開攔截,並且是向上層層生效的。同時我們也可以通過一些邏輯控制,使 requestDisallowInterceptTouchEvent 只作用在部分情況下。


長按右側二維碼

查看更多開發者精彩分享



"開發者說·DTalk" 面向中國開發者們徵集 Google 移動應用 (apps & games)相關的產品/技術內容。歡迎大家前來分享您對移動應用的行業洞察或見解、移動開發過程中的心得或新發現、以及應用出海的實戰經驗總結和相關產品的使用反饋等。我們由衷地希望可以給這些出眾的中國開發者們提供更好展現自己、充分發揮自己特長的平台。我們將通過大家的技術內容着重選出優秀案例進行谷歌開發技術專家 (GDE)的推薦。



點擊屏末|閱讀原文|即刻報名參與"開發者說·DTalk"

arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()