事件分發(fā)機(jī)制,是Android提供的一套完善的對(duì)觸摸事件進(jìn)行處理的機(jī)制,熟悉整個(gè)事件分發(fā)流程很有必要,因?yàn)樗彩茿ndroid中常見的滑動(dòng)沖突問題解決的理論基礎(chǔ)。這幾天閱讀了《Android開發(fā)藝術(shù)探索》等書籍,總結(jié)如下。
一、引入
二、事件分發(fā)機(jī)制
1.概述
2.詳細(xì)
三、源碼解析
1.ViewGroup事件分發(fā)
2.View事件分發(fā)
四、滑動(dòng)沖突解決
五、總結(jié)
一、引入
在介紹Android事件分發(fā)機(jī)制之前,我們先看生活中的一個(gè)例子。公司里有三個(gè)角色,老板,項(xiàng)目經(jīng)理,程序員。有一天老板接到一個(gè)任務(wù),他將任務(wù)分配給項(xiàng)目經(jīng)理完成,項(xiàng)目經(jīng)理又把任務(wù)分給程序員。程序員完成任務(wù)后,告訴項(xiàng)目經(jīng)理任務(wù)完成了,項(xiàng)目經(jīng)理再向老板報(bào)告任務(wù)完成了。從老板接到任務(wù),到老板最終去交付任務(wù),這是個(gè)完整的過程。
在這個(gè)過程中,可能會(huì)有其它情況。假如在一開始老板接到任務(wù)時(shí),決定自己完成,不需要把任務(wù)往下分配,那么老板就自己做,項(xiàng)目經(jīng)理和程序員就沒事。同樣,如果項(xiàng)目經(jīng)理決定自己去做,那么就沒有程序員的事。上面的這個(gè)例子其實(shí)就是任務(wù)在老板、項(xiàng)目經(jīng)理和程序員這三個(gè)角色間的傳遞過程,Android中屏幕上的觸摸事件就相當(dāng)于這個(gè)任務(wù),事件分發(fā)就類似于這個(gè)傳遞過程。
二、事件分發(fā)機(jī)制
我們知道,Android的界面可能是由多個(gè)視圖層層嵌套構(gòu)成,一個(gè)ViewGroup視圖組合中可以包含其它的ViewGroup以及View,當(dāng)一個(gè)觸摸事件發(fā)生時(shí),系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的View,由它來完成處理。從事件發(fā)生,到傳遞給具體的View去完成,這個(gè)傳遞的過程就是View的事件分發(fā)。

