事件分發是一個老生常談的話題,既然是一個「冷飯」,那爲何今天又開始「炒冷飯」了呢?說白了,仍是本身高估了對事件分發的理解。java
這裏拋出幾個問題:api
onTouchEvent()
不會被響應? -> 答案在:方法展開2部分。onTouchEvent()
返回了true,爲啥它下層的View就不再會響應任何事件回調了? -> 答案在:方法展開1部分onTouchEvent()
並返回了true,那麼它的onInterceptTouchEvent()
還會被回調嗎? -> 答案在:1.二、部分總結部分。dispatchTouchEvent()
並直接返回true,會怎麼樣?-> 答案在:方法展開2部分。若是各位小夥伴能夠很是清晰的回答這些問題,那麼這篇文章就不用看了,左上角點X,唱、跳、Rap、打會籃球什麼的...固然若是你願意留下來點點廣告,那也是極好的~哈哈學習
既然叫作事件分發,那麼本質其實就是分發。我猜你們剛開始瞭解這一塊內容時,確定繞不開三個方法:dispatchTouchEvent()
、onInterceptTouchEvent()
、onTouchEvent()
。不過我真以爲,扯上後邊倆個方法,反而把問題複雜化。動畫
對於事件分發來講,核心就是dispatchTouchEvent()
的實現,onInterceptTouchEvent()
、和onTouchEvent()
只是讓咱們參與到分發流程當中來的接口而已。this
所以,這篇文章的核心就在於梳理、閱讀ViewGroup和View的dispatchTouchEvent()
方法實現。相信我,閱讀完這篇文章絕對有收穫~~spa
源碼基於api-28code
關於dispatchTouchEvent()的邏輯,這裏主要分爲倆個大部分,前半部分側重於事件消費對象的肯定(1.1部分);後半部分側重於對事件消費對象的後續分發(1.2部分)。orm
這部分代碼邏輯主要爲了:cdn
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的邏輯
}
複製代碼
此時總結並解釋一下開頭寫的:一、找到並記錄命中消費事件的View;二、對各層View的DOWN事件分發。 一、找到並記錄命中消費事件的View: 當DOWN來到ViewGroup的時候,若是自身不攔截,那麼就會嘗試分發。最終將根據命中View是否消費(重寫onTouchEvent()
/onTouch()
/重寫dispatchTouchEvent()
)來決定是否對mFirstTouchTarget進行賦值(記錄命中消費事件的View)。 二、對各層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,那這種想法能不能實現呢?答案是不能,爲何請閱讀:事件分發額外閱讀。
此部分邏輯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;
}
// 取消的邏輯暫時不作考慮
}
}
}
複製代碼
此部分代碼較少,並且邏輯清晰。主要就在於倆個分支,一個是沒有找到可以消費的View,那麼分發給本身,直接super.dispatchTouchEvent()
。本身的onTouchEvent()處理。不然經過mFirstTouchTarget,分發後續產生的事件。
此部份內容,請結合1、ViewGroup中的dispatchTouchEvent()「食用」
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個問題)白話文:背鍋的已經找到,此事無序再追查。哈哈~
// 能夠看到,對於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()返回值。
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
複製代碼
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事件的部分補上。
不着急,咱先把今天的文章嘮明白。