1.介紹
1.MotionEvent
當(dāng)我們點(diǎn)擊屏幕時(shí)界弧,就產(chǎn)生了點(diǎn)擊事件气嫁,這個(gè)點(diǎn)擊事件被封裝成了一個(gè)類(lèi):MotionEvent料睛。當(dāng)手機(jī)接觸屏幕后會(huì)產(chǎn)生一系列的事件府蔗,其中典型的事件類(lèi)型有如下幾種:
- ACTION_DOWN:手指剛接觸屏幕灾票;
- ACTION_MOVE:手指在屏幕上移動(dòng)峡谊;
- ACTION_UP:手指從屏幕上松開(kāi)的一瞬間。
2.事件分發(fā)的3個(gè)重要方法
當(dāng)一個(gè)MotionEvent產(chǎn)生后刊苍,系統(tǒng)需要把這個(gè)事件傳遞給一個(gè)具體的View既们,這個(gè)傳遞過(guò)程就是事件分發(fā)過(guò)程。事件的分發(fā)的過(guò)程由3個(gè)重要的方法來(lái)共同完成:
-
public boolean dispatchTouchEvent(MotionEvent ev)
用來(lái)進(jìn)行事件的分發(fā)正什。如果事件能夠傳遞給當(dāng)前View啥纸,那么此方法一定會(huì)被調(diào)用,返回結(jié)果受當(dāng)前View的TouchEvent和下級(jí)View的dispatchTouchEvent方法影響婴氮,表示是否消耗當(dāng)前事件斯棒。 -
public boolean onInterceptTouchEvent(MotionEvent ev)
在dispatchTouchEvent方法中調(diào)用,用來(lái)判斷是否攔截某個(gè)事件主经。如果當(dāng)前View攔截了某個(gè)事件荣暮,那么在同一個(gè)事件序列當(dāng)中,此方法不會(huì)被再次調(diào)用罩驻,返回結(jié)果表示是否攔截當(dāng)前事件穗酥。需要注意的是View沒(méi)有提供該方法。 -
public boolean onTouchEvent(MotionEvent event)
在dispatchTouchEvent方法中調(diào)用惠遏,用來(lái)處理點(diǎn)擊事件砾跃。返回結(jié)果表示是否消耗當(dāng)前事件,如果不消耗爽哎,則在同一個(gè)事件序列中蜓席,當(dāng)前View無(wú)法再次接收到事件。
2.Activity對(duì)事件的分發(fā)
當(dāng)一個(gè)事件產(chǎn)生時(shí)课锌,事件會(huì)最先傳遞給當(dāng)前Activity,即Activity的dispatchTouchEvent方法會(huì)被調(diào)用祈秕。在Activity的dispatchTouchEvent方法中會(huì)將事件交給PhoneWindow處理渺贤,PhoneWindow又會(huì)將事件傳遞給DecorView,即事件傳遞順序?yàn)椋?strong>Activity->Window->DecorView请毛。我們先來(lái)看下Activity的dispatchTouchEvent方法志鞍,源碼如下:
//Activity的dispatchTouchEvent方法
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
上面Activity的dispatchTouchEvent方法中,先調(diào)用了window的superDispatchTouchEvent方法方仿,交給window來(lái)處理固棚。如果window的superDispatchTouchEvent返回true统翩,表示事件被處理,整個(gè)事件循環(huán)就結(jié)束了此洲;如果返回false厂汗,表示事件沒(méi)有被處理,那么Activity的onTouchEvent方法會(huì)被調(diào)用呜师,事件由Activity來(lái)處理娶桦。
接下來(lái)看Window的superDispatchTouchEvent方法。通過(guò)源碼可以發(fā)現(xiàn)Window是個(gè)抽象類(lèi)汁汗,它的superDispatchTouchEvent方法也是個(gè)抽象方法衷畦。所以我們需要看Window的唯一實(shí)現(xiàn)類(lèi)PhoneWindow的處理,源碼如下:
//PhoneWindow的superDispatchTouchEvent
public boolean superDispatchTouchEvent(MotionEvent event){
return mDecor.superDispatchTouchEvent(event);
}
在PhoneWindow的superDispatchTouchEvent方法中知牌,我們可以知道事件傳遞給了DecorView祈争。再看下DecorView的superDispatchTouchEvent方法,源碼如下:
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
DecorView繼承自FrameLayout角寸,它的super.dispatchTouchEvent方法調(diào)用的就是ViewGroup的dispatchTouchEvent方法菩混。并且Activity中通過(guò)setContentView所設(shè)置的根View,是getWindow.getDecorView().findViewById(Android.R.id.content).getChildAt(0)返回的View袭厂,那么可以確定根View是DecorView的一個(gè)子元素墨吓。所以事件傳遞到DecorView之后,其實(shí)就是ViewGroup的事件分發(fā)過(guò)程纹磺。
3.ViewGrou對(duì)事件的分發(fā)
ViewGroup對(duì)事件的分發(fā)過(guò)程帖烘,主要實(shí)現(xiàn)是在它的dispatchTouchEvent方法中。這個(gè)方法大概邏輯是先判斷是否攔截事件橄杨,如果不攔截事件秘症,則將事件交給子元素處理,如果攔截了事件或者子元素沒(méi)有處理事件則由自己處理事件式矫。
1.判斷是否攔截
先來(lái)看下如何判斷是否攔截事件的乡摹,源碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...省略無(wú)關(guān)代碼
final boolean intercepted;
//1. 判斷是否有子元素處理了該事件
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
//2. 判斷子元素是否設(shè)置了禁止攔截的標(biāo)記位
if (!disallowIntercept) {
//3. 調(diào)用自己的onInterceptTouchEvent方法,詢(xún)問(wèn)自己是否要攔截事件
intercepted = onInterceptTouchEvent(ev);
ev.setAction(action); // restore action in case it was changed
} else {
intercepted = false;
}
} else {
intercepted = true;
}
//...省略無(wú)關(guān)代碼
}
上面源碼可以看到采转,有兩個(gè)判斷聪廉,如果都為true,則會(huì)調(diào)用自己的onInterceptTouchEvent方法故慈,來(lái)詢(xún)問(wèn)自己是否攔截事件板熊。那么什么情況下不調(diào)用自己的onInterceptTouchEvent方法呢?
首先第一個(gè)判斷mFirstTouchTarget != null察绷,從后面的邏輯可以看出來(lái)干签,當(dāng)事件由子元素成功處理時(shí),mFirstTouchTarget會(huì)被賦值并指向子元素拆撼。
那么 (actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null) 這個(gè)判斷的主要目的就是容劳,當(dāng)沒(méi)有子元素去處理事件時(shí)喘沿,則該事件序列中的其它事件(move、up等事件)到來(lái)時(shí)竭贩,ViewGroup會(huì)默認(rèn)攔截事件蚜印,并且不再調(diào)用onInterceptTouchEvent方法。
第二個(gè)判斷boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0娶视,這里的FLAG_DISALLOW_INTERCEPT標(biāo)記位是通過(guò)requestDisallowInterceptTouchEvent方法設(shè)置的晒哄,一般用于子View中。一旦FLAG_DISALLOW_INTERCEPT設(shè)置后肪获,ViewGroup將無(wú)法攔截除了down事件以外的其它事件寝凌。為什么說(shuō)是down以外的其它事件呢?這是因?yàn)樾⒑眨琕iewGroup在分發(fā)事件時(shí)较木,如果是down事件機(jī)會(huì)重置這個(gè)標(biāo)記位。源碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...省略無(wú)關(guān)代碼
// Throw away all previous state when starting a new touch gesture.
// The framework may have dropped the up or cancel event for the previous gesture
// due to an app switch, ANR, or some other state change.
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
}
3.具體分發(fā)過(guò)程
如果ViewGroup不攔截事件青柄,會(huì)將事件交由它的子元素處理伐债,這段源碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...省略無(wú)關(guān)代碼
//如果不攔截事件,遍歷所有子元素致开,判斷子元素是否能夠接收事件
if (!canceled && !intercepted) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {//1. 倒序遍歷子元素
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
//2. 判斷子元素是否可以接收點(diǎn)擊事件
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// Child is already receiving touch within its bounds.
// Give it the new pointer in addition to the ones it is handling.
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
//3. 事件分發(fā)
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;
}
}
}
}
從上面源碼可以看到峰锁,首先ViewGroup會(huì)遍歷所有子元素,然后判斷子元素是否能夠接收點(diǎn)擊事件双戳。是否能夠接收點(diǎn)擊事件主要由兩點(diǎn)來(lái)衡量:子元素是否在播放動(dòng)畫(huà)和點(diǎn)擊事件的坐標(biāo)是否落在子元素的區(qū)域內(nèi)虹蒋。如果某個(gè)子元素滿(mǎn)足這兩個(gè)條件,就會(huì)調(diào)用dispatchTransformedTouchEvent方法將事件交給這個(gè)子元素處理飒货。
接下來(lái)我們看下View的dispatchTransformedTouchEvent方法魄衅,在該方法內(nèi)部有如下一段內(nèi)容:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
//...省略無(wú)關(guān)代碼
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
//省略無(wú)關(guān)代碼
handled = child.dispatchTouchEvent(event);
}
return handled;
}
因?yàn)樵谙蜃釉胤职l(fā)事件時(shí),child不為null塘辅,所以它會(huì)直接調(diào)用child的dispatchTouchEvent方法晃虫,將事件交給子元素處理。并且可以看到dispatchTransformedTouchEvent的返回值就是child.dispatchTouchEvent(event)的返回值扣墩。
再回到ViewGroup的dispatchTouchEvent方法中哲银,可以看如果dispatchTransformedTouchEvent返回true,那么mFirstTouchTarget就會(huì)被賦值呻惕,同時(shí)break(跳出循環(huán))盘榨。當(dāng)然,如果返回false蟆融,ViewGroup會(huì)繼續(xù)向下一個(gè)子元素分發(fā)。如下所示:
//事件分發(fā)
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
//...省略無(wú)關(guān)代碼
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
mFirstTouchTarget的賦值是在過(guò)程是在addTouchTarget方法內(nèi)部完成的守呜,addTouchTarget方法內(nèi)部可以看出型酥,mFirstTouchTarget是一種單鏈表結(jié)構(gòu)山憨。前面提到過(guò)mFirstTouchTarget,mFirstTouchTarget是否賦值弥喉,將影響VIewGroup對(duì)事件的攔截策略郁竟。如果mFirstTouchTarget為null,那么ViewGroup就會(huì)默認(rèn)攔截同一序列中down事件以外的其它所有事件由境。mFirstTouchTarget賦值源碼如下:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
3.ViewGroup自己處理事件
如果ViewGroup攔截了事件或者它的所有子元素都沒(méi)有處理事件棚亩,mFirstTouchTarget為null,那么ViewGroup會(huì)自己處理事件虏杰,這段代碼如下:
@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
//...省略無(wú)關(guān)代碼
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
}
//...省略無(wú)關(guān)代碼
}
在這里的dispatchTransformedTouchEvent方法第三個(gè)參數(shù)child為null讥蟆。前面有介紹這個(gè)方法,如果child為null纺阔,會(huì)調(diào)用super.dispatchTouchEvent方法瘸彤,而ViewGroup的父類(lèi)是View,所以實(shí)際調(diào)用的是View的dispatchTouchEvent方法笛钝。那么可以知道质况,無(wú)論事件是分發(fā)給子View還是ViewGroup自己處理,都會(huì)轉(zhuǎn)到View的dispatchTouchEvent方法玻靡,接下來(lái)繼續(xù)分析View的dispatchTouchEvent方法结榄。
4.View對(duì)事件的處理
1.View的dispatchTouchEvent方法
因?yàn)閂iew(這里不包含ViewGroup)是一個(gè)單獨(dú)的元素,它沒(méi)有子元素?zé)o法向下傳遞事件囤捻,因此它只能自己處理事件臼朗。源碼如下:
public boolean dispatchTouchEvent(MotionEvent event) {
boolean result = false;
//...省略無(wú)關(guān)代碼
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//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;
}
}
//...省略無(wú)關(guān)代碼
return result;
}
從上面的源碼可以看出View對(duì)點(diǎn)擊事件的處理過(guò)程,首先會(huì)判斷有沒(méi)有設(shè)置OnTouchListener最蕾。如果OnTouchListener中的onTouch方法返回true依溯,那么onTouchEvent就不會(huì)被調(diào)用,可見(jiàn)OnTouchListener的優(yōu)先級(jí)高于OnTouchEvent瘟则,這樣做的好處是方便在外界處理點(diǎn)擊事件黎炉。
2.View的OnTouchEvent方法
下面看下OnTouchEvent的實(shí)現(xiàn),首先當(dāng)View處于不可用狀態(tài)下照樣會(huì)消耗點(diǎn)擊事件醋拧,盡管它看起來(lái)不可用慷嗜。這段源碼如下:
final int action = event.getAction();
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if ((viewFlags & ENABLED_MASK) == DISABLED
&& (mPrivateFlags4 & PFLAG4_ALLOW_CLICK_WHEN_DISABLED) == 0) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
接著,如果View設(shè)置有代理丹壕,那么還會(huì)執(zhí)行TouchDelegate的onTouchEvent方法庆械,這個(gè)onTouchEvent的工作機(jī)制看起來(lái)和OnTouchListener類(lèi)似。
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
接下來(lái)看下onTouchEvent中對(duì)點(diǎn)擊事件的具體處理菌赖,如下所示:
public boolean onTouchEvent(MotionEvent event) {
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
if (clickable) {
switch (event.getAction()) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
//...省略無(wú)關(guān)代碼
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClickInternal();
}
}
}
//...省略無(wú)關(guān)代碼
}
break;
case MotionEvent.ACTION_DOWN:
//...省略無(wú)關(guān)代碼
break;
case MotionEvent.ACTION_CANCEL:
//...省略無(wú)關(guān)代碼
break;
case MotionEvent.ACTION_MOVE:
//...省略無(wú)關(guān)代碼
break;
}
return true;
}
return false;
}
從上面的代碼來(lái)看瓣俯,只要View的CLICKABLE和LONG_CLICKABLE有一個(gè)為true,那么它就會(huì)消耗這個(gè)事件蛋叼,即onTouchEvent方法返回true,不管它是不是DISABLE狀態(tài)策幼。然后就是當(dāng)ACTION_UP事件發(fā)生時(shí),會(huì)觸發(fā)performClick方法奴紧,如果View設(shè)置了onClickListener特姐,那么performClick方法內(nèi)部會(huì)調(diào)用它的onClick方法,如下所示:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
return result;
}
View的LONG_CLICKABLE屬性默認(rèn)為false黍氮,而CLICKABLE屬性是否為false和具體的View有關(guān)唐含。確切的來(lái)說(shuō)是可點(diǎn)擊的View其CLICKABLE為true,不可點(diǎn)擊的View其CLICKABLE為false沫浆,比如Button是可點(diǎn)擊的捷枯,TextView是不可點(diǎn)擊的。通過(guò)setClickable和setLongClickable可以分別改變View的CLICKABLE和LONG_CLICKABLE屬性件缸。另外铜靶,setOnClickListener會(huì)自動(dòng)將View的CLICKABLE設(shè)為true,setLongClickListener則會(huì)自動(dòng)將View的LONG_CLICKABLE設(shè)為true他炊,這一點(diǎn)從源碼中可以看出來(lái)争剿,如下所示:
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
public void setOnLongClickListener(@Nullable OnLongClickListener l) {
if (!isLongClickable()) {
setLongClickable(true);
}
getListenerInfo().mOnLongClickListener = l;
}
到這里,事件分發(fā)機(jī)制的源碼也就分析完了痊末。