Android事件分發(fā)機制源碼和實例解析
1.事件分發(fā)過程的理解
1.1. 概述
1.2. 主要方法
1.3. 核心行為
1.4. 特殊情況
2.案例分析
2.1. 案例1:均不消費 down 事件
2.2. 案例2:View0 消費 down 事件
2.3. 案例3:ViewGroup2nd 消費 down 事件
3.down 事件分發(fā)圖
1. 事件分發(fā)過程的理解
1.1. 概述
事件主要有 down(MotionEvent.ACTION_DOWN),move(MotionEvent.ACTION_MOVE),up(MotionEvent.ACTION_UP)。
基本上的手勢均由 down 事件為起點,up 事件為終點,中間可能會有一定數(shù)量的move 事件。這三種事件是大部分手勢動作的基礎(chǔ)。
事件和相關(guān)信息(比如坐標(biāo))封裝成 MotionEvent。
大體的分發(fā)過程為:首先傳遞到 Activity,然后傳給了 Activity 依附的 Window,接著由 Window 傳給視圖的頂層 View 也就是 DecorView,***由 DecorView 向整個 ViewTree 分發(fā)。分發(fā)還會有回溯的過程。***還會回到 Activity 的調(diào)用中。
Activity 的分發(fā)事件源碼
- public boolean dispatchTouchEvent(MotionEvent ev) { if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- onUserInteraction();
- } if (getWindow().superDispatchTouchEvent(ev)) { return true;
- } return onTouchEvent(ev);
- }
在 getWindow().superDispathTouchEvent 就是用來分發(fā)事件到 DecorView 中。如果整個 ViewTree 沒有消費事件,會調(diào)用 Activity 的 onTouchEvent。
1.2. 主要方法
1.2.1. 概覽
主要涉及到的 View 或 ViewGroup 的方法有:
dispatchTouchEvent,該方法封裝了事件分發(fā)的整個過程。是事件分發(fā)的 調(diào)度者 和 指揮官 。的核心過程均在該方法中。下面的 onInterceptTouchEvent 和 onTouchEvent 的回調(diào)的調(diào)用就在該方法體中。是否傳遞事件到
onInterceptTouchEvent 和 onTouchEvent 由 dispatchTouchEvent 決定。
onInterceptTouchEvent,該方法決定了是否攔截事件。只有 ViewGroup 有該回調(diào)。返回 true 表示攔截,返回 false 表示不攔截。自定義 View 的時候,可以重載該方法,通過一些特定的邏輯來決定是否攔截事件。如果攔截,接下來會調(diào)用該 ViewGroup 的 onTouchEvent 來處理事件。
onTouchEvent,該方法處理了事件,并決定是否繼續(xù)消費后續(xù)事件。該方法調(diào)用的前置條件:
- 該 View 攔截了事件
- 子 View 都不消費事件
- 沒有子 View
該方法正式處理 MotionEvent。返回 true 表示消費,返回 false 不消費。如果消費,接下來的事件還會傳遞到該 View 的 dispatchTouchEvent 中;如果不消費,后面的事件不會再傳過來。
onTouchListener 的 onTouch 回調(diào),和 onTouchEvent 一樣,優(yōu)先級比 onTouchEvent 高,如果有設(shè)置該監(jiān)聽,并且 onTouch 返回 true,就不會再調(diào)用 onTouchEvent 了。如果返回 false,事件還是會傳遞到 onTouchEvent 中。
1.2.2. dispatchTouchEvent 方法中的一些細(xì)節(jié)處理:
大部分手勢的起點為 down 事件,dispatchTouchEvent 如果收到 down 事件,會重新設(shè)置一些變量和標(biāo)記
重置變量和標(biāo)記的源碼
- // Handle an initial down.if (actionMasked == MotionEvent.ACTION_DOWN) { // Throw away all previous state when starting a new touch gesture.
- // The framework may have dropped the up or cancel event for the previous gesture
- // due to an app switch, ANR, or some other state change.
- cancelAndClearTouchTargets(ev);
- resetTouchState();
- }
實際的源碼中,ViewGroup 繼承于 View。 當(dāng)子 View 不消費事件或者 ViewGroup 攔截了事件會傳空值到 dispatchTransformedTouchEvent 中,內(nèi)部會調(diào)用 super.dispatchTouchEvent,最終把事件傳給 onTouchEvent 進行處理。
dispatchTransformedTouchEvent 關(guān)鍵部分
- // Perform any necessary transformations and dispatch.if (child == null) {
- handled = super.dispatchTouchEvent(transformedEvent);
- } else { final float offsetX = mScrollX - child.mLeft; final float offsetY = mScrollY - child.mTop;
- transformedEvent.offsetLocation(offsetX, offsetY); if (! child.hasIdentityMatrix()) {
- transformedEvent.transform(child.getInverseMatrix());
- }
- handled = child.dispatchTouchEvent(transformedEvent);
- }
也就是 dispatchTransformTouchEvent 完成了分發(fā)的***過程:
a. 傳入的 child 不為空,轉(zhuǎn)化坐標(biāo)為 child 的坐標(biāo)系,調(diào)用 child.dispatchTouchEvent向 child 分發(fā)事件
b. 傳入的 child 為空,調(diào)用 super.dispatchTouchEvent 分發(fā)事件到 onTouchEvent 中
1.2.3 方法的主要關(guān)系
對于一個 ViewGroup 來說,幾個重要方法的關(guān)系如下
幾個重要方法關(guān)系偽代碼
- public boolean dispatchTouchEvent(MotionEvent e) { boolean consumed = false; if (onInterceptTouchEvent(e)) {
- consumed = onTouchEvent(e);
- } else { for (View view: childs) {
- consumed = view.dispatchTouchEvent(e); if (consumed) { break;
- }
- } if (!consumed) {
- consumed = onTouchEvent(e);
- }
- }
- return consumed;
- }
這是事件分發(fā)過程的簡單描述,具體遠比這復(fù)雜的多。
1.3. 核心行為
View 或 ViewGroup 有兩個核心的行為:攔截(intercept) 和 消費(consume)。這兩者是相互獨立的,攔截不一定消費。是否要攔截看 onIntercepTouchEvent。是否要消費看 onTouchEvent。
注意:是否攔截還有其他因素影響。如果不是 down 事件,并且 mFirstTouchTarget 為空值,就會直接攔截事件。
在 dispatchTouchEvent 中有這樣的代碼
攔截的關(guān)鍵源碼
- // Check for interception.final boolean intercepted;if (actionMasked == MotionEvent.ACTION_DOWN
- || mFirstTouchTarget != null) { final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0; if (!disallowIntercept) {
- intercepted = onInterceptTouchEvent(ev);
- ev.setAction(action); // restore action in case it was changed
- } else {
- intercepted = false;
- }
- } else { // There are no touch targets and this action is not an initial down
- // so this view group continues to intercept touches.
- intercepted = true;
- }
從上面的源碼可以看出,在不是 down 事件,并且 mFirstTouchTarget 為空的情況下,不會走 onInterceptTouchEvent 而是直接攔截。如果滿足了,還會看 FLAG_DISALLOW_INTERCEPT 標(biāo)記,如果不允許攔截(disallowIntercept 為 true),也不會走onInterceptTouchEvent,直接標(biāo)記不攔截。
處理調(diào)用 onTouchEvent 的源碼
- boolean result = false;
- ...
- ListenerInfo li = mListenerInfo;if (li != null && li.mOnTouchListener != null
- && (mViewFlags & ENABLED_MASK) == ENABLED
- && li.mOnTouchListener.onTouch(this, event)) {
- result = true;
- }if (!result && onTouchEvent(event)) {
- result = true;
- }
可以看出,在該 View 為 ENABLE 的狀態(tài)并且有 mTouchListener,會先調(diào)用 onTouch。在onTouch 返回 false 時才會繼續(xù)調(diào)用 onTouchEvent。
onTouch 或者 onTouchEvent 的處理結(jié)果有:
- 返回 true,會繼續(xù)消費后續(xù)事件。意味著,后面的事件將會繼續(xù)傳遞到該 View 的 dispatchTouchEvent 方法中進行調(diào)度。父 View 會為該 View 創(chuàng)建一個 TouchTarget 實例加入鏈表中,鏈表的***項為 mFirstTouchTarget。后續(xù)的 move 和 up 事件會直接交給該 View 的 dispatchTouchEvent。
- 返回 false,不再消費后續(xù)事件。意味著,后面的事件將會被父 View 攔截,而不再傳遞下來。
1.4. 特殊情況
比較特殊的情況有,子 View 可以使用 requestDisallowInterceptTouchEvent 影響去父 View 的分發(fā),可以決定父 View 是否要調(diào)用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用調(diào)用 onInterceptTouchEvent 來判斷攔截,而就是不攔截。
該方法可以用來解決手勢沖突。比如子 View 先消費了事件,但是后面父 View 也滿足了手勢觸發(fā)的條件而攔截事件,導(dǎo)致子 View 手勢執(zhí)行一半后無法繼續(xù)響應(yīng)??梢允褂?requestDisallowInterceptTouchEvent(true),這樣后面的事件,父 View 不會走 onInterceptTouchEvent 回調(diào)來判斷是否要攔截事件,而是直接把事件繼續(xù)傳下來。
2. 案例分析
下面舉三個簡單的例子,三個類 ViewGroup1st,ViewGroup2nd 和 View0,層級關(guān)系為
- <ViewGroup1st>
- <ViewGroup2nd>
- <View0 />
- </ViewGroup2nd></ViewGroup1st>
這三個類有兩層 ViewGroup,***層為 View,這幾個例子主要理解 消費 行為,所以不做事件的攔截。
2.1. 案例1:均不消費 down 事件
在觸摸屏幕中 View0 的區(qū)域后,輸出 log 信息如下
- 12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent before12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent before12-30 14:06:03.694 31323-31323/lyn.demo D/View0: onTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onTouchEvent return:false12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:false
當(dāng) down 事件從 DecorView 開始了分發(fā)過程:
ViewGroup1st 收到事件,執(zhí)行 onInterceptTouchEvent 返回 false,不攔截,于是調(diào)用 ViewGroup2nd 的 dispatchTouchEvent 向 ViewGroup2nd分發(fā)。
ViewGroup2nd 收到事件,dispatchTouchEvent 重復(fù) ViewGroup1st 的分發(fā)策略。因為都不攔截,所以調(diào)用了 View0 的 dispatchTouchEvent。
View0 收到事件,而 View0 不是 ViewGroup 類型,所以把事件直接交給了 onTouchEvent。
View0 不消費事件,onTouchEvent 返回 false,dispatchTouchEvent 方法因此也返回 false。
ViewGroup2nd 因為 View0 的 dispatchTouchEvent 返回 false,確定了子類不消費事件,于是把事件傳遞給 onTouchEvent。但本身也不消費事件,所以 onTouchEvent 也返回 false,繼續(xù)把事件上拋到 ViewGroup1st。
ViewGroup1st 重復(fù)了 ViewGroup2nd 的過程。
隨后,move 事件不會再往下傳了,而是直接被 Activity 攔截。
2.2. 案例2:View0 消費 down 事件
首先是 down 事件的傳遞,log 如下
- 12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent before12-30 14:14:09.384 7350-7350/lyn.demo D/View0: onTouchEvent return:true12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
ViewGroup1st 和 ViewGroup2st 的傳遞和案例1一樣。區(qū)別在于 View0 onTouchEvent 返回 true 消費后續(xù)事件后,View0 的 dispatchTouchEvent 也返回 true,ViewGroup2nd 和 ViewGroup1st 不執(zhí)行 onTouchEvent 也直接返回 true
然后稍微移動一下手指,move 事件往下傳遞
- 12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent before12-30 14:14:09.484 7350-7350/lyn.demo D/View0: onTouchEvent return:true12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
過程和 down 事件的傳遞一樣。因為同樣會經(jīng)過 ViewGroup2nd 的 onInterceptTouchEvent,如果這時候 ViewGroup2nd 有攔截行為,move 事件就不會傳到 View0 了。要避免這種情況發(fā)生,需要調(diào)用 View0 的requestDisallowInterceptTouchEvent,可見 1.4 部分。
2.3. 案例3:ViewGroup2nd 消費 down 事件
首先是 down 事件的傳遞,log 如下
- 12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent before12-30 14:25:30.084 18848-18848/lyn.demo D/View0: onTouchEvent return:false12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent return:false12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
由于 View0 不消費事件,dispatchTouchEvent 返回 false,所以執(zhí)行了 ViewGroup2nd 的 onTouchEvent 方法。
ViewGroup2nd 消費事件,onTouchEvent 返回 true,之后 ViewGroup2nd 和 ViewGroup1st 的 dispatchTouchEvent 均返回 true。
動一下手指,move 事件接著傳
- 2-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
這時候,ViewGroup2nd 直接攔截了 move 事件,不再經(jīng)過 onInterceptTouchEvent,也不再向 View0 分發(fā),而是直接調(diào)用 onTouchEvent 進行處理。
3. down 事件分發(fā)圖
在每個 View 都不攔截 down 事件的情況下,down 事件是這樣傳遞的