前言
事件分發是一個老生常談的話題,既然是一個“冷飯”,那爲什麼今天又開始“炒冷飯”了呢?說白了,還是自己高估了對事件分發的理解。
這裏拋出幾個問題:
- 1、對一個View進行setOnTouchListener操作,並且onTouch()返回true,爲啥它的onTouchEvent()不會被響應? -> 答案在:方法展開2部分。
- 2、一個View的onTouchEvent()返回了true,爲啥它下層的View就再也不會響應任何事件回調了? -> 答案在:方法展開1部分
- 3、如果一個ViewGroup只重寫了onTouchEvent()並返回了true,那麼它的onInterceptTouchEvent()還會被回調嗎? -> 答案在:1.2、部分總結部分。
- 4、重寫dispatchTouchEvent()並直接返回true,會怎麼樣?-> 答案在:方法展開2部分。
如果各位小夥伴可以非常清晰的回答這些問題,那麼這篇文章就不用看了,左上角點X,唱、跳、Rap、打會籃球什麼的…當然如果你願意留下來點點廣告,那也是極好的~哈哈
正文
既然叫做事件分發,那麼本質其實就是分發。我猜大家剛開始瞭解這一塊內容時,肯定繞不開三個方法:dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
。不過我真覺得,扯上後邊倆個方法,反而把問題複雜化。
對於事件分發來說,核心就是dispatchTouchEvent()的實現,onInterceptTouchEvent()
、和onTouchEvent()
只是讓我們參與到分發流程當中來的接口而已。
因此,這篇文章的核心就在於梳理、閱讀ViewGroup和View的dispatchTouchEvent()方法實現。相信我,閱讀完這篇文章絕對有收穫~~
一、ViewGroup中的dispatchTouchEvent()
源碼基於api-28
關於dispatchTouchEvent()的邏輯,這裏主要分爲倆個大部分,前半部分側重於事件消費對象的確定(1.1部分);後半部分側重於對事件消費對象的後續分發(1.2部分)。
1.1、mFirstTouchTarget的首次賦值
這部分代碼邏輯主要爲了:
- 1、找到並記錄命中消費事件的View
- 2、對各層View的DOWN事件分發
public boolean dispatchTouchEvent(MotionEvent ev){
// 記住這個mFirstTouchTarget,很關鍵
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null){
// 如果子View沒有調用requestDisallowInterceptTouchEvent(true),則調用自身的onInterceptTouchEvent()
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
intercepted = onInterceptTouchEvent(ev);
} else {
intercepted = false;
}
} else {
// 如果不是DOWN事件,並且mFirstTouchTarget == null,那麼就直接認定當前View攔截
intercepted = true;
}
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false; // 注意一下這個局部變量,會用到
if (!canceled && !intercepted) {
// 省略部分代碼
if (newTouchTarget == null && childrenCount != 0) {
// 遍歷View(這裏的順序可以通過重寫setChildrenDrawingOrderEnabled() + getChildDrawingOrder()自定義順序)
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 如果當前的View出在動畫;或者x、y不在View區域內直接continue
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
continue;
}
// 該方法會遍歷TouchTarget,但是初始的target需要通過mFirstTouchTarget進行賦值,此時爲null。具體實現細節可查看:方法展開4
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
// 多指操作,暫時忽略
break;
}
// DOWN事件一定會走到此,因爲newTouchTarget == null,此方法邏輯見:方法展開1
// 此方法便開始向其他層級的View進行分發事件,此方法的返回值決定了是否走if的邏輯。
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 當子View選擇消費這個事件時,那麼將會走接下來的代碼。這裏主要的內容就是給newTouchTarget和mFirstTouchTarget進行賦值。(此方法邏輯見:方法展開3)
// 也就是說,如果代碼走到這,那麼mFirstTouchTarget將不再爲null
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
}
}
}
}
// 截止到此是intercepted位false的邏輯
}
1.2、部分總結
此時總結並解釋一下開頭寫的:1、找到並記錄命中消費事件的View;2、對各層View的DOWN事件分發。
1、找到並記錄命中消費事件的View:
當DOWN來到ViewGroup的時候,如果自身不攔截,那麼就會嘗試分發。最終將根據命中View是否消費(重寫onTouchEvent()/onTouch()/重寫dispatchTouchEvent())來決定是否對mFirstTouchTarget進行賦值(記錄命中消費事件的View)。
2、對各層View的DOWN事件分發:
這部分代碼裏,我們第一個遇到了dispatchTransformedTouchEvent()方法,這個方法會調用child或者super的dispatchTouchEvent(),最終通過View的onTouchEvent()/onTouch()等方法的返回值來決定dispatchTransformedTouchEvent()的返回值。因此拿到返回值的時候,其實這個事件已經在所有的View中分發了一遍。
此時如果mFirstTouchTarget不爲null,那麼後續的MOVE和UP事件將重走這一套流程(if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget != null))。
或者intercepted直接爲true;直接交給自己處理。
這裏解答開篇的第三個問題,通過代碼我們可以看到只要mFirstTouchTarget不爲null,並且子View不調用requestDisallowInterceptTouchEvent(true),那麼當前ViewGroup的onInterceptTouchEvent()一定會調用,它和onTouchEvent()的返回值沒有任何關係。
解答完這個問題,不知道有沒有小夥伴想到一個點:那就是如果ViewGroup的onInterceptTouchEvent()在滿足條件下,一定會調用。那麼我是不是可以在某一層View消費了一定的事件後,然後再通過一些條件判斷讓ViewGroup中的onInterceptTouchEvent()返回true。這樣就可以做到事件沒消費完繼續分發給其他View,那這種想法能不能實現呢?答案是不能,爲什麼請閱讀:事件分發額外閱讀。
1.3、MOVE/UP事件分發的關鍵
此部分邏輯DOWN也會觸發,但更多的是爲了分發MOVE/UP
public boolean dispatchTouchEvent(MotionEvent ev){
// 此邏輯分析承接上半部分
// 如果mFirstTouchTarget == null有倆種可能,一個是的確沒有找到能夠命中的View,另一個是自己直接攔截
if (mFirstTouchTarget == null) {
// 此時child這個字段傳null,也就是說直接調自己的super.dispatchTouchEvent()分發給了自己。
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 能走到此方法說明mFirstTouchTarget已經不會null,也就是找個了可以去分發的View
// 省略部分條件
TouchTarget target = mFirstTouchTarget;
while (target != null) {
// 這裏用到了alreadyDispatchedToNewTouchTarget,很簡單對於DOWN事件來說,其實分發已經走了一遍,並且爲mFirstTouchTarget賦了值,如果此處不過濾掉那麼分發流程就會走倆遍。
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
handled = true;
} else {
// 否則向其他View分發事件,其實我猜大家應該都明白了,MOVE/UP事件會通過此邏輯完成分發
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
// 取消的邏輯暫時不做考慮
}
}
}
1.4、部分總結
此部分代碼較少,而且邏輯清晰。主要就在於倆個分支,一個是沒有找到能夠消費的View,那麼分發給自己,直接super.dispatchTouchEvent()。自己的onTouchEvent()處理。否則通過mFirstTouchTarget,分發後續產生的事件。
二、方法展開
此部分內容,請結合一、ViewGroup中的dispatchTouchEvent()“食用”
2.1、方法展開1:
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 省略部分代碼
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
// 如果事件命中了某個View,此時將調用這個View的dispatchTouchEvent()。當然如果此時的View是一個ViewGroup那麼會不斷進行上述的過程,此時的返回值就是super.dispatchTouchEvent(event),也就是View的dispatchTouchEvent(此方法邏輯見:方法展開2)。
// 不過這裏肯定有同學會問如果我當前的View重寫了dispatchTouchEvent(),並return true會怎麼樣?-> 看一下 方法展開2 就會明白
handled = child.dispatchTouchEvent(event);
}
return handled;
}
對於此方法來說,一旦handled返回了true,那麼對於ViewGroup的dispatchTouchEvent()來說就可以確定mFirstTouchTarget。有了mFirstTouchTarget,意味着消費的View已經被確定,無需要在將事件往下分發。(這也就解答了開篇拋出來的第2個問題)白話文:背鍋的已經找到,此事無序再追查。哈哈~
2.2、方法展開2:View中的dispatchTouchEvent()
// 可以看到,對於View來說dispatchTouchEvent()的返回值,依賴onTouchEvent()的返回值、onTouch()返回值。
// 並且這也說明了一個嚴重的問題:那就是onTouchEvent等事件的調用是在View的dispatchTouchEvent之中,如果我們重寫了某個View的dispatchTouchEvent直接return會了true,那麼就意味着onTouchEvent等方法將再也沒有機會執行了。(這也就解答了開篇拋出來的第4個問題)
public boolean dispatchTouchEvent(MotionEvent event) {
// 省略
if (onFilterTouchEventForSecurity(event)) {
// 省略
ListenerInfo li = mListenerInfo;
// 此處可以看到,如果listener不爲null,並且onTouch()返回true,那麼result這個局部變量就會爲true。那麼就對於下邊的判斷條件來說第一個條件就不滿足,因此就不會再調用onTouchEvent()了。(這也就解答了開篇拋出來的第1個問題)
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;
}
總結方法展開1 + 方法展開2:
如果我們某個View重寫了dispatchTouchEvent()並且直接返回true,那麼對於dispatchTransformedTouchEvent()這個方法來說,將直接得到true;否則將依賴View中
onTouchEvent()的返回值、onTouch()返回值。
2.3、方法展開3:
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
2.3、方法展開4:
private TouchTarget getTouchTarget(@NonNull View child) {
// 因爲mFirstTouchTarget的默認值是null,因此首次調用此方法一定return null。也就是DOWN來的時候,此方法return null。
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
三、事件分發額外閱讀
上文產生的這個問題,首先明確答案是不行。因爲我們已經看罷了通篇的源碼。當事件已經開始被某個View消費,那麼就意味着mFirstTouchTarget不爲null,那麼getTouchTarget(child)也將不爲null,因此將不會重新分發此事件。同一個事件序列只會繼續分發給mFirstTouchTarget。
對於當前的dispatchTouchEvent()來說。事件已經被其他View消費,木已成舟。此時再想改變onInterceptTouchEvent()爲true,已經“無力迴天”。
尾聲
本篇文章到此就結束了,可能有朋友會問,關於CANCEL事件還沒講!沒錯,爲啥沒聊呢?因爲我還沒看。有機會的話,會把關於CANCEL事件的部分補上。
不着急,咱先把今天的文章嘮明白。