概述
在事件分發(fā)機(jī)制中,涉及到的幾個(gè)關(guān)鍵部分分別是:TouchEvent(觸摸事件)、ViewGroup(視圖組合)、View(視圖)。下面先對(duì)這幾個(gè)部分做個(gè)介紹。
- TouchEvent(觸摸事件)
觸摸事件就是觸摸屏幕產(chǎn)生的動(dòng)作事件,比如常見的手指按下,移動(dòng),抬起等等,Android為我們提供了一個(gè)專門的MotionEvent類,它包含了發(fā)生的動(dòng)作事件以及相關(guān)坐標(biāo)信息,利用MotionEvent,我們可以處理很多與動(dòng)作相關(guān)的工作。
- View
我們經(jīng)常提到View事件分發(fā)機(jī)制,其實(shí)這里指的是View以及ViewGroup,我們知道View是Android中所有控件的基類,而ViewGroup翻譯為視圖組合,它是繼承自View的,可以包含子控件。我們?cè)诮酉聛淼挠懻撝?,?huì)把ViewGroup和View分開討論。
詳解
上面介紹了一些事件分發(fā)的基本概念,下面對(duì)分發(fā)流程有個(gè)總體的把握。Android中事件分發(fā)機(jī)制主要涉及到三個(gè)重要方法,如下:
- dispatchTouchEvent ( MotionEvent event ) 事件分發(fā)
- onInterceptTouchEvent 決定是否攔截事件
- onTouchEvent 處理事件
上面三個(gè)方法之間的關(guān)系大概如下,當(dāng)事件傳遞到某個(gè)View時(shí),先執(zhí)行dispatchTouchEvent方法進(jìn)行事件分發(fā),在這個(gè)方法內(nèi)會(huì)調(diào)用方法onInterceptTouchEvent方法來決定是否攔截,如果返回true表示攔截,則調(diào)用onTouchEvent進(jìn)行事件處理,否則繼續(xù)往下傳遞,執(zhí)行子View的dispatchTouchEvent方法。
需要注意一點(diǎn),View沒有onInterceptTouchEvent方法,一旦有事件傳遞給它,那么它的onTouchEvent方法就會(huì)被調(diào)用。ViewGroup默認(rèn)不攔截任何事件,因?yàn)閺脑创a中可以看到ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false.
我們知道,四大組件中,Activity通常提供界面用于交互,我們會(huì)通過setContentView來設(shè)置界面布局,一般如果我們不希望布局頂部出現(xiàn)一個(gè)標(biāo)題欄,我們可能會(huì)調(diào)用requestWindowFeature(Window.FEATURE_NO_TITLE);方法,這里我們簡(jiǎn)單了解一下Android的界面架構(gòu)。
界面上一個(gè)點(diǎn)擊事件發(fā)生時(shí),它最先被傳遞的是給當(dāng)前的Activity,由Activity的dispatchTouchEvent來進(jìn)行事件分發(fā),而Activity內(nèi)部其實(shí)是包含一個(gè)Window的,這個(gè)抽象Window的實(shí)現(xiàn)是PhoneWindow,Activity把事件傳遞給PhoneWindow,PhoneWindow里又包含DecorView,PhoneWindow繼續(xù)把事件傳遞給DecorView,DecorWindow里包含有我們?cè)O(shè)置的布局,DecorView繼承自FrameLayout,事件最終傳遞給我們?cè)O(shè)置的布局,一般來說設(shè)置的布局是一個(gè)ViewGroup。所以,觸摸事件最后就是在ViewGroup中的分發(fā)過程。
三、源碼解析
前面我們已經(jīng)提到,事件分發(fā)機(jī)制其實(shí)是觸摸事件在ViewGroup和View兩種情況下的分發(fā)過程,下面我們結(jié)合源碼來分析,因?yàn)閂iew的過程相對(duì)來說較為簡(jiǎn)單,我們先看ViewGroup事件分發(fā)。
ViewGroup事件分發(fā)
ViewGroup事件分發(fā)過程簡(jiǎn)述主要如下,事件到達(dá)ViewGroup后會(huì)調(diào)用方法dispatchTouchEvent,在其中會(huì)調(diào)用onInterceptTouchEvent進(jìn)行判斷是否攔截,如果返回true表示攔截則事件由ViewGroup處理,如果返回false不攔截,則事件會(huì)傳遞給子View,子View的dispatchTouchEvent會(huì)被調(diào)用。默認(rèn)情況下,onInterceptTouchEvent返回false.
下面我們看下源碼。
1、首先是dispatchTouchEvent方法里判斷是否攔截。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) {
//默認(rèn)是false 允許攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action);
} else {
intercepted = false;
}
}else {
intercepted = true;
}
這里可以看到,ViewGroup會(huì)在兩種情況下進(jìn)行是否攔截的判斷,第一種是發(fā)生ACTION_DOWN事件,第二種是mFirstTouchTarget != null。第二種情況是指,ViewGroup是否不攔截事件并把事件交由子View處理,如果是,那么mFirstTouchTarget != null就成立。
進(jìn)行判斷時(shí),會(huì)看變量disallowIntercept的值,這個(gè)值默認(rèn)是false不允許攔截,所以!disallowIntercept為true,然后調(diào)用onInterceptTouchEvent為false,即不攔截。有種情況,如果ACTION_DOWN判斷時(shí)被ViewGroup攔截,那么mFirstTouchTarget!=null就不成立,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP來臨時(shí),不進(jìn)行判斷,直接攔截。
這里有兩條結(jié)論,某個(gè)View一旦決定攔截一個(gè)事件后,那么系統(tǒng)會(huì)把同一個(gè)事件序列的其它方法都交給這個(gè)View處理。某個(gè)View如果不消耗ACTION_DOWN事件交給了子View處理,那么同一個(gè)事件序列的其它方法都不會(huì)交給它處理。
2、當(dāng)ViewGroup不攔截事件,事件分發(fā)給子View處理。
//子View
final View[] children = mChildren;
//循環(huán)遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
... ...
//如果子View接收不到事件 或者 不在播動(dòng)畫 就不分發(fā)
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
//分發(fā)事件給子View
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//調(diào)用子元素的dispatchTouchEvent
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
ev.setTargetAccessibilityFocus(false);
}
可以看到大概流程如下,循環(huán)遍歷子View,判斷子元素能否接收到點(diǎn)擊事件。能否接收到事件主要由兩點(diǎn)衡量,一是是否在播放動(dòng)畫,二是點(diǎn)擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)。如果子元素滿足條件,則事件傳遞給子View處理。dispatchTransformedTouchEvent方法里調(diào)用了子View的dispatchTouchEvent方法。
如果子View的dispatchTouchEvent返回true,那么終止子元素的遍歷,如果返回false,則繼續(xù)分發(fā)給下個(gè)子元素。如果遍歷所有的子元素后事件都沒處理,那么ViewGroup就自己處理事件。
**綜上,觸摸事件傳遞到ViewGroup時(shí),會(huì)執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā),如果事件是Down類型(或者同一事件序列沒被攔截已經(jīng)交由子元素處理),那么就調(diào)用方法onInterceptTouchEvent進(jìn)行攔截判斷,默認(rèn)情況下不會(huì)攔截事件。ViewGroup不攔截的話,那么就會(huì)遍歷它的子View,判斷能否接收到事件,如果接收到那么就調(diào)用子View的dispatchTouchEvent方法繼續(xù)進(jìn)行分發(fā)。如果遍歷子View后都沒處理事件,那么ViewGroup自己處理事件。
**
View事件分發(fā)
View的事件分發(fā)比ViewGroup簡(jiǎn)單,因?yàn)閂iew不包含子View,所以它只能自己處理事件。
下面是它的dispatchTouchEvent方法內(nèi)的部分源碼。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
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;
}
}
...
return result;
}
View對(duì)點(diǎn)擊事件的處理,首先會(huì)判斷有沒有設(shè)置OnTouchListener,因?yàn)镺nTouchListener的優(yōu)先級(jí)高于onTouchEvent。
onTouchEvent中,即使View處于不可用狀態(tài),照樣會(huì)消耗點(diǎn)擊事件。下面代碼可以看出來。
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return (((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
}
A disabled view that is clickable still consumes the touch events, it just doesn't respond to them,一個(gè)不可用的View仍然可以消耗事件,只是不做任何響應(yīng)。
onTouchEvent中對(duì)點(diǎn)擊事件的具體處理流程大概如下,只要View的CLICKABLE和LONG_CLICKABLE有一個(gè)為true,那么它就會(huì)消耗事件,返回true??偟膩碚f,View的可不可用不影響是否消耗事件,只要clickable或者longClickable有一個(gè)為true,那么它就會(huì)消耗事件。
**綜上,觸摸事件傳遞到View時(shí),會(huì)執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā),這里會(huì)判斷有沒有設(shè)置OnTouchListener,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不會(huì)被調(diào)用。View的onTouchEvent默認(rèn)都會(huì)消耗事件,除非它是不可點(diǎn)擊的(clickable和longClickable同時(shí)為false),而View的enable屬性并不影響onTouchEvent的返回值。
**
四、滑動(dòng)沖突解決
上面主要主要介紹了View的事件分發(fā)機(jī)制的整個(gè)過程,在平常的開發(fā)中,在熟悉整個(gè)分發(fā)過程后,滑動(dòng)沖突問題應(yīng)該就不再是難題了。下面主要以一個(gè)典型的例子,介紹下滑動(dòng)沖突問題的解決。
滑動(dòng)沖突的產(chǎn)生主要是因?yàn)榻缑嬷袃?nèi)外兩層都可以滑動(dòng),比如一個(gè)界面外部可以左右滑動(dòng),內(nèi)部可以上下滑動(dòng)。這時(shí)就可以采取外部攔截法,前面我們提到分發(fā)過程中方法onInterceptTouchEvent主要是用于判斷是否攔截,那么外部攔截中我們可以重寫父容器的onInterceptTouchEvent方法,根據(jù)需要決定是否攔截。
public boolean onInterceptHoverEvent(MotionEvent event) {
boolean intercepted = false;
int x = (int) event.getX();
int y = (int) event.getY();
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
break;
case MotionEvent.ACTION_MOVE:
if(父容器需要當(dāng)前點(diǎn)擊事件){
intercepted = true;
}else {
intercepted = false;
}
break;
case MotionEvent.ACTION_UP:
break;
default:
break;
}
mLastXIntercept = x;
mLastYIntercept = y;
return intercepted;
}
五、總結(jié)
到這里關(guān)于Android中View的事件分發(fā)機(jī)制就介紹的差不多了,歡迎指正批評(píng)。