Android事件分發機制詳解

Android事件分發機制

一.說些廢話

Android事件分發真的非常非常重要,幾乎所有的滑動衝突以及點擊衝突都需要深刻了解該機制纔可以解決問題。所以我希望大家可以仔細閱讀該篇文章並且自己手動來實驗,一定要自己打斷點看看源碼,不管幹什麼都要下功夫不是嗎?

雖然重要,但其實Android事件分發機制也很簡單,只要理解了Android事件分發的三個方法,以及傳遞的流程,你就可以輕鬆掌握Android事件分發機制。

在開始之前,再說一下事件流:

事件流指的是一次完整的觸摸事件,一次完整的觸摸事件應該包括是:

down(一次按下)-->move move move(多次滑動).....-->up(一次擡起)

所以事件流總是以down事件爲開端,以up事件爲終止。

那麼接下來就正式開始吧!

二. 重要方法

首先看這三個方法的名稱以及擁有情況:

注意:類型不同,方法名相同,源碼並不一致

方法名 Activity ViewGroup View
dispatchTouEvent true true true
onInterceptTouchEvent false true false
onTouchEvent true true true

true代表擁有,false代表沒有此方法

如果想要了解Android的事件分發機制,就必須先了解這三個方法

  • dispatchTouEvent()

    事件的分發方法,用來分發事件,在Activity、ViewGroup、View中都有該方法。

  • onInterceptTouchEvent()

    事件的攔截方法,只有ViewGroup有該方法。

  • onTouchEvent()

    事件的響應,當該事件屬於我時,會執行該方法,返回true代表消費事件,返回false代表不消費事件並將該事件向外傳遞。

三個方法的含義知道了,這很easy,沒錯,就三個方法而已。接下來讓我們深入分析:

三. 具體案例

首先,我們來想象一個簡單的佈局
佈局

這個佈局很簡單,FrameLayout1的中間放了一個FrameLayout2,FrameLayout2的中間放了一個Button。

我在各個佈局當中重寫了所有事件響應的方法,並且沒有修改任何返回值,接下來我要點擊Button了!

我點!

我點

首先接收到事件的是Activity,接下來事件傳遞到F1,F2,最後傳遞到Button的onTouchEvent被消費,很好理解,因爲事件由外向裏傳遞

接下來我們就着這個簡單的按下事件,來分析一源碼

1、Activity

(1) dispatchTouchEvent()

現在以debug的模式來看源碼。

我的源碼是Android-23。

首先接收到事件的是就是acitivity,所以我們把斷點就放在Activity的dispatchTouchEvent()方法中

然後debug模式運行項目,點擊下一步進入Activity的dispatchTouchEvent()的源碼中:

上面的註釋大概說的是:你可以重寫此方法在所有窗體接收到事件之前將其攔截,如果不需要攔截,則保持原樣。

簡單來說呢,就是:我可以重寫該方法修改返回值使整個Activity都無法相應事件。

如果我在Activity中重寫該方法並修改了它的返回值,不管將該方法的返回值改爲true或者false,該事件都消失,不會再向下傳遞(可怕)。

所以不要修改Activity的dispatchTouchEvent()的默認返回值。

接下來我們來分析上面的代碼,首先是判斷事件是否是down事件,如果是的話執行了onUserInteraction()

這個方法是個空實現,我們可以在Activity中重寫該方法。這裏你要想到的是,因爲down事件是一個事件流的開端,並且這個方法放在了分發之前,在最上端,所以我們可以在onUserInteraction()中做一些事件響應開始的操作。

接下來執行了Window的superDispatchTouchEvent(),點進去你會發現Window中的該方法是一個空實現,但是根據斷點進去可以直接進到它的實現類:PhoneWindow(這也是斷點的一個好處不是嗎)。那麼我們來看看PhoneWindow的:

superDispatchTouchEvent()

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

代碼就一行,調用了Decor類的superDispatchTouchEvent()

但是!But!But,oh no!What’s Decor?

這裏是一篇講解Decor類的文章,我也簡單說一下:

爲什麼Activity可以利用setContentView()來設置佈局?誰是容器?

沒錯,就是Decor。我們查看Decor會發現,它是PhoneWindow的內部類,繼承自FrameLayout。

FrameLayout??難道我們setContentView()最終是調用了Decor.addView()嗎??

這裏我可以告訴你:雖然不是,但是原理就是這樣的!

看下面這張圖片就會明白頂層佈局的原理了:

titleBar就是標題欄,main.xml就是我們設置的佈局文件,而最外層是Decor。

好了,就到這裏。對Android窗體感興趣的小夥伴,快去看看這篇講解Decor類的文章

我們接着查看源碼,你會發現mDecor.superDispatchTouchEvent(event)最終調用的是ViewGroup的dispatchTouchEvent()

/**
    Decor類的superDispatchTouchEvent
    注意Decor類繼承自FrameLayout
    所以super指的即是FrameLayout
    但是FrameLayout沒有重寫該方法,查看源碼會直接跳轉至ViewGroup類
**/
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

