Android事件分發機制小結


基本知識

事件傳遞的三個主體

Activity、ViewGroup、View
他們三個的嵌套關係通常是這樣:

可是還要明白的是:


java

  • ViewGroup固然能夠嵌套ViewGroup,即ViewGroup也能夠是另外一個ViewGroup的子View。
  • ViewGroup實際上是繼承於View,是View的子類。

事件分發機制相關三個經典函數

  • dispatchTouchEvent():分發函數
  • onInterceptTouchEvent():攔截函數
  • onTouchEvent():消費函數

它們的功能和它們名字同樣。其中攔截函數是ViewGroup獨有的,其它兩個函數在上面說的三個主體都存在。函數

事件分發機制四個經典事件

  • ACTION_DOWN
  • ACTION_MOVE
  • ACTION_UP
  • ACTION_CANCLE

意如其名源碼分析

事件分發機制場景

參考文章:https://blog.csdn.net/caifengyao/article/details/65437695?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param學習

不攔截、不消費

當三個主體的任何函數的 返回值 都不作任何處理時,即不攔截、不消費:動畫


可見:
ui

  • 對於down事件:我會從外層一層層地分發下去(Activity->ViewGroup->view),看看
  • down不消費,move,up我就不傳遞去了

ViewGroup攔截、無消費

當在ViewGroup使onInterceptTouchEvent()返回true,即ViewGroup對事件進行攔截時:this


可見:
spa

  • 事件被攔截以後就不會往下分發

ViewGoup消費,不攔截

當在ViewGroup使onTouchEvent()返回true,即ViewGroup對事件進行消費時:


可見:


.net

  • 當down被消費了就不會往上冒
  • move up不會往下發,而是直接分發給消費者。

源碼分析

具體代碼怎麼實現?主要是看分發函數dispatchTouchEvent(),接下來咱們看看 三個主體的dispatchTouchEvent() 源碼分析code

Activity的dispatchTouchEvent()源碼

public boolean dispatchTouchEvent(MotionEvent ev) { 
	if (ev.getAction() == MotionEvent.ACTION_DOWN) { 
	    onUserInteraction();//這是一個空方法
	}
	//主要看這一句
	//getWindow().superDispatchTouchEvent(ev)
	//這句函數調用的時DecorView的superDispatchTouchEvent()
	//而DecorView繼承於ViewGroup
	if (getWindow().superDispatchTouchEvent(ev)) { 
	    return true;
	}
	return onTouchEvent(ev);
}

代碼中我寫了註釋,咱們能夠得出下面的結論:

  • getWindow().superDispatchTouchEvent(ev),實際是調用了一個ViewGroup的dispatchTouchEvent()
  • getWindow().superDispatchTouchEvent(ev)返回true,說明有子View消費該事件(爲何呢?咱們要分析完ViewGroup的dispatchTouchEvent()才知道,但如今能夠暫時給出這個結論);這個子View多是某個ViewGroup或者View。
  • 若是有子view消費該事件則返回true,不然調用自身的onTouchEvent(ev),即把事件分發給本身。

ViewGroup的dispatchTouchEvent()源碼

ViewGroup的dispatchTouchEvent()源碼很長,我參考了https://blog.csdn.net/wolinxuebin/article/details/53057075
以後得出一些小結,如今貼出一部分,一段一段分析。

整體邏輯分析

我把部分代碼和具體邏輯去掉,看它的空架子

//這是一個單鏈表,我暫時理解爲用於存放響應了DOWN的事件
private TouchTarget mFirstTouchTarget;

