Android教程網
  1. 首頁
  2. Android 技術
  3. Android 手機
  4. Android 系統教程
  5. Android 游戲
 Android教程網 >> Android技術 >> 關於Android編程 >> 一步步理解Android事件分發機制

一步步理解Android事件分發機制

編輯:關於Android編程

回想一下,通常在Android開發中,我們最常接觸到的是什麼東西?顯然除了Activity以外,就是各種形形色色的控件(即View)了。

與此同時,一個App誕生的起因,終究是根據不同需求完成與用戶的各種交互。而所謂的交互,本質就是友好的響應用戶的各種操作行為。

所以說,有很多時候,一個控件(View)出現在屏幕當中,通常不會是僅僅為了擺設,而是還要能夠負責響應用戶的操作。

以最基本的例子而言:現在某一個界面中有一個按鈕(Button),而每當用戶點擊了該按鈕,我們的程序將做出一定回應。

那麼,如果我們還原“點擊按鈕”這一行為,可以視作其根本實際就是:用戶的手指 與屏幕上該按鈕所處的位置發生了某種接觸。

所以簡單來說,我們可以將用戶每次與屏幕發生接觸,視作是一次“觸摸事件”。而對於每次“觸摸事件”的回應處理,就構成了所謂的“交互”。

由此,也就引出了我們今天在此文裡想要弄清楚的一個知識點,即“Android的事件分發機制”。接著,我們就一步一步的來走進它。

為什麼需要分發?

在一切開始之前,我們先考慮一個問題。那就是為什麼我們發現 關於這個知識點總是被描述為“事件分發”而不是“事件處理”?試想以下情況:

小明注冊了一家公司開始了自己的創業之旅。這天,一位客戶上門為小明的公司帶來了第一單業務。我們可以將“這單業務”視作一次“觸摸事件”。

小明樂壞了,趕緊開始著手這單業務。對應來說,我們可以理解為對“觸摸事件”的處理。是的,目前為止我們都沒發現任何和“分發”相關的東西。

其實,我們不難想象到之所以沒有出現任何“分發”相關的東西,是因為現在公司只有小明獨自一人,除了自己處理,他別無它選。

對於“分發”這個詞本身就是以一定數量作為基礎的。比如小明的公司經過發展擴大到了十個人的規模,劃分了部門。這天又有新的業務來了。

這個時候小明就有很多選擇了,他可以選擇自己來處理這單業務;當然也可以選擇將業務派發下去讓員工來處理。這就是我們所謂的“分發”。

這個案例對應到Android中來說是一樣的,如果我們能保證當前屏幕上永遠只有唯一的一個View,那麼對於“觸摸事件”的處理就沒什麼好說的了。

但顯然一個界面中通常都不會只有一個“獨苗”,它會涉及到不定量的View(ViewGroup)的組合。這個時候當“事件”發生,就不那麼容易處理了。

這其實也是為什麼,“Android的事件分發機制”是我們從菜鳥到進階的過程中繞不開必須掌握的一個知識點。

但實際上也沒關系,當我們明白了“小明創業”的這個例子,實際也就已經掌握了關於事件分發最基本的原理。

看事件如何分發?

我們說了所有與用戶發生交互的行為都是在手機屏幕,即某個界面中產生的。而Android的界面中的元素,無非就是View與ViewGroup。
與之同時,我們關注的另一個關鍵點是代表操作行為的“觸摸事件”,這對應到英文中來說似乎就是“TouchEvent”。
OK,在我們目前兩眼一抹瞎的情況下。不妨試著以“TouchEvent”為關鍵字,到View與ViewGroup類中去研究研究相關的方法。

最終,我們單獨提出以下幾個需要我們理解的方法,它們也是事件分發機制的原理所在:

dispatchTouchEvent onTouchEvent onInterceptTouchEvent (只存在於ViewGroup)

好的,截止現在我們還不太明白這幾個方法的具體作用。只知道如果單獨從命名上看,它們似乎分別代表分發、處理和攔截類似的作用。

起源在哪?

從現在開始,我們要正式的一步步研究一個觸摸事件的處理過程。我們先來看一個十分基礎但具有一定代表性的布局:




    

