Android 事件分發機制的理解

[toc]bash

問題

在進行正文以前,咱們帶着如下幾個問題有目的的進行,而後最後再作問題的解決。app

  • 問題 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 等事件類型。ide

事件(MotionEvent)分發是在哪些對象中進行的?

Activity -> Window -> ViewGroup -> View函數

重點關注的方法

  • dispatchTouchEvent
  • onInterceptTouchEvent
  • onTouchEvent

ACTION_DOWN 事件在 Activity、ViewGroup、View 中根據不一樣函數不一樣的返回值的走向?

關於事件的走向,我覺的如下這張圖能夠很清晰的看出事件的最終走向,該圖來自Kelin 源碼分析

image
注:流程圖來之 Kelin

如下是使用文字對事件走向的描述,幫助對流程圖的理解。佈局

方法 Activity ViewGroup View
dispatchTouchEvent (1) return false 或者是 return true,表示 Activity 已經消費
(2) return 父類的 super.dispatchTouchEvent() 表示向下傳遞,最後結果由下一級決定
(1) return false,表示 ViewGroup 不消費事件,返回給上一級的 ViewGroup 或者 Activity 的 onTouchEvent 進行處理;
(2) return true,表示當前 ViewGroup 已經消費了事件,事件在此終止
(3) return super.dispatchTouchEvent(),表示事件繼續,根據 onInterceptTouchEvent 判斷是否本身處理事件(是否調用 onTouchEvent)
(1) return false,表示 View 不消費事件,返回給上一級的 ViewGroup onTouchEvent 進行處理;
(2) return true,表示當前 View 已經消費了事件,事件在此終止
(3) return super.dispatchTouchEvent(),表示事件繼續,並調用本身的方法 onTouchEvent
onInterceptTouchEvent 沒有此方法 (1) return true,表示 ViewGroup 攔截該事件;以後 onInterceptTouchEvent 不會再被調用
(2) return false 或者 return super.onInterceptTouchEvent 表示不攔截事件,將事件交給子 view 處理。
沒有此方法
onTouchEvent (1) return false,表示全部的 ViewGroup 和 View 都不消費事件,Activity 也不消費;
(2) return true,表示消費該事件,事件在此終止
(1) return false 或者是 return super.onTouchEvent 表示不消費事件,返回給上一級的 onTouchEvent 處理
(2) return true 表示當前 ViewGroup 本身消費了,事件在此終止,不會往上傳也不會往下傳了。
(1) return false 或者是 return super.onTouchEvent 表示不消費事件,返回給上一級的 ViewGroup 進行護理;
(2) return true 表示當前 view 進行處理事件,事件在此終止

結論

  1. dispatchTouchEvent 和 onTouchEvent 中返回 true,ACTION_DOWN 事件就在此終止,不會再往上傳也不會往下傳了。
  2. dispatchTouchEvent 和 onTouchEvent 中返回 false,ACTION_DOWN 事件交給父控件的 onTouchEvent 進行處理
  3. onInterceptTouchEvent 一旦返回 true,那麼 ViewGroup 以後不會再調用 onInterceptTouchEvent

事件分發源碼分析

Activity

一旦事件產生,那麼首先被調用的是 Activity 中的 dispatchTouchEvent 方法。咱們來看看這個方法的實現。post

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 運行的時候,咱們想要知道用戶和設備的交互,那麼咱們就能夠實現這個方法。ui

接着 window 中的 superDispatchTouchEvent 方法被調用,事件傳遞到 window 中。Window 是個抽象類,他的惟一實現是 PhoneWindow。在 Window.superDispatchTouchEvent 中調用了 DecorView 的 superDispatchTouchEvent,而這個 DecorView 就是咱們在 activity 中經過調用 setContentView 設置的佈局的頂層 View。this

@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }

複製代碼

最後,若是 Activity 中的全部下級事件承載對象沒有處理事件,最後 Activity 中的 onTouchEvent 就會被調用,當事件超出邊界或者事件爲 ACTION_DOWN 時,mWindow.shouldCloseOnTouch(this, event) 爲 true,onTouchEvent 默認是返回 false 的。spa

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 方法就會被調用,如此反覆,直到事件被最終處理。

  • 大體流程圖

F4E2879A-152E-4b64-AF93-E01D395B7E36.png

  • 咱們再看看 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();
-> 消費
複製代碼
  • 完整的事件分發走向

touch1.png

問題 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() 
複製代碼
  • 完整的事件分發走向

touch2.png

在一個列表中,同時對父 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;
            }

複製代碼

做者介紹

  • 陳堅潤:廣州蘆葦科技 APP 團隊 Android 開發工程師

內推信息

  • 咱們正在招募小夥伴,有興趣的小夥伴能夠把簡歷發到 app@talkmoney.cn,備註:來自掘金社區
  • 詳情能夠戳這裏--> 廣州蘆葦信息科技
相關文章
相關標籤/搜索