一篇文章帶你真正搞定:android事件分發

前言

事件分發是一個老生常談的話題,既然是一個“冷飯”,那爲什麼今天又開始“炒冷飯”了呢?說白了,還是自己高估了對事件分發的理解。

這裏拋出幾個問題:

  • 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事件的部分補上。

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

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

個人公衆號:鹹魚正翻身

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章