但如果是這樣,我們還是無法研究其事件的分發過程,所以我們自定義兩個類分別繼承LinearLayout以及Button類。
而我們要做的工作也很簡單,就是在我們之前提及的與“事件分發”相關的方法中,分別加上類似如下的日志打印:

Log.d(getTag().toString(),”onTouchEvent”);

然後我們將布局文件中的LinearLayout和Button分別替換成我們自己定義的類,就搞定了。此時,我們運行程序後,對按鈕進行點擊,得到如下日志:

group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent

一口氣吃不成一個胖子,我們這裡首先只關注一點,那就是:當我們點擊按鈕後,最先被觸發的是“group_a“也就是最外層layout的dispatchTouchEvent方法。這個道理其實並不難理解,就像“小明創業”一樣,如果一單業務來臨,自然應該先由最高級別的“小明”知會。

也就是說,當一個“觸摸事件”產生,將最先傳送到最頂層View。這看上去合情合理,但我們又不難想到,如果談到“最頂層”的話:
那麼,我們所有的View,即我們整個布局文件,它實際也有一個載體,那就是它的宿主Activity。因為你一定記得”setContenView(R.layout.xxx)”。
那麼,我們想象一下,“觸摸事件”會不會最先被傳送到Activity呢?打開Activity的源碼我們驚喜的發現:它也包含dispatchTouchEvent和OnTouchEvent方法。OK,我們接著要做的,自然就是在我們的Activity類中覆寫這兩個方法,也加上日志打印,再次運行測試得到如下日志:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent

通過日志,我們不難發現,正如我們推測的一樣,當觸摸事件產生,的確是最先傳遞給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來分發事件的。

if (getWindow().superDispatchTouchEvent(ev))

Window自身是一個抽象類,而其superDispatchTouchEvent也是一個抽象方法。其唯一實現存在PhoneWindow類中。
而在PhoneWindow當中,superDispatchTouchEvent的實現是這樣的:

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

也就是,這時候事件的分發會傳遞到mDecor,即Android視圖結構中所謂的“DecorView”當中,而DecorView自身本就是FrameLayout。
所以,這個時候事件分發的本質就很簡單了,它回歸到了FrameLayout,即ViewGroup的事件分發。

P.S: DecorView涉及到了Android的UI界面架構的知識。我們通常可以最簡單的理解為一個Activiy就是一個界面。

但其實隨著對Android越來越熟悉,我們也可以很容易猜測到,實際上肯定不僅僅如此。那麼我們可以這樣簡單理解:

Activity會附屬在PhoneWindow上;這其中會首先包裹一個DecorView(翻譯就是裝飾視圖),它本身是一個FrameLayout。
(所以,我們其實可以理解為,DecorView才是我們界面中真正位於最頂層的父View) 而同時,通常來說,DecorView中會有兩個子View,分別是:TitleView及ContentView。 其中,TitleView簡單來說就是我們平時所說的ActionBar(TitleBar)的那一列。而ContentView我們就熟悉了:“setContenView(R.layout.xxx)”

好了,話到這裡,我們先對我們目前的收獲進行總結,很簡單:
當一個“觸摸事件”產生,會首先傳遞到當前Activity。Activity會通過其所屬Window,找到其中DecorView,從而開始逐級向下分發事件。

繼續傳遞

回憶我們點擊按鈕時,所產生的輸出日志:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent

我們發現,經由MainActivity進行傳遞,最終會首先到達該Activity的ContentView,即我們定義的布局文件中的最外層LinearLayou上。
這之後發生的事,我們通過日志觀察的現象似乎是:事件在繼續向下傳遞,直到最後到達了最裡的Button,才通過onTouchEvent進行了處理。
所以,我們大膽猜測:一個“觸摸事件”發生,會逐級dispatchTouchEvent,直至到達最底層的View,然後通過onTouchEvent進行處理。
這看上去是說得通的,但唯一的缺陷就在於,onInterceptTouchEvent 這個名字透露著攔截事件的方法,似乎並沒有得到什麼發揮?

那麼,我們干脆直接打開ViewGroup中onInterceptTouchEvent的源碼:

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

我們發現源碼特別簡單,唯一值得注意的是,該方法有一個布爾型的返回值,並且默認的返回值是false。
秉承著一貫的“手賤主義”的思想,我們將我們自定義的LinearLayou類中的onInterceptTouchEvent返回值修改為true。然後再次測試:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_a: onTouchEvent

