如何提升安卓水準?安卓開發者必須了解的事件分發機制。
最全面、最易懂的形式來講解Android事件分發機制。
0. 前言
鑒于安卓分發機制較為複雜,故分為多個層次進行講解,分别為基礎篇、實踐篇與進階篇。
- (一)基礎篇:從基本概念入手,介紹了分發機制中的核心方法,通過分析其核心邏輯,總結其事件分發機制。
- (二)實踐篇:該篇設計了簡單與複雜的兩個demo樣例,從現象與應用的角度去講解分發機制的核心内容,幫助讀者從另一個角度了解事件分發機制。
- (三)進階篇:從源碼角度去分析分發機制背後的原因,讓讀者對分發機制背後的本質有更為全面與深刻的了解。
1. 内容簡介
本文内容為(一)基礎篇,本篇主要對事件分發中的基本概念做了介紹。同時,介紹了負責參與分發事件的主要方法。從這些方法的核心邏輯中,總結事件分發的規律。避免了許多文章直接給初學者講解源碼所帶來的困惑。
本文深入淺出,通過閱讀本文,可以幫助開發者對安卓事件分發機制有一個整體的了解,并且能夠幫助開發者快速解決一些常見的實際問題,進而實作快速開發。
2. 被分發的對象
被分發的對象是那些?被分發的對象是使用者觸摸螢幕而産生的點選事件,事件主要包括:按下、滑動、擡起與取消。這些事件被封裝成MotionEvent對象。該對象中的主要事件如下表所示:
事件 | 觸發場景 | 單次事件流中觸發的次數 |
---|---|---|
MotionEvent.ACTION_DOWN | 在螢幕按下時 | 1次 |
MotionEvent.ACTION_MOVE | 在螢幕上滑動時 | 0次或多次 |
MotionEvent.ACTION_UP | 在螢幕擡起時 | 0次或1次 |
MotionEvent.ACTION_CANCLE | 滑動超出控件邊界時 | 0次或1次 |
按下、滑動、擡起、取消這幾種事件組成了一個事件流。事件流以按下為開始,中間可能有若幹次滑動,以擡起或取消作為結束。
在安卓對事件分發的處理過程中,主要是對按下事件作分發,進而找到能夠處理按下事件的元件。對于事件流中後續的事件(如滑動、擡起等),則直接分發給能夠處理按下事件的元件。故本文讨論的内容則是主要針對按下事件的。
3. 分發事件的元件
分發事件的元件,也稱為分發事件者,包括Activity、View和ViewGroup。它們三者的一般結構為:
從上圖中可以看出,Activity包括了ViewGroup,ViewGroup又可以包含多個View。
元件 | 特點 | 舉例 |
---|---|---|
Activity | 安卓視圖類 | 如MainActivity |
ViewGroup | View的容器,可以包含若幹View | 各種布局類 |
View | UI類元件的基類 | 如按鈕、文本框 |
4. 分發的核心方法
負責對事件進行分發的方法主要有三個,分别是:
- dispatchTouchEvent(
- onTouchEvent()
- onInterceptTouchEvent()。
它們并不存在于所有負責分發的元件中,其具體情況總結于下面的表格中:
元件 | dispatchTouchEvent | onTouchEvent | onInterceptTouchEvent |
---|---|---|---|
Activity | 存在 | 存在 | 不存在 |
ViewGroup | 存在 | 存在 | 存在 |
View | 存在 | 存在 | 不存在 |
從表格中看,dispatchTouchEvent,onTouchEvent方法存在于上文的三個元件中。而onInterceptTouchEvent為ViewGroup獨有。這些方法的具體作用在下文作介紹。
ViewGroup類中,實際是沒有onTouchEvent方法的,但是由于ViewGroup繼承自View,而View擁有onTouchEvent方法,故ViewGroup的對象也是可以調用onTouchEvent方法的。故在表格中表明ViewGroup中存在onTouchEvent方法的。
5. 事件分發過程
這一小節是本文的核心内容,會從整體上對事件的分發過程作介紹。
對于事件分發過程從,筆者認為網上的一些教程中的觀點是有誤的。
- 網上部分教程認為事件是從内部(如Button)開始分發的,這是有誤的。
- 網上部分教程常使用’向上‘、’向下‘傳播等描述,但又未對‘何為上’、‘何為下’作解釋。
- 網上部分教程将Java的子類對象調用父類方法(向上轉型)的過程也稱為‘向上’傳播,即将事件在元件之間的傳播與程式語言多态特性混為一談,讓初學者費解。
- 子類在覆寫的方法中調用父類的同名方法,被稱為’向上傳播‘,這也是不對的。
為此在介紹分發過程之前,先對一些概念作定義:
- 向下傳播:Activity包括Layout,事件從Activity向Layout傳播被稱作’向下傳播‘。Layout包含若幹View,事件從Layout向其子View傳播,也被稱為’向下傳播‘。
- 向上傳播:與’向下傳播‘相反。
’向上轉型‘不能稱為傳播,即子類對象調用父類方法,或在覆寫的方法中調用父類方法,都不能稱為傳播。不能将面向對象程式語言中的概念與布局層次中的上下傳播混為一談。
分發方法dispatchTouchEvent
從方法的名稱中可以看出該方法主要是負責分發,是安卓事件分發過程中的核心。事件是如何傳遞的,主要就是看該方法,了解了這個方法,也就了解了安卓事件分發機制。
在了解該方法的核心機制之前,需要知道一個結論:
- 如果某個元件的該方法傳回TRUE,則表示該元件已經對事件進行了處理,不用繼續調用其餘元件的分發方法,即停止分發。
- 如果某個元件的該方法傳回FALSE,則表示該元件不能對該事件進行處理,需要按照規則繼續分發事件。在不複寫該方法的情況下,除了一些特殊的元件,其餘元件都是預設傳回False的。後續有例子說明。
為何傳回TRUE就不用繼續分發,而傳回FALSE就停止分發呢?為了解決這個疑問,需要看一看該方法的具體分發邏輯。為了便于了解,下面對dispatchTouchEvent方法進行簡化,隻保留最核心的邏輯。
Activity的dispatchTouchEvent方法
// Activity中該方法的核心部分僞代碼
public boolean dispatchTouchEvent(MotionEvent ev) {
if (child.dispatchTouchEvent(ev)) {
return true; //如果子View消費了該事件,則傳回TRUE,讓調用者知道該事件已被消費
} else {
return onTouchEvent(ev); //如果子View沒有消費該事件,則調用自身的onTouchEvent嘗試處理。
}
}
首先,從核心邏輯中看出,當事件傳遞給Activity後,它先将事件分發給子View處理。
- 如果經過子View層層傳遞或處理後,該事件被消費了(即傳回了TRUE),則Activity的分發方法也傳回TRUE,同樣也表示該事件已經被消費了。
- 如果經過子View層層傳遞或處理後,該事件沒有被消費(即傳回了FALSE),則Activity的分發方法就不會傳回TRUE了,而是調用onTouchEvent()去處理,看其實際的處理情況。
- 如果onTouchEvent消費了事件,那依然能傳回TRUE(表示已消費事件),這個TRUE作為dispatchTouchEvent的傳回值,讓調用它的對象知道該Activity已經消費了事件。
- 如果onTouchEvent沒有消費該事件,那就傳回FALSE(表示未消費事件),這個FALSE作為dispatchTouchEvent的傳回值,讓調用它的對象知道該Activity沒有消費事件,需要繼續處理。
ViewGroup的dispatchTouchEvent方法
// ViewGroup中該方法的核心部分僞代碼
public boolean dispatchTouchEvent(MotionEvent ev) {
if (!onInterceptTouchEvent(ev)) {
return child.dispatchTouchEvent(ev); //不攔截,則傳給子View進行分發處理
} else {
return onTouchEvent(ev); //攔截事件,交由自身對象的onTouchEvent方法處理
}
}
ViewGroup的該方法與Activity的類似,隻是新添了一個onInterceptTouchEvent方法。當事件傳入時,首先會調用onInterceptTouchEvent。
- 如果該方法傳回了FALSE(表示不攔截),則交給子View去調用dispatchTouchEvent()方法
- 如果該方法傳回了TRUE(表示攔截),則直接交給該ViewGroup對象的onTouchEvent(ev)方法處理,具體是否能處理以onTouchEvent()的實際情況為準。
實際上,在onInterceptTouchEvent傳回TURE表示攔截時,實際調用的是super.dispatchTouchEvent方法,即View的該方法,進而由該方法調用onTouchEvent.
View的dispatchTouchEvent方法
// View中該方法的核心部分僞代碼
public boolean dispatchTouchEvent(MotionEvent ev) {
//如果該對象的監聽成員變量不為空,則會調用其onTouch方法,
if (mOnTouchListener != null && mOnTouchListener.onTouch(this, event)) {
return true; //若onTouch方法傳回TRUE,則表示消費了該事件,則dispachtouTouchEvent傳回TRUE,讓其調用者知道該事件已被消費。
}
return onTouchEvent(ev); //若監聽成員為空或onTouch沒有消費該事件,則調用對象自身的onTouchEvent方法處理。
}
從該方法的核心邏輯中可以看到,事件傳遞進來後,首先會對mOnTouchListener判空,如果之前Set了Listener,則會調用其onTouch方法。
- 若onTouch方法傳回TRUE,則dispatchTouchEvent也會傳回TRUE,表示消費該事件。
- 若onTouch方法傳回FALSE,或者mOnTouchListener本來就是空,則調用自身的onTouchEvent()來處理,是否消費事件,可以由其傳回值判斷。
實際上,在View的onTouchEvent方法中,如果設定了onClickListener監聽對象,則會調用其onClick方法。
在同時設定了onTouchListener與onClickListener對象的情況下,正是由于View的dispacthTouchEvent方法會先調用mOnTouchListener的onTouch,才會調用onTouchEvent方法,是以onTouchListener對象的onTouch方法是優先于onClickListener對象的onClick方法調用的。這裡隻簡單描述結論,具體源碼請檢視本文對應的進階篇内容。
小節:dispatchTouchEvent方法
回顧上面Activity、ViewGroup和View中的dispatchTouchEvent方法,它們大體都可以分為兩部分,前一部分是交由子View的dispatchTouchEvent方法或onTouch方法進行處理,後一部分是交給自身的onTouchEvent方法處理。這樣了解的話,就非常便于記憶了。
為了便于記憶和了解,可以将各元件的dispatchTouchEvent方法分為兩部分:
- 子View的dispatchTouchEvent 或 onTouch方法
- 自身的onTouchEvent方法
這個結構有點類似于遞歸的過程,就是元件的dispatchTouchEvent會自用子元件的同名方法,子元件一樣會調用子子元件的同名方法,直到遞歸到底,然後在從遞歸底部傳回上層,直到傳回到最上層,整個過程結束。或者在這個過程中,事件傳遞到某個子View,該子View決定處理該事件,則事件交給其自身的onTouchEvent方法處理,如果onTouchEvent方法處理不了,再交由父元件的同名方法處理,直到向上傳遞到頂層結束。
于是,就有了很多教程裡的U型圖。
從U型圖中可以發現,其實安卓事件分發的主體思路非常簡單,即由父元件不斷向子元件分發,若子元件能夠處理,則立刻傳回。若子元件都不處理,那傳遞到底層的子元件,再傳回回來。這個過程類似上面說的遞歸的過程。
這裡對這個U型圖做一下說明,先看圖中左上角,事件傳到Activity,首先調用其dispatchTouchEvent方法,其會傳遞給子View處理,該子View(在圖中是ViewGroup)會調用其dispatchTouchEvent方法,如果該方法被覆寫直接傳回TRUE,則立即傳回Activity,表示已經消費事件。如果該方法沒有被覆寫或調用了super的同名方法,則會調用onInterceptTouchEvent方法,如果該方法傳回TRUE攔截事件,則交給自身的onTouchEvent處理,如果該方法傳回FALSE不攔截,則繼續傳給子子View(圖中是View)的dispatchTouchEvent方法處理。此時,再看看這個U型圖,該遞歸調用已經到底了,若在該方法中的onTouchListener方法不處理,則調用自身的onTouchEvent處理。若還是處理不了,則從遞歸底部向上傳回,依次調用ViewGroup的、Activity的onTouchEvent方法。
實際上,用這個U型圖來描述安卓的事件分發機制并不一定準确,因為同一對象的dispatchTouchEvent方法實際是包含了另外幾個方法的(Activity與View隻包含onTouchEvent),但是在這個圖中,卻是将幾個方法分别畫在不同的框中。是以通過該U型圖來了解事件分發機智是不準确的。但是對于部分讀者可能會有所幫助。要準确了解事件調用機制,還是應該回到上面,檢視三個核心方法的核心邏輯,就能夠準确了解。
強調說明,安卓事件分發的‘向上’與‘向下‘傳播,不要與面向對象程式語言中基類與子類關系,或子類向上調用父類方法等概念搞混淆。對于安卓事件分發的‘向上’與‘向下‘傳播,這裡的上與下,是指在’遞歸‘調用過程中的上與下(也展現到U型圖裡的上與下)。這個概念,展現到布局中,就是外與内。即這裡所說的事件’向下‘傳播,等同于在布局上,由外向内傳播,而’向上’傳播,等同于在布局上,由内向外傳播。
在面向對象程式語言中,對于子類覆寫父類方法,或子類調用父類方法,這些‘上’與‘下’的關系,在布局層面上并沒有跨越布局層次,不要與事件傳播的方向概念相混淆。
攔截方法onInterceptTouchEvent
該方法是ViewGroup類對象所獨有的,用于對事件進行提前攔截。在一般情況下,該方法是預設傳回FALSE的,即不攔截。
如果自定義的ViewGroup希望攔截事件,不希望事件繼續往子View傳播,可以覆寫該方法,傳回TRUE,即可阻止向下的傳播過程。
實際上,從上面的核心邏輯的僞代碼中可以看出,在ViewGroup調用dispatchTouchEvent後,肯定會調用該方法,根據該方法的傳回值來确定如何處理。若該方法傳回True,則會将事件攔截掉,就給自身的onTouchEvent處理。如果傳回False,則繼續傳遞給child執行分發流程。
處理方法onTouchEvent
該方法主要對事件進行處理,若傳回True表示已經處理了事件,若傳回False則表示沒有對事件進行處理,需要繼續傳遞事件。一般情況下,預設為FALSE。在View的onTouchEvent方法中,如果設定了onClickListener監聽對象,則會調用其onClick方法。
6. 總結
本文在介紹了事件分發基本概念的基礎上,介紹了負責參與事件分發的核心方法,包括dispatchTouchEvent()、onInterceptTouchEvent與onTouchEvent方法。通過僞代碼的形式介紹了這些方法的核心邏輯,重點分析了在Activity、ViewGroup與View中的dispatchTouchEvent方法。它們三者中的該方法結構類似,都是先調用子View的同名方法或者listener方法,然後再調用自身的onTouchEvent方法。
這些方法在調用關系中展現了一個類似‘遞歸’的調用過程,通過dispatchTouchEvent将事件傳遞下去,又通過onTouchEvent将事件傳遞上來。中間的這一過程可以通過讓onInterceptTouchEvent方法(對于ViewGroup),或者另外的負責分發的方法傳回TRUE,均可以提前終止這一類似’遞歸‘的調用過程,進而讓事件的處理符合我們的預期。
若有錯漏,煩請斧正。轉載請注明出處。
- 作者:程式引力 | 謝一 (Evan Xie)
- 郵箱:[email protected]
若喜歡本篇文章,請點贊或關注:程式引力