public boolean dispatchTouchEvent(MotionEvent ev) { 
	...
	//判斷是不是模糊窗口,若是是窗口,則表示不但願處理改事件。(如dialog後的窗口)
	if (onFilterTouchEventForSecurity(ev)) { 
	    // 清空以前的狀態
	    if (actionMasked == MotionEvent.ACTION_DOWN) { }
	
	    //檢查是否須要攔截
	    final boolean intercepted;
	    if (actionMasked == MotionEvent.ACTION_DOWN
	            || mFirstTouchTarget != null) { } else { }
	            
	    //檢查是否要取消,即標記了PFLAG_CANCEL_NEXT_UP_EVENT 或者 當前是一個Cancel事件
	    final boolean canceled = resetCancelNextUpFlag(this)
	            || actionMasked == MotionEvent.ACTION_CANCEL;
	
	    //不用取消,無需攔截,則進行事件分發 
	    if (!canceled && !intercepted) { 
	       //分發DOWN事件
	        if (actionMasked == MotionEvent.ACTION_DOWN
	                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
	                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) { 
	           //分發DOWN給child
	            if (newTouchTarget == null && childrenCount != 0) {  
	                
	            }
	            
	            //若是沒有child相應該事件,則將此事件交給最近加入的target?
	            //這裏不是很懂,若是沒有child響應,那麼mFirstTouchTarget也是null呀
	            if (newTouchTarget == null && mFirstTouchTarget != null) { 
	               
	            }
	        }
	    }
	
	    //mFirstTouchTarget爲空代表沒有child響應這個事件,則分發給本身
	    if (mFirstTouchTarget == null) { }
	    //按照mFirstTouchTarget分發
	    else { }
   
    }
	...
	//若是自身或者child消費了事件則返回true,不然返回false
	return handled;    
}

關於解析看代碼中的註釋。接下來看看其中幾段邏輯具體怎麼實現的。

具體分析一

看一下分發給DOWN給子View的具體邏輯:

//分發DOWN給child
if (newTouchTarget == null && childrenCount != 0) {  
	
	// 對子Views進行排序,有兩種方式:一、按照Z軸,二、按照draw
	final ArrayList<View> preorderedList = buildTouchDispatchChildList();
	final boolean customOrder = preorderedList == null
	        && isChildrenDrawingOrderEnabled();
	final View[] children = mChildren;

	//遍歷子View
	for (int i = childrenCount - 1; i >= 0; i--) { 
		//這裏兩行代碼,簡單的理解根據不一樣的排列選項(一、view添加到 二、view的draw順序 三、viewZ 軸順序)
	    final int childIndex = getAndVerifyPreorderedIndex(
	            childrenCount, i, customOrder);
	    final View child = getAndVerifyPreorderedView(
	            preorderedList, children, childIndex);
	            
		//canViewReceivePointerEvents 判斷child是否爲visiable 或者 是否有動畫
        //isTransformedTouchPointInView 判斷x, y是否在view的區域內(若是是執行了補間動畫 則x,y會經過獲取的matrix變換值
        // 換算當相應的區域,這也是爲何補間動畫的觸發區域不隨着動畫而改變)
	    if (!child.canReceivePointerEvents()
	            || !isTransformedTouchPointInView(x, y, child, null)) { 
	        continue;
	    }
	    
		//若是chile已經在mFirstTouchTarget單鏈表裏面,結束循環
	    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;
	    }
	
		//判斷child的dispatchTouchEvent()是否會返回true,若是是true,將child加入單鏈表,而後結束循環
		//dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)會調用child的dispatchTouchEvent()
	    resetCancelNextUpFlag(child);
	    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { 
	        // Child wants to receive touch within its bounds.
	        mLastTouchDownTime = ev.getDownTime();
	        if (preorderedList != null) { 
	            // 找到childIndex所表明的child的最原始的index
	            for (int j = 0; j < childrenCount; j++) { 
	                if (children[childIndex] == mChildren[j]) { 
	                    mLastTouchDownIndex = j;
	                    break;
	                }
	            }
	        } else { 
	            mLastTouchDownIndex = childIndex;
	        }
			
			//將相應該事件的child包裝成一個Target,添加到mFirstTouchTarget鏈表中
	        mLastTouchDownX = ev.getX();
	        mLastTouchDownY = ev.getY();
	        newTouchTarget = addTouchTarget(child, idBitsToAssign);
	        alreadyDispatchedToNewTouchTarget = true;
	        break;
	    }
	}
}

這裏我只貼出部分的代碼,解析都在註釋,能夠看到,mFirstTouchTarget鏈表只存在一個值,就是響應了事件的那個child。

具體分析二