我們發現日志打印已經發生了改變,通過日志我們觀察到的是:事件傳遞到group_a後不再向下傳遞了,直接通過group_a的onTouchEvent進行處理。
這一現象其實是合理的,它告訴了我們:onInterceptTouchEvent的返回結果將決定事件是否被攔截在當前View層面。返回true則攔截;否則繼續傳遞。
而onInterceptTouchEvent的返回結果之所以能實現上述效果,可以在ViewGroup類中的dispatchTouchEvent方法裡找到答案。
ViewGroup當中的dispatchTouchEvent源碼很多,也很復雜,我們很難去讀個透徹。但通過部分關鍵代碼,我們可以知道它的原理是如下:
ViewGroup裡,dispatchTouchEvent裡會調用到onInterceptTouchEvent方法,並通過判斷其返回結果決定下一步操作:

如果onInterceptTouchEvent返回true,則會調用onTouchEvent方法處理觸摸事件。 如果onInterceptTouchEvent返回false,則會通過child.dispatchTouchEvent的形式向下分發事件。

到現在,看上去我們基本已經對事件的整個分發流程都觸碰到了。但我們想象這樣一種情況:
小明的公司接收了一單新的業務,並分發給了底下的小王去做。但這時候的問題可能是:
小王收到通知後發現自己完成不了。或者說小王給出了一套方案,但他對該方案的信心並不足。
這樣的問題應該怎麼解決,很顯然,小王應該“報告老板”。這單業務最後的處理方案可能還是得您來拿。

這對應到我們的程序中,應該如何來實現呢?我們發現,onTouchEvent這個方法也有一個布爾型的返回值。
現在,我們試著將我們之前自定義的Button類裡的onTouchEvent的返回值修改為固定返回false。再次運行程序測試:

MainActivity: dispatchTouchEvent
group_a: dispatchTouchEvent
group_a: onInterceptTouchEvent
group_b: dispatchTouchEvent
group_b: onInterceptTouchEvent
view: dispatchTouchEvent
view: onTouchEvent
group_b: onTouchEvent
group_a: onTouchEvent

我們發現,事件在經過button類裡onTouchEvent處理後, 又回傳到上層目錄繼續處理了。
由此我們可以知道:onTouchEvent的返回值也是另一種“攔截”,不同的是之前我們說的是攔截事件繼續向下傳遞。
而這裡,是在表明,這個事件“我”是否能夠完全處理。如果能,則返回true,那麼事件經過“我”處理後就結束了。否則返回false,再讓父View去處理。

好了,這裡我們先總結一下到目前為止,我們掌握到的關於事件分發機制的流程:

當一個觸摸事件產生,會首先傳遞到其所屬Activity。Activity將負責將事件向下進行分發。 該觸摸事件將逐級的依次向下傳遞,直到傳遞至最底層View,然後進行處理。 ViewGroup在向下傳遞事件的時候,通過onInterceptTouchEvent的返回值來判斷是否攔截事件。 如果確定攔截事件,那麼此次一系列的觸摸事件都會通過該ViewGroup的onTouchEvent方法進行處理,並不再向下傳遞 onTouchEvent的返回結果決定了事件在此次處理之後,是否需要回傳到上一級的View。

好了,我們繼續看。我們肯定也注意到了,說了這麼多,但我們之前關於事件分發的重點,都是放在上一層View向下層View分發事件的過程。
也就是說,之前我們的重點是在分析ViewGroup的dispatchTouchEvent方法,我們說了該方法會判斷是否攔截事件,而決定是否繼續向下傳遞事件。
當就拿我們此文中的例子來說,當事件最終傳遞到button類當中。我們不難想象到,這時dispatchTouchEvent的工作肯定與之前有所不同。
因為Button自身只是一個View,它不會存在所謂的子視圖。也就說,這個時候事件你肯定要做出處理了,那麼還分發個什麼勁呢?

要得到這個問題的答案,最好的方式當然就是打開View類裡的dispatchTouchEvent方法的源碼看看,這裡我們只截取我們關心的代碼部分:

        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;
            }
        }

