事件分發(fā)機制枪蘑,是Android提供的一套完善的對觸摸事件進(jìn)行處理的機制,熟悉整個事件分發(fā)流程很有必要岖免,因為它也是Android中常見的滑動沖突問題解決的理論基礎(chǔ)岳颇。這幾天閱讀了《Android開發(fā)藝術(shù)探索》等書籍,總結(jié)如下颅湘。
一话侧、引入
二、事件分發(fā)機制
1.概述
2.詳細(xì)
三闯参、源碼解析
1.ViewGroup事件分發(fā)
2.View事件分發(fā)
四瞻鹏、滑動沖突解決
五、總結(jié)
一鹿寨、引入
在介紹Android事件分發(fā)機制之前新博,我們先看生活中的一個例子。公司里有三個角色脚草,老板赫悄,項目經(jīng)理,程序員馏慨。有一天老板接到一個任務(wù)埂淮,他將任務(wù)分配給項目經(jīng)理完成,項目經(jīng)理又把任務(wù)分給程序員写隶。程序員完成任務(wù)后倔撞,告訴項目經(jīng)理任務(wù)完成了,項目經(jīng)理再向老板報告任務(wù)完成了樟澜。從老板接到任務(wù)误窖,到老板最終去交付任務(wù),這是個完整的過程秩贰。
在這個過程中霹俺,可能會有其它情況。假如在一開始老板接到任務(wù)時毒费,決定自己完成丙唧,不需要把任務(wù)往下分配,那么老板就自己做觅玻,項目經(jīng)理和程序員就沒事想际。同樣培漏,如果項目經(jīng)理決定自己去做,那么就沒有程序員的事胡本。上面的這個例子其實就是任務(wù)在老板牌柄、項目經(jīng)理和程序員這三個角色間的傳遞過程,Android中屏幕上的觸摸事件就相當(dāng)于這個任務(wù)侧甫,事件分發(fā)就類似于這個傳遞過程珊佣。
二、事件分發(fā)機制
我們知道披粟,Android的界面可能是由多個視圖層層嵌套構(gòu)成咒锻,一個ViewGroup視圖組合中可以包含其它的ViewGroup以及View,當(dāng)一個觸摸事件發(fā)生時守屉,系統(tǒng)需要把這個事件傳遞給一個具體的View惑艇,由它來完成處理。從事件發(fā)生拇泛,到傳遞給具體的View去完成滨巴,這個傳遞的過程就是View的事件分發(fā)。
概述
在事件分發(fā)機制中碰镜,涉及到的幾個關(guān)鍵部分分別是:TouchEvent(觸摸事件)兢卵、ViewGroup(視圖組合)、View(視圖)绪颖。下面先對這幾個部分做個介紹秽荤。
- TouchEvent(觸摸事件)
觸摸事件就是觸摸屏幕產(chǎn)生的動作事件,比如常見的手指按下柠横,移動窃款,抬起等等,Android為我們提供了一個專門的MotionEvent類牍氛,它包含了發(fā)生的動作事件以及相關(guān)坐標(biāo)信息晨继,利用MotionEvent,我們可以處理很多與動作相關(guān)的工作搬俊。
- View
我們經(jīng)常提到View事件分發(fā)機制紊扬,其實這里指的是View以及ViewGroup,我們知道View是Android中所有控件的基類唉擂,而ViewGroup翻譯為視圖組合餐屎,它是繼承自View的,可以包含子控件玩祟。我們在接下來的討論中腹缩,會把ViewGroup和View分開討論。
詳解
上面介紹了一些事件分發(fā)的基本概念,下面對分發(fā)流程有個總體的把握藏鹊。Android中事件分發(fā)機制主要涉及到三個重要方法润讥,如下:
- dispatchTouchEvent ( MotionEvent event ) 事件分發(fā)
- onInterceptTouchEvent 決定是否攔截事件
- onTouchEvent 處理事件
上面三個方法之間的關(guān)系大概如下,當(dāng)事件傳遞到某個View時盘寡,先執(zhí)行dispatchTouchEvent方法進(jìn)行事件分發(fā)楚殿,在這個方法內(nèi)會調(diào)用方法onInterceptTouchEvent方法來決定是否攔截,如果返回true表示攔截宴抚,則調(diào)用onTouchEvent進(jìn)行事件處理勒魔,否則繼續(xù)往下傳遞甫煞,執(zhí)行子View的dispatchTouchEvent方法菇曲。
需要注意一點,View沒有onInterceptTouchEvent方法抚吠,一旦有事件傳遞給它常潮,那么它的onTouchEvent方法就會被調(diào)用。ViewGroup默認(rèn)不攔截任何事件楷力,因為從源碼中可以看到ViewGroup的onInterceptTouchEvent方法默認(rèn)返回false.
我們知道喊式,四大組件中,Activity通常提供界面用于交互萧朝,我們會通過setContentView來設(shè)置界面布局岔留,一般如果我們不希望布局頂部出現(xiàn)一個標(biāo)題欄,我們可能會調(diào)用requestWindowFeature(Window.FEATURE_NO_TITLE);方法检柬,這里我們簡單了解一下Android的界面架構(gòu)献联。
界面上一個點擊事件發(fā)生時,它最先被傳遞的是給當(dāng)前的Activity何址,由Activity的dispatchTouchEvent來進(jìn)行事件分發(fā)里逆,而Activity內(nèi)部其實是包含一個Window的,這個抽象Window的實現(xiàn)是PhoneWindow用爪,Activity把事件傳遞給PhoneWindow原押,PhoneWindow里又包含DecorView,PhoneWindow繼續(xù)把事件傳遞給DecorView偎血,DecorWindow里包含有我們設(shè)置的布局诸衔,DecorView繼承自FrameLayout,事件最終傳遞給我們設(shè)置的布局颇玷,一般來說設(shè)置的布局是一個ViewGroup笨农。所以,觸摸事件最后就是在ViewGroup中的分發(fā)過程亚隙。
三磁餐、源碼解析
前面我們已經(jīng)提到,事件分發(fā)機制其實是觸摸事件在ViewGroup和View兩種情況下的分發(fā)過程,下面我們結(jié)合源碼來分析诊霹,因為View的過程相對來說較為簡單羞延,我們先看ViewGroup事件分發(fā)。
ViewGroup事件分發(fā)
ViewGroup事件分發(fā)過程簡述主要如下脾还,事件到達(dá)ViewGroup后會調(diào)用方法dispatchTouchEvent伴箩,在其中會調(diào)用onInterceptTouchEvent進(jìn)行判斷是否攔截,如果返回true表示攔截則事件由ViewGroup處理鄙漏,如果返回false不攔截嗤谚,則事件會傳遞給子View,子View的dispatchTouchEvent會被調(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會在兩種情況下進(jìn)行是否攔截的判斷籍胯,第一種是發(fā)生ACTION_DOWN事件竟闪,第二種是mFirstTouchTarget != null。第二種情況是指杖狼,ViewGroup是否不攔截事件并把事件交由子View處理炼蛤,如果是,那么mFirstTouchTarget != null就成立蝶涩。
進(jìn)行判斷時理朋,會看變量disallowIntercept的值,這個值默認(rèn)是false不允許攔截子寓,所以!disallowIntercept為true暗挑,然后調(diào)用onInterceptTouchEvent為false,即不攔截斜友。有種情況炸裆,如果ACTION_DOWN判斷時被ViewGroup攔截,那么mFirstTouchTarget!=null就不成立鲜屏,那么同一事件序列中的剩余事件ACTION_MOVE或者ACTION_UP來臨時烹看,不進(jìn)行判斷,直接攔截洛史。
這里有兩條結(jié)論惯殊,某個View一旦決定攔截一個事件后,那么系統(tǒng)會把同一個事件序列的其它方法都交給這個View處理也殖。某個View如果不消耗ACTION_DOWN事件交給了子View處理土思,那么同一個事件序列的其它方法都不會交給它處理务热。
2、當(dāng)ViewGroup不攔截事件己儒,事件分發(fā)給子View處理崎岂。
//子View
final View[] children = mChildren;
//循環(huán)遍歷
for (int i = childrenCount - 1; i >= 0; i--) {
... ...
//如果子View接收不到事件 或者 不在播動畫 就不分發(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闪湾,判斷子元素能否接收到點擊事件冲甘。能否接收到事件主要由兩點衡量,一是是否在播放動畫途样,二是點擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)江醇。如果子元素滿足條件,則事件傳遞給子View處理何暇。dispatchTransformedTouchEvent方法里調(diào)用了子View的dispatchTouchEvent方法陶夜。
如果子View的dispatchTouchEvent返回true,那么終止子元素的遍歷赖晶,如果返回false律适,則繼續(xù)分發(fā)給下個子元素。如果遍歷所有的子元素后事件都沒處理遏插,那么ViewGroup就自己處理事件。
**綜上纠修,觸摸事件傳遞到ViewGroup時胳嘲,會執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā),如果事件是Down類型(或者同一事件序列沒被攔截已經(jīng)交由子元素處理)扣草,那么就調(diào)用方法onInterceptTouchEvent進(jìn)行攔截判斷了牛,默認(rèn)情況下不會攔截事件。ViewGroup不攔截的話辰妙,那么就會遍歷它的子View鹰祸,判斷能否接收到事件,如果接收到那么就調(diào)用子View的dispatchTouchEvent方法繼續(xù)進(jìn)行分發(fā)密浑。如果遍歷子View后都沒處理事件蛙婴,那么ViewGroup自己處理事件。
**
View事件分發(fā)
View的事件分發(fā)比ViewGroup簡單尔破,因為View不包含子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對點擊事件的處理餐济,首先會判斷有沒有設(shè)置OnTouchListener,因為OnTouchListener的優(yōu)先級高于onTouchEvent胆剧。
onTouchEvent中絮姆,即使View處于不可用狀態(tài),照樣會消耗點擊事件。下面代碼可以看出來篙悯。
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冤灾,一個不可用的View仍然可以消耗事件,只是不做任何響應(yīng)辕近。
onTouchEvent中對點擊事件的具體處理流程大概如下韵吨,只要View的CLICKABLE和LONG_CLICKABLE有一個為true,那么它就會消耗事件移宅,返回true归粉。總的來說漏峰,View的可不可用不影響是否消耗事件糠悼,只要clickable或者longClickable有一個為true,那么它就會消耗事件浅乔。
**綜上倔喂,觸摸事件傳遞到View時,會執(zhí)行方法dispatchTouchEvent()進(jìn)行事件分發(fā)靖苇,這里會判斷有沒有設(shè)置OnTouchListener席噩,如果OnTouchListener的onTouch方法返回true,那么onTouchEvent就不會被調(diào)用贤壁。View的onTouchEvent默認(rèn)都會消耗事件悼枢,除非它是不可點擊的(clickable和longClickable同時為false),而View的enable屬性并不影響onTouchEvent的返回值脾拆。
**
四馒索、滑動沖突解決
上面主要主要介紹了View的事件分發(fā)機制的整個過程,在平常的開發(fā)中名船,在熟悉整個分發(fā)過程后绰上,滑動沖突問題應(yīng)該就不再是難題了。下面主要以一個典型的例子渠驼,介紹下滑動沖突問題的解決蜈块。
滑動沖突的產(chǎn)生主要是因為界面中內(nèi)外兩層都可以滑動,比如一個界面外部可以左右滑動渴邦,內(nèi)部可以上下滑動疯趟。這時就可以采取外部攔截法,前面我們提到分發(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)前點擊事件){
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ā)機制就介紹的差不多了盹舞,歡迎指正批評产镐。