問題
在進行正文之前,我們帶着以下幾個問題有目的的進行,然後最後再做問題的解決。
問題 1:activity、 ViewGroup和 View 都不消費 ACTION_DOWN,那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
問題 2:在 ViewGroup 中的 onTouchEvent 中消費 ACTION_DOWN 事件(onInterceptTouch 默認設置),那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
在一個列表中,同時對父 View 和子 View 設置點擊方法,優先響應哪個?爲什麼會這樣?
爲什麼子 View 不消費 ACTION_DOWN,之後的所有事件都不會向下傳遞了。
基礎認識
事件分發的對象
首先我們要清楚,事件分發的對象是什麼?其實就 MotionEvent,這個 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件類型。
事件(MotionEvent)分發是在哪些對象中進行的?
Activity -> Window -> ViewGroup -> View
重點關注的方法
dispatchTouchEvent
onInterceptTouchEvent
onTouchEvent
ACTION_DOWN 事件在 Activity、ViewGroup、View 中根據不同函數不同的返回值的走向?
關於事件的走向,我覺的以下這張圖可以很清晰的看出事件的最終走向,該圖來自Kelin
注:流程圖來之 Kelin
以下是使用文字對事件走向的描述,幫助對流程圖的理解。
結論
dispatchTouchEvent 和 onTouchEvent 中返回 true,ACTION_DOWN 事件就在此終止,不會再往上傳也不會往下傳了。
dispatchTouchEvent 和 onTouchEvent 中返回 false,ACTION_DOWN 事件交給父控件的 onTouchEvent 進行處理
onInterceptTouchEvent 一旦返回 true,那麼 ViewGroup 之後不會再調用 onInterceptTouchEvent
事件分發源碼分析
Activity
一旦事件產生,那麼首先被調用的是 Activity 中的 dispatchTouchEvent 方法。我們來看看這個方法的實現。
public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) { onUserInteraction(); } if (getWindow().superDispatchTouchEvent(ev)) { return true; } return onTouchEvent(ev); }
首先,如果是 ACTION_DOWN,那麼 onUserInteraction 方法就會被調用,onUserInteraction 是個空的方法,當事件產生時,那麼這個方法就會被調用,如果在 activity 運行的時候,我們想要知道用戶和設備的交互,那麼我們就可以實現這個方法。
接着 window 中的 superDispatchTouchEvent 方法被調用,事件傳遞到 window 中。Window 是個抽象類,他的唯一實現是 PhoneWindow。在 Window.superDispatchTouchEvent 中調用了 DecorView 的 superDispatchTouchEvent,而這個 DecorView 就是我們在 activity 中通過調用 setContentView 設置的佈局的頂層 View。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
最後,如果 Activity 中的所有下級事件承載對象沒有處理事件,最後 Activity 中的 onTouchEvent 就會被調用,當事件超出邊界或者事件爲 ACTION_DOWN 時,mWindow.shouldCloseOnTouch(this, event) 爲 true,onTouchEvent 默認是返回 false 的。
public boolean onTouchEvent(MotionEvent event) { if (mWindow.shouldCloseOnTouch(this, event)) { finish(); return true; } return false; }
onTouchEvent 方法 當所有的 view 都沒有消費事件的時候,activity 的 onTouchEvent() 就會被調用
我們這邊看一下 getWindow() 裏面的實現,其實也就是返回一個 Window 實例
public Window getWindow() { return mWindow; }
Window(PhoneWindow)
Window 在事件分發的過程中就類似於一箇中間的橋接一樣,是沒有做什麼操作的,只是將事件傳遞給 DecorView 中。
@Override public boolean superDispatchTouchEvent(MotionEvent event) { return mDecor.superDispatchTouchEvent(event); }
ViewGroup
要理解 ViewGroup 對於事件的處理,我們先來個簡化的代碼邏輯來幫助我們理解
void dispathTouchEvent(Event event){ boolean consume = false; if(OnInterceptTouchEvent(event)){ consume = onTouchEvent(event); }else{ consume = childView.dispatchTouchEvent(event); } return consume; }
對於一個 ViewGroup 來說,點擊事件產生之後,dispatchTouchEvent 就會被調用,如果這個 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要攔截當前的事件,接着事件就會給這個 ViewGroup 處理,即它的 onTouchEvent 會被調用;如果 ViewGroup 的 onInterceptTouchEvent 返回 false,就表示它不攔截事件,這時當前事件就會被傳遞給它的子 view,接着調用子元素的 dispatchTouchEvent 方法就會被調用,如此反覆,直到事件被最終處理。
推薦閱讀:阿里騰訊Android開發十年,到中年危機就只剩下這套移動架構體系了!
乾貨很長,文末有彩蛋
大致流程圖
我們再看看 ViewGroup.dispatchTouchEvent() 還原度比較高的源碼
// 省略部分代碼 // 如果是 ACTION_DOWN 事件,重置標誌位 mGroupFlags 爲 非 FLAG_DISALLOW_INTERCEPT,這個標誌位關係到 ViewGroup 的 onInterceptTouch 是否有效。 if (actionMasked == MotionEvent.ACTION_DOWN) { cancelAndClearTouchTargets(ev); resetTouchState(); } // 檢查是否攔截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 處理條件爲: // 1\. 事件爲 ACTION_DOWN // 2\. 有下級的 View 處理事件 // 判斷攔截是否失效?mGroupFlags = FLAG_DISALLOW_INTERCEPT 時,onInterceptTouchEvent 是不會被調用的 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 { // 非 ACTION_DOWN 事件且沒有其他的下級處理該事件的時候,不會再調用 onInterceptTouchEvent intercepted = true; } // 正常事件分發 // 如果 ViewGroup 決定攔截或者已經有 子 view 處理事件,那麼就開始正常的事件分發流程 if (intercepted || mFirstTouchTarget != null) { ev.setTargetAccessibilityFocus(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 childrenCount = mChildrenCount; if (newTouchTarget == null && childrenCount != 0) { final View[] children = mChildren; for (int i = childrenCount - 1; i >= 0; i--) { final int childIndex = getAndVerifyPreorderedIndex( childrenCount, i, customOrder); final View child = getAndVerifyPreorderedView( preorderedList, children, childIndex); // 找到接收該事件的子 View 上,如果找到,則直接跳出循環 newTouchTarget = getTouchTarget(child); if (newTouchTarget != null) { newTouchTarget.pointerIdBits |= idBitsToAssign; break; } // 判斷事件是否落在子 view 上,如果是,則跳出循環 resetCancelNextUpFlag(child); if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // Child wants to receive touch within its bounds. mLastTouchDownTime = ev.getDownTime(); if (preorderedList != null) { // childIndex points into presorted list, find original index for (int j = 0; j < childrenCount; j++) { if (children[childIndex] == mChildren[j]) { mLastTouchDownIndex = j; break; } } } else { mLastTouchDownIndex = childIndex; } mLastTouchDownX = ev.getX(); mLastTouchDownY = ev.getY(); 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(); } } }
從以上的代碼中可以看出,當事件傳遞到 ViewGroup 中的 dispatchTouchEvent 的時候,這次經歷了以下的幾個重要步驟:
step 1: 重置標誌位 mGroupFlags,將 mGroupFlags 標誌位設置爲非 FLAG_DISALLOW_INTERCEPT,如果 mGroupFlags = FLAG_DISALLOW_INTERCEPT,那麼 ViewGroup 將不再調用 onInterceptTouch(),默認 ViewGroup 不攔截任何事件。
step 2: 通過判斷事件是否爲 ACTION_DOWN 或 mFirstTouchTarget != null 來決定是否向下分發 ACTION_DOWN 之外的事件;
若是子 View 消費 ACTION_DOWN,那麼 mFirstTouchTarget 會被賦值,mFirstTouchTarget != null 不成立
若是子 View 不消費 ACTION_DOWN,那麼 mFirstTouchTarget 則爲 null,ViewGroup 默認攔截 ACTION_DOWN 之後的所有事件,不向下傳遞。
step 3:通過 mGroupFlags 標誌位判斷攔截是否有效,若是 mGroupFlags = FLAG_DISALLOW_INTERCEPT,則 ViewGroup 默認不攔截任何事件。
step 4: 循環所有的子 view
(1)判斷是否有子 view 已經處理改事件了,如果有則跳出循環,直接向下級子 view 分發事件。
(2)判斷點擊是否落在某個子 view;
step 5: 如果子 view 消費了事件,那麼將 mFirstTouchTarget 進行賦值,mFirstTouchTarget(鏈表)。
View
View 對於事件的處理要稍微簡單一點,注意這裏的 View 並不包含 ViewGroup。我們先看看 dispatchTouchEvent 方法。
public boolean dispatchTouchEvent(MotionEvent event) { ... boolean result = false; if (onFilterTouchEventForSecurity(event)) { if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) { result = true; } //noinspection SimplifiableIfStatement ListenerInfo li = mListenerInfo; 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; }
View 對於點擊事件的處理就比較簡單了,因爲 View 是個單獨的元素,它沒有子元素,因此無法向下傳遞事件,所以它只能自己處理。
從上面的源碼中,我們可以看出 View 對點擊事件的處理過程,首先會判斷有沒有設置 onTouchListener,如果 onTouchListener 中的 onTouch 返回了 true,那麼 onTouchEvent 就不會再被調用,可見 onTouchListener 的優先級要高於 onTouchEvent,這樣的處理是方便點擊事件在外界進行處理。
View 中的 onTouchEvent 的實現
public boolean onTouchEvent(MotionEvent event) { // 不可用狀態下,View 依然會消耗點擊事件 if ((viewFlags & ENABLED_MASK) == DISABLED) { if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) { setPressed(false); } mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN; return clickable; } // 如果設置了代理,那麼就設置代理的方法 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) { if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) { // This is a tap, so remove the longpress check removeLongPressCallback(); if (!focusTaken) { if (mPerformClick == null) { mPerformClick = new PerformClick(); } if (!post(mPerformClick)) { performClick(); } } } .... } break; } return true; } return false; }
從上面的代碼中可以看出,只要 View 的 CLICKABLE 和 LONG_CLICK 有一個爲 true,那麼它就會消耗這個事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 狀態。
然後當 ACTION_UP 事件發生的時候,會觸發 performClick 方法,如果 View 設置了 onClickListener,那麼 performClick 方法內部就會調用它的 onClick 方法。如下所示:
public boolean performClick() { 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; }
到這裏,點擊事件的源碼分析就結束了。
問題解答
問題 1:activity、 ViewGroup和 View 都不消費 ACTION_DOWN,那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
首先,如果大家都不消費 ACTION_DOWN,那麼 ACTION_DOWN 的事件傳遞流程是這樣的:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent()-> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent() -> Activity.onTouchEvent();
接着,由於大家都不消費 ACTION_DOWN,對於 ACTION_MOVE 和 ACTION_UP 的事件傳遞是這樣的
-> Activity.dispatchTouchEvent()-> Activity.onTouchEvent();-> 消費
完整的事件分發走向
問題 2:在 ViewGroup 中的 onTouchEvent 中消費 ACTION_DOWN 事件(onInterceptTouch 默認設置),那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
首先,我們先分析一下 ACTION_DOWN 的事件走向,由於 ViewGroup 中的 onInterceptTouch 是默認設置的,那麼 ACTION_DOWN 的事件最終在 ViewGroup 中的 onTouchEvent 方法中停止了,事件走向是這樣的:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onInterceptTouchEvent()-> view1.dispatchTouchEvent() -> view1.onTouchEvent() -> ViewGroup1.onTouchEvent()
接着 ACTION_MOVE 和 ACTION_UP 的事件分發流程,之後 onInterceptTouch 和 View 中的方法都不會被調用了,事件分發如下:
-> Activity.dispatchTouchEvent() -> ViewGroup1.dispatchTouchEvent() -> ViewGroup1.onTouchEvent()
完整的事件分發走向
在一個列表中,同時對父 View 和子 View 設置點擊方法,優先響應哪個?爲什麼會這樣?
答案是優先響應子 view,原因很簡單,如果先響應父 view,那麼子 view 將永遠無法響應,父 view 要優先響應事件,必須先調用 onInterceptTouchEvent 對事件進行攔截,那麼事件不會再往下傳遞,直接交給父 view 的 onTouchEvent 處理。
爲什麼子 View 不消費 ACTION_DOWN,之後的所有事件都不會向下傳遞了。
答案是:mFirstTouchTarget。當子 view 對事件進行處理的時,那麼 mFirstTouchTarget 就會被賦值,若是子 view 不對事件進行處理,那麼 mFirstTouchTarget 就爲 null,之後 VIewGroup 就會默認攔截所有的事件。我們可以從 dispatchTouchEvent 中找到如下代碼,可以看出來,若是子 View 不處理 ACTION_DOWN,那麼之後的事件也不會給到它了。
// 檢查是否攔截 final boolean intercepted; if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) { // 省略和問題無關代碼 } else { // 默認攔截 intercepted = true; }
BAT主流Android高級架構技術大綱+學習路線+資料分享
架構技術詳解,學習路線與資料分享都在博客這篇文章裏《年薪50w的BATAndroid架構師知識體系詳解》
(包括自定義控件、NDK、架構設計、混合式開發工程師(React native,Weex)、性能優化、完整商業項目開發等)
阿里P8級Android架構師技術腦圖
全套體系化高級架構視頻;七大主流技術模塊,視頻+源碼+筆記