我們看到,首先會獲取一個ListenerInfo類型的變量。該類型實際就是封裝了各種類型的Listener。隨後會開啟第一次判斷:
如果此對象不為空,mOnTouchListener不為空,且(mViewFlags & ENABLED_MASK) ==ENABLED,則會執行mOnTouchListener。
這其中(mViewFlags & ENABLED_MASK) ==ENABLED是判斷控件狀態是否是ENABLE的,默認情況肯定是的。
mOnTouchListener看著有些眼熟,如果我們查看源碼,你就更眼熟了,因為我們發現它是通過如下代碼進行賦值的:

    public void setOnTouchListener(OnTouchListener l) {
        getListenerInfo().mOnTouchListener = l;
    }

接著看代碼,我們發現如果li.mOnTouchListener.onTouch(this, event)執行的結果也為true,那麼result也將被設置為true。
假設該方法的執行結果返回是false,那麼以上代碼將繼續進行,從而開始第二輪判斷,於是onTouchEvent方法得以執行。

OK,對於這裡的代碼,我覺得有兩點是值得一說的。第一是,我們首先可以明白如下的結論:

假如我們為某個View設置了OnTouchListener,那麼listener會先於onTouchEvent執行。同時: 如果listener的onTouch方法返回true,那麼onTouchEvent將不再執行。

第二點是,我們看到對於View來說:假設onTouchEvent的返回值為false,那麼dispatchTouchEvent也會返回false。
這實際上就構成了整個“onTouchEvent返回值可以決定事件是否回傳”的原因。這是因為:
ViewGroup在通過child.dispatchTouchEvent向下分發事件時,會通過該返回值來判斷子視圖是否具備處理事件的能力。
如果返回為false,就會繼續遍歷子視圖,直至遍歷到有一個具備事件處理能力的子視圖為止。
如果沒有一個具備處理能力的子視圖,那麼ViewGroup就會自己通過onTouchEvent來解決了。

到了這裡,我們實際上已經對整個Android事件的分發機制有了不錯的了解了。但我們肯定會想到一個問題:
那就是,以Button來說,我們通常是通過setOnClickListener的方式來對其設置點擊事件監聽的。
但很顯然,我們目前為止研究過的代碼中,還沒有出現任何與之相關的東西。在View類中,我們剩下沒有看過代碼的,就是onTouchEvent了:

        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
                    if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if (isFocusable() && isFocusableInTouchMode() && !isFocused()) {
                            focusTaken = requestFocus();
                        }

                        if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed.  Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                       }

                        if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
                            // This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if (!focusTaken) {
                                // Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if (!post(mPerformClick)) {
                                    performClick();
                                }
                            }
                        }

以上代碼是我們關心的原因所在,我們看到經過一系列的判斷後,會進入到一個名為performClick的方法調用,打開該方法的源碼:

    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);
        return result;
    }

綜合以上兩段代碼,我們可以得出以下結論:

onClickListener會在onTouchEvent方法中,ACTION為UP的時候得以執行。 但注意前提條件是,控件是可點擊(長按)的;並且mOnClickListener不為空。

最後的總結與比喻

最後,我們仍舊以“小明創業“的例子來總結我們所學到的關於Android的事件分發的相關知識。

公司(Activity)接收了一項新的業務(觸摸事件)。 該業務會最先到達公司的最高層小明手上(最頂層View)。 小明研究了以下此次業務,此時可以分為兩種情況:
1、小明認為此業務難度不大,於是決定將業務分配給部門經理小王(dispatch-event)。
2、小明認為此次業務至關重要,決定自己處理(intercept-event & onTouchEvent)。 假設業務分配給了經理小王,此時小王與小明一樣,同樣有兩種選擇:即自己攔截處理或者繼續向下布置。 業務最終終將被分配到某個員工的手中,它會負責處理(onTouchEvent)。 但該員工接收到任務後,也可以判斷自己是否能夠完成,如果覺得不能完成,
或者還需要上級審核,可以選擇回報給上級(onTouchEvent返回false) 員工處理該任務的方式也許有多種,例如A(OnTouchListener)、B(onTouchEvent)、C(OnClickListener) 等等。
(它們的優先級順序是 A > B > C 。。。。 )
  1. 上一頁:
  2. 下一頁:
熱門文章
閱讀排行版
Copyright © Android教程網 All Rights Reserved