到這裏,Activity已經成功將事件傳遞給ViewGroup,接下來將由ViewGroup來將事件一層一層傳遞至內部。

(2) onTouchEvent()

各位看官,請拉上去回頭看Activity的dispatchTouchEvent()的最後一行:
return onTouchEvent(ev)
是這樣的不?
也就是在getWindow.superDispatchTouchEvent()返回false時,將執行最後一行調用Activity的onTouchEvent(),所以Activity的onTouchEvent()的調用時機,是和接下來ViewGroup的事件分發有着密切關係的。
簡單來說:只有當Activity下的所有View(ViewGroup)的onTouchEvent()都不消費事件時,纔會調用Activity的onTouchEvent()

2、ViewGroup

我們很順利地來到了ViewGroup的dispatchTouchEvent()方法當中。
但是,在分析源碼之前,我想讓各位看官先明白,現在調用ViewGroup的dispatchTouchEvent()方法的變量,指的是誰?是我們的FrameLayou_ONE嗎?
看官:”你他喵的484傻,Activity源碼中明明寫着mDecor.superDispatchTouchEvent(event),很明顯是Activity調用了Decor的dispatchTouchEvent(),這個ViewGroup肯定是Decor啊!能不能講重點,這麼多廢話!“

我:“…..”


行吧,提這個我是想再給各位看官強調一下,我們的layout文件僅僅是add到了系統頂層佈局中,所以接收到事件的肯定是系統的頂層佈局,而不是我們的layout中的佈局。這個如何證實呢?
各位看官可以在ViewGroup的dispatchTouchEvent()打個斷點,然後看官就會發現,這個方法執行了很多次,每次都可以查看一下變量this,看看到底是什麼?比如第一次的this:

com.android.internal.policy.PhoneWindow$DecorView{b9a23df V.E...... R....... 0,0-1080,1920}

好了,接下來我們就看看ViewGroup的事件分發機制吧!

(1)dispatchTouchEvent()

ViewGroup的dispatchTouchEvent()內容很多,有210行左右,有興趣的同學可以打開ViewGroup去查看。我這裏就按着步驟一部分一部分的貼出來。有些部分我也看不大懂,不過有很多的註釋,閱讀起來還是很方便的:
首先執行的是這個判斷,判斷是否分發事件,如果該方法返回false,則此次事件則會被丟棄,因爲dispatchTouchEvent()的所有代碼都包含在這個判斷裏!

if (onFilterTouchEventForSecurity(ev)){}

下面是onFilterTouchEventForSecurity(ev)這個判斷的源碼:
根據方法名和註釋來理解,這是一個觸摸事件安全過濾器。基本上這個方法都會返回true,不會發生系統丟棄事件的情況。

    /**
     * Filter the touch event to apply security policies.
     *
     * @param event The motion event to be filtered.
     * @return True if the event should be dispatched, false if the event should be dropped.
     *
     * @see #getFilterTouchesWhenObscured
     */
public boolean onFilterTouchEventForSecurity(MotionEvent event) {
    //noinspection RedundantIfStatement
    if ((mViewFlags & FILTER_TOUCHES_WHEN_OBSCURED) != 0&& (event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
        // Window is obscured, drop this touch.
            return false;
    }
    return true;
}

第二件事情:如果是down事件(因爲down事件是開端),清除上次事件流的處理結果和狀態。

// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Throw away all previous state when starting a new touch gesture.
    // The framework may have dropped the up or cancel event for the previous gesture
    // due to an app switch, ANR, or some other state change.
    cancelAndClearTouchTargets(ev);
    resetTouchState();
}

第三件事情:檢查事件攔截是否攔截。

// Check for interception.
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;
}

第四件事情:檢查事件取消。

// Check for cancelation.
final boolean canceled = resetCancelNextUpFlag(this)|| actionMasked == MotionEvent.ACTION_CANCEL;

第五件事情:如果沒有被攔截,也沒有被取消,就將此次事件分發給自己的下一級:子ViewGroup或者子View。
這裏的分發邏輯相當複雜,有興趣的看官可以去自行閱讀源碼,或者參考這篇文章

if (!canceled && !intercepted) {}

在最後,又進行一次檢查取消標記,做了相應的處理:

// Update list of touch targets for pointer up or cancel, if needed.
if (canceled|| actionMasked == MotionEvent.ACTION_UP|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
    resetTouchState();
} 
else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
    final int actionIndex = ev.getActionIndex();
    final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
    removePointersFromTouchTargets(idBitsToRemove);
}

(2) onInterceptTouchEvent()

事件流的每個事件都是由外層向裏層依次傳遞的,有時候我們希望雖然點擊的是內部button,但是做出相應的是button的容器,而不是button,也就是說在事件傳遞到button之前,將事件攔截並且消費,這也就是事件攔截的作用。
事件攔截的方法源碼很簡單很簡單:

public boolean onInterceptTouchEvent(MotionEvent ev) {
   return false;
}

