爲啥還在聊:事件分發?還不是由於不會!

前言

事件分發是一個老生常談的話題,既然是一個「冷飯」,那爲何今天又開始「炒冷飯」了呢?說白了,仍是本身高估了對事件分發的理解。java

這裏拋出幾個問題:api

  • 一、對一個View進行setOnTouchListener操做,而且onTouch()返回true,爲啥它的onTouchEvent()不會被響應? -> 答案在:方法展開2部分。
  • 二、一個View的onTouchEvent()返回了true,爲啥它下層的View就不再會響應任何事件回調了? -> 答案在:方法展開1部分
  • 三、若是一個ViewGroup只重寫了onTouchEvent()並返回了true,那麼它的onInterceptTouchEvent()還會被回調嗎? -> 答案在:1.二、部分總結部分。
  • 四、重寫dispatchTouchEvent()並直接返回true,會怎麼樣?-> 答案在:方法展開2部分。

若是各位小夥伴能夠很是清晰的回答這些問題,那麼這篇文章就不用看了,左上角點X,唱、跳、Rap、打會籃球什麼的...固然若是你願意留下來點點廣告,那也是極好的~哈哈學習

正文

既然叫作事件分發,那麼本質其實就是分發。我猜你們剛開始瞭解這一塊內容時,確定繞不開三個方法:dispatchTouchEvent()onInterceptTouchEvent()onTouchEvent()。不過我真以爲,扯上後邊倆個方法,反而把問題複雜化。動畫

對於事件分發來講,核心就是dispatchTouchEvent()的實現,onInterceptTouchEvent()、和onTouchEvent()只是讓咱們參與到分發流程當中來的接口而已。this

所以,這篇文章的核心就在於梳理、閱讀ViewGroup和View的dispatchTouchEvent()方法實現。相信我,閱讀完這篇文章絕對有收穫~~spa

1、ViewGroup中的dispatchTouchEvent()

源碼基於api-28code

關於dispatchTouchEvent()的邏輯,這裏主要分爲倆個大部分,前半部分側重於事件消費對象的肯定(1.1部分);後半部分側重於對事件消費對象的後續分發(1.2部分)。orm

1.一、mFirstTouchTarget的首次賦值

這部分代碼邏輯主要爲了:cdn

  • 一、找到並記錄命中消費事件的View
  • 二、對各層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.二、部分總結

此時總結並解釋一下開頭寫的:一、找到並記錄命中消費事件的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,那這種想法能不能實現呢?答案是不能,爲何請閱讀:事件分發額外閱讀

1.三、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.四、部分總結

此部分代碼較少,並且邏輯清晰。主要就在於倆個分支,一個是沒有找到可以消費的View,那麼分發給本身,直接super.dispatchTouchEvent()。本身的onTouchEvent()處理。不然經過mFirstTouchTarget,分發後續產生的事件。

2、方法展開

此部份內容,請結合1、ViewGroup中的dispatchTouchEvent()「食用」

2.一、方法展開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: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:

private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
複製代碼

2.三、方法展開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;
}
複製代碼

3、事件分發額外閱讀

上文產生的這個問題,首先明確答案是不行。由於咱們已經看罷了通篇的源碼。當事件已經開始被某個View消費,那麼就意味着mFirstTouchTarget不爲null,那麼```getTouchTarget(child)``````也將不爲null,所以將不會從新分發此事件。同一個事件序列只會繼續分發給mFirstTouchTarget。

對於當前的dispatchTouchEvent()來講。事件已經被其餘View消費,木已成舟。此時再想改變onInterceptTouchEvent()爲true,已經「無力迴天」。

尾聲

本篇文章到此就結束了,可能有朋友會問,關於CANCEL事件還沒講!沒錯,爲啥沒聊呢?由於我還沒看。有機會的話,會把關於CANCEL事件的部分補上。

不着急,咱先把今天的文章嘮明白。

我是一個應屆生,最近和朋友們維護了一個公衆號,內容是咱們在從應屆生過渡到開發這一路所踩過的坑,以及咱們一步步學習的記錄,若是感興趣的朋友能夠關注一下,一同加油~

我的公衆號:鹹魚正翻身
相關文章
相關標籤/搜索