作為Android最重要的機制之一,事件分發(fā)一直是一個老生常談的話題,那么我們今天就來仔細(xì)研究一下Android中的事件分發(fā)機制。
本文的要點如下:
- 事件分發(fā)概述
- 事件分發(fā)的流程
- Activity
- ViewGroup
- View
- 總體流程
- 一些問題
- 總結(jié)
事件分發(fā)概述
說到事件分發(fā),那么我們就一定要明確,一個問題:事件分發(fā)的對象是誰?
從名字也能看出來,當(dāng)然是事件咯。沒錯,當(dāng)用戶觸摸屏幕時(View或ViewGroup派生的控件),將產(chǎn)生點擊事件(Touch事件)。Touch事件相關(guān)細(xì)節(jié)(發(fā)生觸摸的位置(X,Y)、時間、歷史記錄、手勢動作等)被封裝成MotionEvent對象。
那么,Touch事件有幾種呢?
主要發(fā)生的Touch事件有如下四種:
- MotionEvent.ACTION_DOWN:按下View(所有事件的開始)
- MotionEvent.ACTION_MOVE:滑動View
- MotionEvent.ACTION_UP:抬起View(與DOWN對應(yīng))
- MotionEvent.ACTION_CANCEL:非人為原因結(jié)束本次事件
其中前三種是正常情況下一個Touch事件列所包含的步驟。
事件列:從手指接觸屏幕至手指離開屏幕,這個過程產(chǎn)生的一系列事件。任何事件列都是以DOWN事件開始,UP事件結(jié)束,中間有無數(shù)的MOVE事件。
明白了事件是什么,我們再來看看事件分發(fā)。將點擊事件(MotionEvent)向某個View進(jìn)行傳遞并最終得到處理的過程即為事件分發(fā)。那么事件都能發(fā)給誰呢?
對于View,ViewGroup和Activity都能處理Touch事件,它們之間處理的先后順序和方法有所不同。一個點擊事件產(chǎn)生后,傳遞順序大致為:Activity(Window)-> ViewGroup -> View。
事件分發(fā)的流程
對事件分發(fā)有了一個感性的認(rèn)知后,我們來仔細(xì)的研究一下事件分發(fā)的流程。
事件分發(fā)過程主要涉及到dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()這三個方法。
首先是Activity
當(dāng)手指觸摸到屏幕時,屏幕硬件會獲取到觸摸事件,從底層產(chǎn)生中斷上報。再通過native層調(diào)用Java層InputEventReceiver中的dispatchInputEvent方法。經(jīng)過層層調(diào)用,最終交由Activity的dispatchTouchEvent方法來處理。
好我們具體來看一看這個方法:
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
//onUserInteraction為空方法,每當(dāng)Key,Touch,Trackball事件分發(fā)到當(dāng)前Activity就會被調(diào)用。
//如果你想當(dāng)你的Activity在運行的時候,能夠得知用戶正在與你的設(shè)備交互,你可以override該方法。
onUserInteraction();
}
//若getWindow().superDispatchTouchEvent(ev)的返回true
//則Activity.dispatchTouchEvent()就返回true,則方法結(jié)束
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
//沒有返回則繼續(xù)往下調(diào)用Activity.onTouchEvent
return onTouchEvent(ev);
}
其中關(guān)鍵是getWindow().superDispatchTouchEvent(ev)方法,getWindow() 方法會獲取Window類的對象,而Window類是抽象類,其唯一實現(xiàn)類是PhoneWindow類。來看具體實現(xiàn):
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
// mDecor 為頂層View(DecorView)的實例對象
}
DecorView類是PhoneWindow類的一個內(nèi)部類,DecorView繼承自FrameLayout,是所有界面的父類,我們又知道FrameLayout是ViewGroup的子類,因此DecorView的間接父類為ViewGroup。
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
// 調(diào)用ViewGroup的dispatchTouchEvent()
}
暫時不管ViewGroup的dispatchTouchEvent(),先來看Activity本身的onTouchEvent()。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
可以看到,里面邏輯很簡單,就是用shouldCloseOnTouch方法對事件進(jìn)行判斷,根據(jù)返回值決定是否消費事件。
舉個例子:在開發(fā)過程中,我們有時會通過Activity實現(xiàn)彈窗效果,實現(xiàn)很簡單,在AndroidMenifest.xml中將對應(yīng)的Activity增加android:theme="@android:style/Theme.Dialog"屬性即可(也可以自定義彈窗的樣式)。對于彈窗,點擊其周圍的空白區(qū)域,正常情況下彈窗都會自動消失。就是Activity中的onTouchEvent產(chǎn)生的作用。即shouldCloseOnTouch判斷觸摸點在邊界外,那么就會finish(),因此對話框Activity就會關(guān)閉。
接著來看ViewGroup
從上面Activity事件分發(fā)機制可知,ViewGroup事件分發(fā)機制從dispatchTouchEvent()開始。看過源碼的都知道ViewGroup的dispatchTouchEvent有200多行,我們在這里就不貼源碼了,主要看看其工作流程。
整體的工作流程可以簡化為以下三步:
- 判斷自身是否需要(詢問 onInterceptTouchEvent 是否攔截),如果需要,調(diào)用自己的 onTouchEvent。
- 自身不需要或者不確定,則詢問 ChildView ,一般來說是調(diào)用手指觸摸位置的 ChildView。
- 如果子 ChildView 不需要則調(diào)用自身的 onTouchEvent。
接下來我們看看每一步的具體工作:
ViewGroup每次事件分發(fā)時,都先判斷disallowIntercept是否為true,然后調(diào)用onInterceptTouchEvent()詢問是否攔截事件:
if (disallowIntercept || !onInterceptTouchEvent(ev)) { ...... }disallowIntercept為false則代表禁用事件攔截功能,可以通過requestDisallowInterceptTouchEvent()修改。
onInterceptTouchEvent()中返回false代表不攔截事件,返回true則會攔截事件,即事件不會向下層view傳遞。
如果要向下層傳遞,那么問題就來了,該把事件傳遞給哪個子View呢?
for (int i = count - 1; i >= 0; i--) {
final View child = children[i];
......判斷event的坐標(biāo)是否包含在child中、子view是否可以處理touch事件等
}
可以看到,源碼中其實是遍歷了所有的子View,根據(jù)坐標(biāo)從而找到當(dāng)前被點擊的View。那么就出現(xiàn)了一個問題,如果兩個子View有重疊,那么應(yīng)該給誰呢?
這個問題的答案就在上面的源碼中,int i = count - 1,可以發(fā)現(xiàn),遍歷是從后往前的,即后面的View如果能處理就不會發(fā)給前面的View。那么View的順序是怎么定的呢?是加載的先后。為什么要先給后加載的View呢?因為View繪制時,后加載的View會覆蓋掉先加載的View,顯示在最上面的是最后加載的,因此當(dāng) ChildView 重疊時,一般會分配給顯示在最上面的 ChildView。(當(dāng)然了,前提是最上面的ChildView可以處理touch事件)
如果所有的ChildView都不接收事件或者是覆寫的onInterceptTouchEvent()返回了true(即攔截事件),則會調(diào)用:
super.dispatchTouchEvent(ev);
我們知道,ViewGroup的父類為View,那么就會調(diào)用View類的dispatchTouchEvent()(也就是把此ViewGroup當(dāng)作一個View來看,調(diào)用其dispatchTouchEvent方法)。
最終事件都來到了View類
同樣,從上面ViewGroup事件分發(fā)機制可以知道,View事件分發(fā)機制是從dispatchTouchEvent()開始的。
那么問題就來了,ViewGroup 有 dispatchTouchEvent 也就算了,畢竟人家有一堆 ChildView 需要管理,但為啥 View 也有?
其實很簡單,我們都知道 View 可以注冊很多事件監(jiān)聽器,單擊事件(onClick)、長按事件(onLongClick)、觸摸事件(onTouch),并且View自身也有 onTouchEvent 方法,到底該由哪個監(jiān)聽器來響應(yīng)呢?這就是dispatchTouchEvent的工作了。
那么問題就又來了,dispatchTouchEvent中View 事件相關(guān)的各個方法調(diào)用順序是怎樣的?我們可以先拋開源碼不看,思考一下:
單擊事件(onClickListener) 需要兩個兩個事件(ACTION_DOWN 和 ACTION_UP )才能觸發(fā),如果先分配給onClick判斷,等它判斷完,用戶手指已經(jīng)離開屏幕,黃花菜都涼了,肯定會使得 View 無法響應(yīng)其他事件,所以應(yīng)該最后調(diào)用。
長按事件(onLongClickListener) 也是需要長時間等待才能出結(jié)果,肯定不能排到前面,但因為不需要ACTION_UP,應(yīng)該排在 onClick 前面。(onLongClickListener > onClickListener)。
觸摸事件(onTouchListener) 如果用戶注冊了觸摸事件,說明用戶要自己處理觸摸事件了,這個應(yīng)該排在最前面。
View自身處理(onTouchEvent) 算是提供了一種默認(rèn)的觸摸事件的處理方式,如果用戶已經(jīng)設(shè)置了處理方式,那也就不需要了,所以應(yīng)該排在 onTouchListener 后面。
再來看看源碼:
public boolean dispatchTouchEvent(MotionEvent event) {
if (mOnTouchListener != null && (mViewFlags & ENABLED_MASK) == ENABLED &&
mOnTouchListener.onTouch(this, event)) {
return true;
}
return onTouchEvent(event);
}
似乎和我們想的不一樣,OnClick 和 OnLongClick不見了。其實實際的原理是一樣的,只不過OnClick 和 OnLongClick 的處理被放到了onTouchEvent中。
再來看看onTouchEvent:
public boolean onTouchEvent(MotionEvent event) {
if (((viewFlags & CLICKABLE) == CLICKABLE ||
(viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)) {
switch{
//......用case判斷具體該用哪個處理方式
}
// 若該控件可點擊,就一定返回true
return true;
}
// 若該控件不可點擊,就一定返回false
return false;
}
其實關(guān)鍵不是switch判斷,而是return true和return false??梢钥闯觯?strong>只要控件可以點擊,那么就一定會return true,即一定會消費事件,這個返回值和switch里面的判斷是一點關(guān)系也沒有的,也就是說:
- 不論 View 自身是否注冊點擊事件,只要 View 是可點擊的就會消費事件。
- 事件是否被消費由返回值決定,true 表示消費,false 表示不消費,與是否使用了事件無關(guān)。
總體流程
最后我們用一張圖來回顧一下事件分發(fā)的整體流程:

一些問題
一個事件列應(yīng)該被同一View消費
顯然,View中onClick事件需要同時接收到ACTION_DOWN和ACTION_UP才能觸發(fā),如果分配給了不同的 View,那么onClick 將無法被正確觸發(fā)。
因此,安卓為了保證一個事件列都是被一個 View 消費的,對第一次的事件( ACTION_DOWN )進(jìn)行了特殊判斷,View 只有消費了 ACTION_DOWN 事件,才能接收到后續(xù)的事件(可點擊控件會默認(rèn)消費所有事件),并且會將后續(xù)所有事件傳遞過來,不會再傳遞給其他 View,除非上層 View 進(jìn)行了攔截。
如果上層 View 攔截了當(dāng)前正在處理的事件,會收到一個 ACTION_CANCEL,表示當(dāng)前事件已經(jīng)結(jié)束,后續(xù)事件不會再傳遞過來。
View的滑動沖突的解決
常見的滑動沖突有兩種:
- 外層與內(nèi)層滑動方向不一致,外層ViewGroup是可以橫向滑動的,內(nèi)層View是可以豎向滑動的(類似ViewPager,每個頁面里面是ListView)
- 外層與內(nèi)層滑動方向一致,外層ViewGroup是可以豎向滑動的,內(nèi)層View同樣也是豎向滑動的(類似ScrollView包裹ListView)
這些情況下,就會產(chǎn)生滑動沖突,即到底應(yīng)該執(zhí)行哪個的滑動方法呢?
當(dāng)然,還可以更多層的嵌套,不過原理都是一樣的,一層一層處理即可。
(eg:UC瀏覽器、新浪微博等)
這里可能有些人會說,ViewPager帶ListView并沒有出現(xiàn)滑動沖突啊,我用過都沒問題啊。那是因為ViewPager已經(jīng)為我們處理了滑動沖突!如果我們自己定義一個水平滑動的ViewGroup內(nèi)部再使用ListView,那么是一定需要處理滑動沖突的。
那么該如何解決呢?
針對上面第一種場景,由于外部與內(nèi)部的滑動方向不一致,那么我們可以根據(jù)當(dāng)前滑動方向,水平還是垂直來判斷這個事件到底該交給誰來處理。至于如何獲得滑動方向,我們可以得到滑動過程中的兩個點的坐標(biāo)。一般情況下根據(jù)水平和豎直方向滑動的距離差就可以判斷方向,當(dāng)然也可以根據(jù)滑動路徑的斜率、或者水平和豎直方向滑動速度差來判斷。
第二種場景,由于外部與內(nèi)部的滑動方向一致,就只能根據(jù)業(yè)務(wù)邏輯來判斷了。以微博熱搜為例,當(dāng)熱搜標(biāo)簽欄滾動到頂部時,熱搜微博才能滾動。
講了半天都是理論,那么實際怎么實現(xiàn)呢?
常用的也就兩種方法:
外部攔截法:指點擊事件都先經(jīng)過父容器的攔截處理,如果父容器需要此事件就攔截,否則就不攔截。具體方法:需要重寫父容器的onInterceptTouchEvent方法,在內(nèi)部做出相應(yīng)的攔截。
內(nèi)部攔截法:指父容器不攔截任何事件,而將所有的事件都傳遞給子容器,如果子容器需要此事件就直接消耗,否則就交由父容器進(jìn)行處理。具體方法:需要配合requestDisallowInterceptTouchEvent方法
總結(jié)
1. 事件分發(fā)原理: 責(zé)任鏈模式,事件層層傳遞,直到被消費。
2. Touch事件的傳遞順序大致為Activity(Window)-> ViewGroup -> View。
3. 事件分發(fā)過程主要涉及到dispatchTouchEvent() 、onInterceptTouchEvent()和onTouchEvent()這三個方法,其中dispatchTouchEvent 主要用于分發(fā)事件,具體由onTouchEvent處理,onInterceptTouchEvent則是負(fù)責(zé)在ViewGroup中攔截事件。
4. ViewGroup 中有多個ChildView時,將事件分配給包含點擊位置且能夠點擊的最后加載的ChildView。
5. 一個事件列應(yīng)該被同一View消費
6. 如果當(dāng)前正在處理的事件被上層 View 攔截,會收到一個 ACTION_CANCEL,后續(xù)事件不會再傳遞過來。
7. 滑動沖突可以用外部攔截法或內(nèi)部攔截法解決