默認不攔截事件,如果攔截事件將返回值改爲true,事件將被攔截並交給攔截事件的ViewGroup的onTouchEvent()處理。
這個方法是ViewGroup獨有的,難道Activity和View就無法攔截事件了嗎?
首先,Activity的dispatchTouchEvent()的返回值不可輕易更改,更改之後整個Activity都無法響應事件,所以如果我們需要Activity做攔截操作,可以修改Activity的dispatchTouchEvent()的邏輯,使之滿足某些條件時修改返回值,否則不做修改。這樣就可以使Activity進行事件的攔截。
而至於View就更簡單了,View本身就是最小的控件,事件傳遞到View是已經無法向下傳遞,所以無需攔截。

(3) onTouchEvent()

ViewGroup類中沒有重寫onTouchEvent(),由於ViewGroup繼承了View,所以View和ViewGroup的onTouchEvent()完全一致。
這裏我們再debug一下我們的demo:
將斷點打到View的onTouchEvent()的第一行,然後點擊button,會發現斷點執行,而this指代的是我們的Button,而不是上層的ViewGroup,也就是說上層中根本沒有執行onTouchEvent(),第一次執行onTouchEvent()時,事件已經分發到了View。
所以在這裏,我們需要理解到這樣一個地步:

事件從Activity的dispatchTouchEvent()開始,首先分發到ViewGroup(DecorView、我們自己的Layout佈局文件等)的dispatchTouchEvent(),並且在每次分發的時候會調用onInterceptTouchEvent()判斷事件是否被攔截,如果事件被攔截則會執行將事件攔截的ViewGroup的onTouchEvent()方法並將接下來的整個事件流,都交給自己來處理,不會重複執行onInterceptTouchEvent(),如果沒有攔截事件,最終將事件成功傳遞給View,View將調用onTouchEvent()將事件消費。

上面這段話希望每位耐心看到這裏的讀者都能理解,在這裏還需要解釋一下的是ViewGroup攔截事件交給onTouchEvent()之後,如果onTouchEvenet()返回了false(ViewGroup的onTouchEvent()默認返回false),則代表不消費此事件,則此次事件將逐層向上傳遞,調用每一層的onTouchEvnet(),如果中途所有的onTouchEvent()都返回false,那麼事件將最終傳遞到Activity的onTouchEvent()中,到這裏事件完成整個傳遞過程(向裏傳遞,裏不要,再傳出來),不管Activity的onTouchEvent()返回什麼結果,事件都將消失。

3、View

(1)dispatchTouchEvent()

public boolean dispatchTouchEvent(MotionEvent event) {
       boolean result = false;
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(event, 0);
        }

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Defensive cleanup for new gesture
            stopNestedScroll();
        }

        if (onFilterTouchEventForSecurity(event)) {
            //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;
            }
        }

        if (!result && mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
        }

        // Clean up after nested scrolls if this is the end of a gesture;
        // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
        // of the gesture.
        if (actionMasked == MotionEvent.ACTION_UP ||
                actionMasked == MotionEvent.ACTION_CANCEL ||
                (actionMasked == MotionEvent.ACTION_DOWN && !result)) {
            stopNestedScroll();
        }

        return result;
}

以上則是View的部分源碼,我們只需要理解的是:在滿足了相應條件之後,View的dispatchTouchEvent()主動調用了自己的onTouchEvent(),而View的onTouchEvenet()默認返回true消費事件。

(2)onTouchEvent()

onTouchEvent是用來消費事件的,在Activity、ViewGroup和View三者當中,只有View的onTouchEvent()默認返回true,代表消費事件。雖然ViewGroup和View的onTouchEvent()完全相同,但是其中存在某些邏輯判斷,致使ViewGroup的返回值爲false。
推薦做法還是將事件處理的邏輯放到onTouchEvent()當中,而分發和攔截只發揮自己應有的作用即可。
這裏我就不貼View(ViewGroup)的onTouchEvent()的源碼了,想看的同學可自行打開View搜索onToucEvent()

四.總結

到這裏,三者的各個方法的分析已經結束了,我想最後再簡單總結一下:
1、Activity的dispatchTouchEvent()不可修改返回值,否則將導致整個Activity都無法接收事件,不管修改後返回true或是false。

2、在Activity中我們可以重寫onUserInteraction()方法,來在整個事件流響應之前做自己想做的事情。

3、一個事件的傳遞過程默認總是從Activity的dispatchTouchEvent()開始,到View的onTouchEvent()結束,注意,這裏是默認。

4、ViewGroup可以攔截事件交給自己的onTouchEvent()處理,如果自己的onTouchEvent()返回false,則事件不會消失,會繼續向上傳遞,直至傳遞到Activity的onTouchEvent()

5、ViewGroup的onInterceptTouchEvent()不會在每次事件都調用,整個事件流只會在down事件或是第一次接觸到事件時調用,所以不要在該方法中做事件邏輯處理。

6、三者當中只有View的onTouchEvent()默認消費事件。


其實還有很多更深入的東西,這篇文章只是淺顯地講解了一下Android的事件分發機制,希望對各位看官有些幫助!

有任何問題或疑問都可以聯繫我:[email protected]

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章