Android:事件分發機制原始碼解讀與滑動衝突解決方案
事件分發原始碼分析
1. Activity 事件分發
首先從 Activity 的 dispatchTouchEvent 方法入手
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
可以看出,Activity 其實是呼叫了 Window 的 superDispatchTouchEvent 方法,而 Window 的實現類是 PhoneWindow,因此我們直接檢視 PhoneWindow 的 superDispatchTouchEvent 方法
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
發現是直接呼叫的 DecorView 的 superDispatchTouchEvent 方法,再進一步檢視
public boolean superDispatchTouchEvent(MotionEvent event) { return super.dispatchTouchEvent(event); }
原來這兒就呼叫了 ViewGroup 的 dispatchTouchEvent 方法,也就是說介面上的事件直接傳遞給了根佈局的 dispatchTouchEvent 方法
2. ViewGroup 事件分發
在講 ViewGroup 的 dispatchTouchEvent 方法之前,我們先看看 ViewGroup 的 dispatchTransformedTouchEvent 方法,dispatchTouchEvent 內部多次呼叫了 dispatchTransformedTouchEvent 方法。因為程式碼量較多,這裡只提取我們關心的部分
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) { final boolean handled; // 如果是取消操作,則直接分發取消事件 final int oldAction = event.getAction(); if (cancel || oldAction == MotionEvent.ACTION_CANCEL) { event.setAction(MotionEvent.ACTION_CANCEL); // 如果傳入的 child 不為空,則呼叫 child 的 dispatchTouchEvent 方法,否則呼叫自身的 dispatchTouchEvent 方法 if (child == null) { handled = super.dispatchTouchEvent(event); } else { handled = child.dispatchTouchEvent(event); } event.setAction(oldAction); return handled; } ...... // 如果傳入的 child 不為空,則呼叫 child 的 dispatchTouchEvent 方法,否則呼叫自身的 dispatchTouchEvent 方法 if (child == null) { handled = super.dispatchTouchEvent(transformedEvent); } else { ...... handled = child.dispatchTouchEvent(transformedEvent); } ...... return handled; }
可以看出 dispatchTransformedTouchEvent 方法主要做了兩件事
- 如果傳入的事件是 ACTION_CANCEL,或者 cancel 引數為 true,則直接分發 ACTION_CANCEL 事件
- 分發過程中,如果 child 為空,則呼叫當前 View 的 super.dispatchTouchEvent 方法,這是因為 ViewGroup 的 dispatchTouchEvent 方法會被重寫,而此時呼叫 super 的方法也就是呼叫 View 的 dispatchTouchEvent 方法;如果 child 不為空,則呼叫這個子 View 的 dispatchTouchEvent 方法。
然後我們再來看看 dispatchTouchEvent 方法,同樣程式碼量特別多,我們只抽取我們關心的,即使這樣程式碼量依然很多。我們先列出來,不用仔細看,後面會分塊拆分講解
@Override public boolean dispatchTouchEvent(MotionEvent ev) { ...... boolean handled = false; if (onFilterTouchEventForSecurity(ev)) { final int action = ev.getAction(); final int actionMasked = action & MotionEvent.ACTION_MASK; // 1. DOWN 事件進行初始化,清空 TouchTargets 和 TouchState if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 2. 檢查是否攔截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 是否強制不允許攔截,子 View 可以設定 parent 強制不允許攔截,預設為 false 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 { intercepted = true; } ...... // 3. 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { ...... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ...... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ...... for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); ...... // 找到 Visible 並且處於點選範圍的子 View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } ...... // 相當於呼叫子 View 的 dispatchTouchEvent 方法 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ...... // 賦值 TouchTarget,重新整理標誌位 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ...... } ...... } ...... } } // 4. 是自己處理事件還是交由子 View 處理事件 if (mFirstTouchTarget == null) { // 沒有子 View 消耗事件,則自己消耗,相當於呼叫 super.dispatchTouchEvent 方法 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // 如果是 DOWN 事件,則上面已經呼叫了子 View 的 dispatchTouchEvent 方法,則什麼都不用做 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { // 根據 intercepted 決定是否將事件強制改為 CANCEL 事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 相當於呼叫子 View 的 dispatchTouchEvent 方法。如果 intercepted=true,此時會強制將 action 改為 CANCEL;如果 intercepted=false,則 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } // 如果 intercepted=true,則將 mFirstTouchTarget 置為 null if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } } ...... } ...... return handled; }
dispatchTouchEvent 方法主要由4個模組組成的
- DOWN 事件進行初始化,清空 TouchTargets 和 TouchState
- 檢查是否攔截
- 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget
- 是自己處理事件還是交由子 View 處理事件
A. 第一步:DOWN 事件時進行初始化
// 1. DOWN 事件進行初始化,清空 TouchTargets 和 TouchState if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); }
我們知道,一個事件序列是由一個 ACTION_DOWN,零個或多個 ACTION_MOVE 和一個 ACTION_UP 組成的。ACTION_DOWN 是一個事件序列的開始,ACTION_UP 是一個事件序列的結束。在 ACTION_DOWN 時,需要對一些狀態進行初始化和重置。上面的 cancelAndClearTouchTargets 和 resetTouchState 方法,都是初始化一些狀態,最重要的我們關心的就是內部呼叫了這個初始化程式碼
private void clearTouchTargets() { TouchTarget target = mFirstTouchTarget; if (target != null) { do { TouchTarget next = target.next; target.recycle(); target = next; } while (target != null); mFirstTouchTarget = null; } }
可以看出來這裡是用迴圈的方式把單鏈表 mFirstTouchTarget 給清空了,至於 mFirstTouchTarget 是什麼這裡先不講,只需要知道它儲存了一個指定消耗事件的子 View 便可,後續的所有事件會直接交給這個 View 消耗 。
B. 第二步:檢查是否攔截
// 2. 檢查是否攔截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 是否強制不允許攔截,子 View 可以設定 parent 強制不允許攔截,預設為 false 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 { intercepted = true; }
這裡我們可以看出,如果是 ACTION_DOWN 事件,或者 mFirstTouchTarget 不為空的時候,就會進入是否攔截的邏輯判斷。這裡有一個 disallowIntercept 的判斷,這個標記是子 View 設定 ViewGroup 是否允許攔截的情況,這裡我們暫不用理解這種情況,預設 disallowIntercept 為 false,也就是允許攔截。此時會判斷 onInterceptTouchEvent 方法,而 onInterceptTouchEvent 方法預設返回 false。
而 mFirstTouchTarget 是怎麼來的呢?在後面會有詳細的講解,這裡先簡單的說明一下:
- 如果 intercepted 為 false,並且在後面有一個子 View 的 dispatchTouchEvent 方法在 ACTION_DOWN 時返回了 true,那麼就會對 mFirstTouchTarget 進行賦值;
- 如果 intercepted 為 true,那麼在後面就會對 mFirstTouchTarget 置為 null
我們來走一遍所有可能的路徑:
- 事件序列開始,也就是說,在 ACTION_DOWN 時,會呼叫 onInterceptTouchEvent 判斷是否攔截;
- 如果此時 onInterceptTouchEvent 返回 true,intercepted 賦值為 true,則後面 mFirstTouchTarget 不會再被賦值,mFirstTouchTarget 會一直為 null,後續的 ACTION_MOVE 和 ACTION_UP 事件 onInterceptTouchEvent 方法不會再被呼叫,intercepted 會一直為 true
- 如果此時 onInterceptTouchEvent 返回 false,intercepted 賦值為 false,後面如果有子 View 的 dispatchTouchEvent 方法返回了 true,mFirstTouchTarget 會被賦值,後續的 ACTION_MOVE 和 ACTION_UP 事件每次都會呼叫 onInterceptTouchEvent 方法
- 在後續的 ACTION_MOVE 和 ACTION_UP 事件中,如果 onInterceptTouchEvent 返回了 true,那麼在後面會清空 mFirstTouchTarget 的值,那麼在下一個事件直到最後一個事件,onInterceptTouchEvent 方法又不會被呼叫了
總結來說,onInterceptTouchEvent 一旦某一次返回了 true,那麼後面的事件都不會再呼叫 onInterceptTouchEvent 進行是否攔截的判斷,intercepted 的值會一直為 true
C. 第三步:先處理 DOWN 事件
// 3. 如果沒有被攔截, 先處理 DOWN 事件,主要是賦值 TouchTarget TouchTarget newTouchTarget = null; boolean alreadyDispatchedToNewTouchTarget = false; if (!canceled && !intercepted) { ...... if (actionMasked == MotionEvent.ACTION_DOWN || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN) || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { ...... final int childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { ...... for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); ...... // 找到 Visible 並且處於點選範圍的子 View if (!canViewReceivePointerEvents(child) || !isTransformedTouchPointInView(x, y, child, null)) { ev.setTargetAccessibilityFocus(false); continue; } ...... // 相當於呼叫子 View 的 dispatchTouchEvent 方法 if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { ...... // 賦值 TouchTarget,重新整理標誌位 newTouchTarget = addTouchTarget(child, idBitsToAssign); alreadyDispatchedToNewTouchTarget = true; break; } ...... } ...... } ...... } }
這段程式碼的前提條件就是 intercepted 為 false,並且是 ACTION_DOWN 事件,此時會遍歷每一個滿足條件(處於Visible狀態並且處於點選範圍內)的子 View,呼叫了 dispatchTransformedTouchEvent 方法,dispatchTransformedTouchEvent 方法最開始我們就講解過,此時的 child 引數不為空,所以就是呼叫的 child 的 dispatchTouchEvent 方法。如果 dispatchTransformedTouchEvent 返回為 true,則呼叫 addTouchTarget 方法對 mFirstTouchTarget 進行賦值,並且將變數 alreadyDispatchedToNewTouchTarget 置為 true,然後緊跟了一個 break 跳出迴圈,為什麼要跳出迴圈呢?這是因為一個事件只能被一個 View 消耗(dispatchTouchEvent 返回 true 代表消耗)。
我們看看 addTouchTarget 方法
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) { final TouchTarget target = TouchTarget.obtain(child, pointerIdBits); target.next = mFirstTouchTarget; mFirstTouchTarget = target; return target; }
就是一個單鏈表操作,新增一個 TouchTarget 並插入到表頭,然後將表頭賦值給 mFirstTouchTarget。
總結來說,ACTION_DOWN 時,如果 intercepted 為 true,則不會有任何子 View 呼叫 dispatchTouchEvent 方法,並且 mFirstTouchTarget 不會被賦值,會一直為 null;如果 intercepted 為 false,那麼在滿足條件的某個子 View 的 dispatchTouchEvent 方法返回 true 時,mFirstTouchTarget 也會被賦值
D. 第四步:分發事件
// 4. 是自己處理事件還是交由子 View 處理事件 if (mFirstTouchTarget == null) { // 沒有子 View 消耗事件,則自己消耗,相當於呼叫 super.dispatchTouchEvent 方法 handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); } else { TouchTarget predecessor = null; TouchTarget target = mFirstTouchTarget; while (target != null) { final TouchTarget next = target.next; // 如果是 DOWN 事件,則上面已經呼叫了子 View 的 dispatchTouchEvent 方法,則什麼都不用做 if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { handled = true; } else { // 根據 intercepted 決定是否將事件強制改為 CANCEL 事件 final boolean cancelChild = resetCancelNextUpFlag(target.child) || intercepted; // 相當於呼叫子 View 的 dispatchTouchEvent 方法。如果 intercepted=true,此時會強制將 action 改為 CANCEL;如果 intercepted=false,則 if (dispatchTransformedTouchEvent(ev, cancelChild, target.child, target.pointerIdBits)) { handled = true; } // 如果 intercepted=true,則將 mFirstTouchTarget 置為 null if (cancelChild) { if (predecessor == null) { mFirstTouchTarget = next; } else { predecessor.next = next; } target.recycle(); target = next; continue; } } predecessor = target; target = next; } }
可以看到,只要 mFirstTouchTarget 為 null,那麼就會直接呼叫 dispatchTransformedTouchEvent 方法,child 引數為 null,那麼就會呼叫當前 ViewGroup 的 super.dispatchTouchEvent 方法。
如果 mFirstTouchTarget 不為 null,首先判斷 alreadyDispatchedToNewTouchTarget 和 newTouchTarget 變數決定是否直接返回。由上面第3步可知,如果 alreadyDispatchedToNewTouchTarget 為 true,則代表當前是 ACTION_DOWN 事件,並且找到了一個子 View 並且呼叫過了 child 的 dispatchTouchEvent 方法,因此這裡什麼都不用做。
然後再看看 cancelChild 變數,它是由 intercepted 決定的。假如 cancelChild 為 false,說明 intercepted 一定為 false,然後因為 mFirstTouchTarget 不為空,那麼呼叫 dispatchTransformedTouchEvent 時 child 不為空,則直接呼叫 child 的 dispatchTouchEvent 方法;如果 intercepted 為 true,那麼 cancelChild 一定為 true,dispatchTransformedTouchEvent 的 cancel 引數為 true,那麼會直接給 mFirstTouchTarget 對應的 child 傳遞一個 ACTION_CANCEL 事件,然後在 mFirstTouchTarget 對應的單鏈表中,刪除 mFirstTouchTarget 當前對應的 TouchTarget
總結一下,如果 mFirstTouchTarget 為 null,則呼叫自身的 super.dispatchTouchEvent 方法;如果 mFirstTouchTarget 不為 null,如果上面已經處理過 ACTION_DOWN 事件,則什麼都不做;如果 intercepted 為 false,則直接呼叫 mFirstTouchTarget 對應的 child 的 dispatchTouchEvent方法;如果 intercepted 為 true,則會強制給 mFirstTouchTarget 對應的 child 分發一個 ACTION_CANCEL 事件,然後將 mFirstTouchTarget 置為 null
E. ViewGroup 的 dispatchTouchEvent 事件分發流程
我們將一個完整的事件序列的整體流程走一遍
- 當 ACTION_DOWN 事件時,會將 mFirstTouchTarget 置為 null
- 然後判斷 onInterceptTouchEvent 是否攔截,如果返回 true,則後續事件都不會再呼叫 onInterceptTouchEvent 方法來判斷是否攔截並且預設都是 true
- 因為 intercepted 為 true,並且 mFirstTouchTarget 為 null,因此會直接呼叫自身的 super.dispatchTouchEvent 方法。並且後續事件都會如此執行(若下一個事件能到達)。
- 如果第2步的 onInterceptTouchEvent 返回 false,則會嘗試找到一個滿足條件的子 View,分發此次 ACTION_DOWN 事件,並對 mFirstTouchTarget 進行賦值
- 如果第4步找不到合適的子 View,mFirstTouchTarget 依然為 null,則會呼叫自身的 super.dispatchTouchEvent 方法。並且後續事件都會如此執行(若下一個事件能到達)。
- 如果第4步找到了合適的子 View,也就意味著分發了一個 ACTION_DOWN 事件,那麼 ACTION_DOWN 就什麼都不用做了。
- 當 ACTION_MOVE 和 ACTION_UP 事件時,如果第4步找到了合適的子 View,mFirstTouchTarget 被賦值,那麼會再次判斷 onInterceptTouchEvent 是否攔截。
- 如果第7步判斷為不攔截,則直接分發此次事件給 mFirstTouchTarget 對應的 child
- 如果第7步判斷為攔截,則強制分發 ACTION_CANCEL 事件給 mFirstTouchTarget 對應的 child,然後將 mFirstTouchTarget 置為 null
3. View事件分發
public boolean dispatchTouchEvent(MotionEvent event) { ...... if (onFilterTouchEventForSecurity(event)) { ...... if (li != null && li.mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED && li.mOnTouchListener.onTouch(this, event)) { result = true; } if (!result && onTouchEvent(event)) { result = true; } } ...... return result; }
看完了 ViewGroup 的 dispatchTouchEvent,再來看 View 的 dispatchTouchEvent 就發現是不是太簡單了。這裡簡單提醒一下:在 ViewGroup 呼叫 super.dispatchTouchEvent 其實就是呼叫的 View 的 dispatchTouchEvent 方法。
首先,如果設定了 mOnTouchListener,則會優先處理 mOnTouchListener 的 onTouch 方法,如果 onTouch 方法返回 true,則此方法直接返回而不會再呼叫 onTouchEvent 方法了。也就是說 mOnTouchListener 的優先順序高於 onTouchEvent 方法。如果在 mOnTouchListener 的 onTouch 方法中返回了 true,也許會因為沒有執行 onTouchEvent 方法而導致點選等回撥不被呼叫。最後會將 mOnTouchListener 或 onTouchEvent 方法的返回值作為此方法的返回值。
然後我們看看 onTouchEvent 方法
public boolean onTouchEvent(MotionEvent event) { ...... final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE; if ((viewFlags & ENABLED_MASK) == DISABLED) { ...... return clickable; } ...... if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) { switch (action) { case MotionEvent.ACTION_UP: ...... boolean focusTaken = false; if (isFocusable() && isFocusableInTouchMode() && !isFocused()) { focusTaken = requestFocus(); } ...... if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClickInternal(); } } ...... break; ...... } return true; } return false; }
首先,只要可以點選或者長按,clickable 就為 true。不管當前控制元件是不是 DISABLED,都返回 clickable。clickable 根據不同的控制元件而值不同,比如 Button 就為 true,TextView 就為 false。設定對應的點選監聽器會預設將 clickable 設為 true。也就是說,只要當前控制元件是可點選的,那麼 onTouchEvent 預設返回 true 而消耗這一串事件序列。
在 ACTION_UP 中,會完成點選事件的判斷,具體是通過 performClickInternal 方法完成的
private boolean performClickInternal() { // Must notify autofill manager before performing the click actions to avoid scenarios where // the app has a click listener that changes the state of views the autofill service might // be interested on. notifyAutofillManagerOnClick(); return performClick(); }
也就是通過 performClick 方法來完成的
public boolean performClick() { // We still need to call this method to handle the cases where performClick() was called // externally, instead of through performClickInternal() notifyAutofillManagerOnClick(); final boolean result; final ListenerInfo li = mListenerInfo; if (li != null && li.mOnClickListener != null) { playSoundEffect(SoundEffectConstants.CLICK); li.mOnClickListener.onClick(this); result = true; } else { result = false; } sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED); notifyEnterOrExitForAutoFillIfNeeded(true); return result; }
在這裡,顯示觸發 mOnClickListener 的 onClick 方法來完成點選事件的回撥。
滑動衝突解決
根據以上原始碼分析,我們可以找到一種解決滑動衝突的套路:
@Override public boolean onInterceptTouchEvent(MotionEvent ev) { boolean intercept = false; float x = ev.getX(); float y = ev.getY(); switch (ev.getAction()) { case MotionEvent.ACTION_DOWN: intercept = false; break; case MotionEvent.ACTION_MOVE: // 橫向滑動則自己處理,豎向滑動則子View處理 if (Math.abs(x - lastX) > Math.abs(y - lastY)){ intercept = true; } else { intercept = false; } break; case MotionEvent.ACTION_UP: intercept = false; break; } lastX = x; lastY = y; return intercept; }
- ACTION_DOWN 事件一定要返回 false,否則所有的事件都會被 ViewGroup 攔截,並且不會再呼叫 onInterceptTouchEvent 方法。因此 ACTION_DOWN 事件一定會被子 View 所消耗。如果當前 ViewGroup 需要在 ACTION_DOWN 處理一些邏輯,則可以在 dispatchTouchEvent 或者 onInterceptTouchEvent 方法處理
- ACTION_MOVE 事件根據需要來決定是否攔截,一旦攔截,則會立馬給 mFirstTouchTarget 指定的 child 分發一個 ACTION_CANCEL 事件,後續的事件序列都直接交由 ViewGroup 消耗,並且不會再呼叫 onInterceptTouchEvent 方法
- ACTION_UP 事件的返回值返回 false,如果在之前的事件序列返回過 true,那麼 ACTION_UP 返回什麼都無所謂,因為根本不會被執行到;但是如果之前的事件序列沒有返回過 true,那麼 ACTION_UP 返回 true,會直接強制替換為 ACTION_CANCEL 分發給子 View,導致子 View 的點選事件無法響應。