//這一段的要麼分發down給本身要麼按照單鏈表分發move、up
//僞代碼
// 
if (mFirstTouchTarget == null) { 
    //沒有child響應事件,則分發給本身,handled將做爲返回值返回。
    handled = dispatchTransformedTouchEvent(ev, canceled, null,
            TouchTarget.ALL_POINTER_IDS);
} else { 
    // Dispatch to touch targets, excluding the new touch target if we already
    // dispatched to it. Cancel touch targets if necessary.
    TouchTarget predecessor = null;
    TouchTarget target = mFirstTouchTarget;
    while (target != null) { 
        final TouchTarget next = target.next;
        //在前面Down事件處理中,已經將這個事件交給newTouchTarget處理過了,就不重複處理了
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) { 
            handled = true;
        } else { 
        	//這裏實際上是分發move和up事件,由於down事件在前面已經處理完了,不會進入這裏
        	//再次斷定是否須要cancel,由於有可能在move過程事件被攔截
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            if (dispatchTransformedTouchEvent(ev, cancelChild,
                    target.child, target.pointerIdBits)) { 
                handled = true;
            }
            //若是cancel,回收鏈表節點空間,最後使mFirstTouchTarget置null
            if (cancelChild) { 
                if (predecessor == null) { 
                    mFirstTouchTarget = next;
                } else { 
                    predecessor.next = next;
                }
                target.recycle();
                target = next;
                continue;
            }
        }
        predecessor = target;
        target = next;
    }
}

View的dispatchTouchEvent()源碼

一樣的,我省略了部分代碼,解析看註釋

public boolean dispatchTouchEvent(MotionEvent event) { 
	boolean result = false;
	
	if (onFilterTouchEventForSecurity(event)) { 
	    //若是設置了onTouchListener,會先調用onTouch()
	    ListenerInfo li = mListenerInfo;
	    if (li != null && li.mOnTouchListener != null
	            && (mViewFlags & ENABLED_MASK) == ENABLED
	            && li.mOnTouchListener.onTouch(this, event)) { 
	        result = true;
	    }
	
		//調用onTouchEvent()
	    if (!result && onTouchEvent(event)) { 
	        result = true;
	    }
	}
	return result;
}

能夠看到:

  • View的dispatchTouchEvent()的返回值取決於onTouch()或onTouchEvent(),也就是有沒有消費該事件。
  • 若是onTouch()返回true的話就不會調用onTouchEvent(),這就是爲何有些博客寫onTouch()優先於onTouchEvent()。

總結

分析源碼就能夠知道前面說的三種場景是怎麼回事了。

有點像皇帝派任務,皇帝說如今有個好活兒,可是不知道誰要接這個活兒,因而派了一個小太監去探測一下;
小太監先去找宰相,宰相又讓他去找知府,知府讓他去找衙門小兵。

這裏面皇帝就像Acticity、各級官員就像ViewGroup、小兵就像View、而小太監就像DOWN事件,活兒就是跟着DOWN後面的MOVE和UP事件。

  • 不攔截、不消費:小太監一層層找到小兵後,沒有一個小兵想接這個活兒(一層層分發DOWN事件),因而小兵沿路返回報告給知府、知府也不想作就報告給宰相,宰相不想作就回去報告給皇帝,皇帝說沒人作那我看看本身能不能作吧(DOWN事件回到Activity派給本身,MOVE、UP也再也不分發而是直接派給本身)
  • ViewGroup攔截、不消費:宰相讓小太監找到知府的時候, 這個知府有點霸道直接把小太監攔下了,小太監就就再也不繼續通知下級人員了(攔截事件)。可是呢這個官員只是單純攔下了小太監但他並不想接這個活兒,因而小兵仍是沿路回去報告給說下面沒人接活,最終仍是傳回給皇帝說沒人作那我看看本身能不能作(MOVE、UP再也不分發直接派給本身)
  • ViewGroup消費、不攔截:一樣,無人攔截的話,小太監一層層找到小兵,發現小兵沒人想作,就回去報告知府,這時候知府說小兵不作我來作(DOWM在這裏被消費了)。而後知府寫信報告宰相說這活兒我接了(返回true),宰相又報告皇帝說下面有人接受任務了,然會皇帝下次就直接派發任務給宰相,宰相找到那個願意接受任務知府,把任務派給他。(派發MOVE、UP)

分發函數分發DOWN時其實有點相似於遞歸的方式,只不過不是本身調用本身,而是一層層地調用child的同名函數。分發MOVE、UP則再也不一一詢問,而是根據DOWN是否被消費進行分發。

源碼很長如今頭都有點亂,那麼從源碼能夠學習到什麼呢,你們幫忙補充吧

  • 若有有這種嵌套式的應答需求,能夠學習ViewGroup的分發函數,用相似於遞歸的方式提問和接收應答。
  • 對於較大量的信息,命令傳送,能夠先派一個小兵嗅探一下,記錄可行的路線,後續信息按路線分發。
相關文章
相關標籤/搜索