深入淺出Android事件分發機制:最全面最易懂:實踐篇(二)

歡迎關注程式引力
如何提升安卓水平?安卓開發者必須瞭解的事件分發機制。
最全面、最易懂的形式來講解Android事件分發機制。
若有錯漏,煩請斧正。轉載請註明出處。
- 作者:程式引力 | 謝一 (Evan Xie)
- 郵箱: ofollow,noindex">[email protected]
0. 前言
鑑於安卓分發機制較為複雜,故分為多個層次進行講解,分別為基礎篇、實踐篇與高階篇。
- (一)基礎篇:從基本概念入手,介紹了分發機制中的核心方法,通過分析其核心邏輯,總結其事件分發機制。
- (二)實踐篇:該篇設計了簡單與複雜的兩個demo樣例,從現象與應用的角度去講解分發機制的核心內容,幫助讀者從另一個角度理解事件分發機制。
- (三)高階篇:從原始碼角度去分析分發機制背後的原因,讓讀者對分發機制背後的本質有更為全面與深刻的理解。
1. 內容簡介
本文內容為(二)實踐篇,本篇主要設計了兩個demo樣例,分別為基礎樣例與複雜樣例。在基礎樣例中,只涉及單個的Activity、ViewGroup與View,對該樣例中的事件實際分發情況進行分析,並總結其規律。在複雜樣例中,涉及多個ViewGroup相互巢狀的情況,該樣例更符合開發者在開發實際應用時所遇到的情況,由其現象得到的結論更具有借鑑意義。
2. 基本樣例分析
經過(一)基礎篇對分發過程的介紹,相信讀者對安卓事件的分發過程已經有了一個基本的瞭解。下面以一個最基本的佈局為例,看看事件分發過程是否真的如此。

事件分發者結構
佈局檔案如下:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" android:padding="50dp" tools:context=".lesson01.part01.OuterActivity"> <com.evanxie.tutorial.lesson01.part01.LayoutX android:background="@color/blue" android:id="@+id/layout2" android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent"> <com.evanxie.tutorial.lesson01.part01.TextViewX android:background="@color/colorAccent" android:id="@+id/tv21" android:layout_margin="90dp" android:textSize="20sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView" /> <com.evanxie.tutorial.lesson01.part01.TextViewX android:background="@color/colorAccent" android:id="@+id/tv22" android:layout_margin="90dp" android:textSize="20sp" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="TextView" /> </com.evanxie.tutorial.lesson01.part01.LayoutX> </LinearLayout>
對於Activity,主要是覆寫了如下方法,方便檢視日誌:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.i(TAG, "dispatchTouchEvent: "); return super.dispatchTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.i(TAG, "onTouchEvent: "); return super.onTouchEvent(event); }
對於Layout(ViewGroup),主要是覆寫了如下方法:
@Override public boolean dispatchTouchEvent(MotionEvent ev) { Log.i(TAG, "dispatchTouchEvent: " + getIdName()); return super.dispatchTouchEvent(ev); } @Override public boolean onInterceptTouchEvent(MotionEvent ev) { Log.i(TAG, "onInterceptTouchEvent: " + getIdName()); return super.onInterceptTouchEvent(ev); } @Override public boolean onTouchEvent(MotionEvent event) { Log.i(TAG, "onTouchEvent: " + getIdName()); return super.onTouchEvent(event); } private String getIdName() { return getResources().getResourceEntryName(getId()); }
對於TextView,主要是覆寫了如下方法:
@Override public boolean dispatchTouchEvent(MotionEvent event) { Log.i(TAG, "dispatchTouchEvent: " + getText()); return super.dispatchTouchEvent(event); } @Override public boolean onTouchEvent(MotionEvent event) { Log.i(TAG, "onTouchEvent: " + getText()); return super.onTouchEvent(event); }
點選其中的TextView,並篩選其dispatchTouchEvent方法的日誌,得到如下日誌:
L0101: OuterActivity: dispatchTouchEvent: L0101: LayoutX: dispatchTouchEvent: layout2 L0101: TextViewX: dispatchTouchEvent: TextView
從該日誌可以看出,事件是從Activity -> Layout(ViewGroup) -> TextView(View)進行傳播的。從這裡可以看出,有些文章中指出的,事件是由內部View(如button)開始傳播的,這是有誤的。
若取消前面的篩選,顯示幾個核心方法的日誌,可以得到:
L0101: OuterActivity: dispatchTouchEvent: L0101: LayoutX: dispatchTouchEvent: layout2 L0101: LayoutX: onInterceptTouchEvent: layout2 L0101: TextViewX: dispatchTouchEvent: TextView L0101: TextViewX: onTouchEvent: TextView L0101: LayoutX: onTouchEvent: layout2 L0101: OuterActivity: onTouchEvent:
從上面日誌可以看出,Activity由於沒有onInterceptTouchEvent()方法,所以在呼叫了dispatchTouchEvent()方法後,就呼叫了Layout的同名方法,但是在Layout卻有所不同,它在呼叫dispatchTouchEvent()方法後,還呼叫了onInterceptTouchEvent()方法。
因為我們並沒有改變方法後,還呼叫了onInterceptTouchEvent()方法的返回值,故該事件繼續分發,傳遞給TextView在呼叫dispatchTouchEvent()處理。若不能繼續分發,則會呼叫onTouchEvent()方法,同時,該方法也不能處理的話,則會向上傳播,呼叫ViewGroup、Activity的onTouchEvent方法。
從這樣的現象處罰,可以觀察到如下結論:
- 結論5.1:事件分發的順序是 Activity -> Layout(ViewGroup) -> TextView(View)
- 結論5.2:事件分發是一個遞迴呼叫的規程,通過dispatchTouchEvent()進行遞迴呼叫。若考慮事件的整個傳播過程,其分發的順序是 Activity -> Layout(ViewGroup) -> TextView(View) -> Layout(ViewGroup) -> Activity.
- 結論5.3:事件若分發到ViewGroup,則首先會呼叫其onInterceptTouchEvent()方法,若該方法返回TURE,則事件不會繼續向下傳遞,而是交由其自身onTouchEvent()處理,然後可能由onTouchEvent()由下往上傳遞回去。
3. 複雜樣例分析
在實際的專案中,佈局結果往往不會僅僅是上面基本樣例那樣簡單。實際情況是可能會存在多個佈局巢狀。故本節以一個較為複雜的樣例來分析事件分發過程,理解該複雜樣例的分發機制,有利於幫助開發者在實際專案中解決實際問題。
其Activity、Layout(ViewGroup)、與TextView的Java類程式碼與基本樣例分析中的一致。唯獨佈局變得更為複雜,其佈局示意圖如下:

0102複雜樣例佈局圖
對於該圖中各空間的命名做如下約定:即 Name_X_Y。其中Name表示元件名,X表示層次,Y表示該層中第幾個元件。對於TextView_2_3,表示位於第2層、第三個的TextView元件,對於Layout_3_4,表示處在第3層中第4個的Layout(ViewGroup)元件。最外層為第一層,向內一次遞增。
為了更好地理解這些元件的關係,將它們繪製成樹狀圖。其樹狀圖如下:

0102複雜樣例佈局樹狀圖
從樹狀圖中,可以清晰地看到該佈局中有4層,第一層為一個Layout(ViewGroup),其內部有4個元件,分別是2個Layout(ViewGroup)與TextView。其餘的元件關係也非常清晰。該複雜樣例在設計時,考慮了ViewGroup多層巢狀的情況,通過該該樣例的實驗,可以對安卓事件分發機制有更深刻的理解,對於解決實際安卓應用的事件分發問題有一定的借鑑意義。
將該樣例的程式碼執行起來後,點選TextView_4_3,設定篩選條件,只檢視dispatchTouchEvent()方法的日誌,得到相應的日誌輸出,結果如下
L0102: OuterActivity: dispatchTouchEvent: L0102: LayoutX: dispatchTouchEvent: Layout_1 L0102: LayoutX: dispatchTouchEvent: Layout_2_2 L0102: LayoutX: dispatchTouchEvent: Layout_3_4 L0102: TextViewX: dispatchTouchEvent: TextView_4_3
從該日誌輸出可以看出,分發事件從最外部的OuterActivity開始,然後傳遞第1層的Layout_1,然後依次傳給Layout_2_2 -> Layout_3_4 -> TextView_4_3.
若將參與分發的元件用橙色標識出來,其傳遞的路徑如下圖所示:

0102複雜樣例佈局樹狀點選圖
從這樣的結果可以看出,事件在複雜佈局的情況下,並沒有去遍歷每一個子元件。
對於網路上的部分教程,其表示事件在傳播時會遍歷子View,這是比較模糊的。部分教程表示會ViewGroup會遍歷其子View的dispatchTouchEvent()方法,那這可以說是有誤的。
事實上,在一般情況下,ViewGroup並不會遍歷其View的dispatchTouchEvent()方法,而像是”知道“被點選的子View的最短路徑一樣,通過該路徑去分發事件,並讓事件抵達被點選的元件。對於這個樹狀圖,其餘兄弟節點的元件的dispatchTouchEvent()方法並不會被呼叫。這是一個非常重要的結論。
從該現象出發,可以得到結論:
- 結論6.1:在一般情況下,可以認為事件分發是以‘最短路徑’來分發的。
實際上,在原始碼中會判斷點選的位置座標是否處於其他元件的範圍內,如果在點選位置的座標在其範圍之外,則不會去呼叫其dispatchTouchEvent()。這也就是上面結論的原因。除了一些較為特殊的情況,例如在同一層的元件存在重疊的情況下,其兄弟元件的dispatchTouchEvent()方法是可能被呼叫的。在一般情況下,就可以認為事件分發是以‘最短路徑’來分發的。
若將日誌的篩選條件去掉,點選TextView_4_3的日誌如下所示:
L0102: OuterActivity: dispatchTouchEvent: L0102: LayoutX: dispatchTouchEvent: Layout_1 L0102: LayoutX: onInterceptTouchEvent: Layout_1 L0102: LayoutX: dispatchTouchEvent: Layout_2_2 L0102: LayoutX: onInterceptTouchEvent: Layout_2_2 L0102: LayoutX: dispatchTouchEvent: Layout_3_4 L0102: LayoutX: onInterceptTouchEvent: Layout_3_4 L0102: TextViewX: dispatchTouchEvent: TextView_4_3 L0102: TextViewX: onTouchEvent: TextView_4_3 L0102: LayoutX: onTouchEvent: Layout_3_4 L0102: LayoutX: onTouchEvent: Layout_2_2 L0102: LayoutX: onTouchEvent: Layout_1 L0102: OuterActivity: onTouchEvent: L0102: OuterActivity: dispatchTouchEvent: L0102: OuterActivity: onTouchEvent:
從該日誌可以印證前面的結論,事件從Activity想多個ViewGroup分發,最終抵達TextView,若沒有元件能處理該事件,則通過onTouchEvent傳遞回去。
請注意日誌的最後兩行,似乎又有事件開始傳遞。實際上,前面一起的傳遞過程是‘按下’事件的傳遞過程,也是本文預設討論的事件。最後兩行是‘抬起’事件的傳遞過程。因為‘按下’事件在自上向下,又自下向上傳遞後,都沒有元件能夠處理,故預設Activity將該事件處理了。而對於‘按下’事件之後的其他事件,如‘抬起’事件,則預設交給處理了‘按下’事件的元件。
- 結論6.2:在一個事件序列(事件流)中,第一個事件(按下)被哪個元件處理了,那後續事件都會被直接交給這個元件處理。
再進行一次點選事件,此時不在點選TextView_4_3,而是點選佈局圖中僅僅屬於Layout_2_2的綠色區域,得到的日誌輸出如下:
L0102: OuterActivity: dispatchTouchEvent: L0102: LayoutX: dispatchTouchEvent: Layout_1 L0102: LayoutX: onInterceptTouchEvent: Layout_1 L0102: LayoutX: dispatchTouchEvent: Layout_2_2 L0102: LayoutX: onInterceptTouchEvent: Layout_2_2 L0102: LayoutX: onTouchEvent: Layout_2_2 L0102: LayoutX: onTouchEvent: Layout_1 L0102: OuterActivity: onTouchEvent:
從該現象可以看出,事件傳遞到Layout_2_2後,就不會再繼續向下傳播了,哪怕它還有許多子View。
- 結論6.3:當事件分發的被點選的元件時,則停止傳播。不管這個元件是否還有子元件。
4. 總結
為了讓讀者更好地理解安卓事件呼叫機制,本文從實驗的角度,設計了分別是較為基礎的與較為複雜的兩種樣例,從該實驗的現象出發,得出了若干結論。通過對這些結論的思考,可以幫助讀者更深刻地理解安卓事件的分發機制,也可以幫助讀者直接應用到實際專案中。此時思考(一)基礎篇中的事件分發的核心方法邏輯,會發現其實安卓的事件分發機制原來如此簡單。
- 結論1:事件分發的順序是 Activity -> Layout(ViewGroup) -> TextView(View)
- 結論2:事件分發是一個遞迴呼叫的規程,通過dispatchTouchEvent()進行遞迴呼叫。若考慮事件的整個傳播過程,其分發的順序是 Activity -> Layout(ViewGroup) -> TextView(View) -> Layout(ViewGroup) -> Activity.
- 結論3:事件若分發到ViewGroup,則首先會呼叫其onInterceptTouchEvent()方法,若該方法返回TURE,則事件不會繼續向下傳遞,而是交由其自身onTouchEvent()處理,然後可能由onTouchEvent()由下往上傳遞回去。
- 結論4:在一般情況下,可以認為事件分發是以‘最短路徑’來分發的。
- 結論5:在一個事件序列(事件流)中,第一個事件(按下)被哪個元件處理了,那後續事件都會被直接交給這個元件處理。
- 結論6:當事件分發的被點選的元件時,則停止傳播。不管這個元件是否還有子元件。
若有錯漏,煩請斧正。轉載請註明出處。
- 作者:程式引力 | 謝一 (Evan Xie)
- 郵箱: [email protected]