事件分發(fā)過程的理解
1.1. 概述
1.2. 主要方法
1.3. 核心行為
1.4. 特殊情況案例分析
2.1. 案例1:均不消費(fèi) down 事件
2.2. 案例2:View0 消費(fèi) down 事件
2.3. 案例3:ViewGroup2nd 消費(fèi) down 事件down 事件分發(fā)圖
1. 事件分發(fā)過程的理解
1.1. 概述
事件主要有 down (MotionEvent.ACTION_DOWN),move(MotionEvent.ACTION_MOVE),up(MotionEvent.ACTION_UP)。
基本上的手勢(shì)均由 down 事件為起點(diǎn),up 事件為終點(diǎn),中間可能會(huì)有一定數(shù)量的 move 事件。這三種事件是大部分手勢(shì)動(dòng)作的基礎(chǔ)。
事件和相關(guān)信息(比如坐標(biāo))封裝成 MotionEvent。
大體的分發(fā)過程為:首先傳遞到 Activity,然后傳給了 Activity 依附的 Window,接著由 Window 傳給視圖的頂層 View 也就是 DecorView,最后由 DecorView 向整個(gè) ViewTree 分發(fā)。分發(fā)還會(huì)有回溯的過程。最后還會(huì)回到 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 中。如果整個(gè) ViewTree 沒有消費(fèi)事件,會(huì)調(diào)用 Activity 的 onTouchEvent。
1.2. 主要方法
1.2.1. 概覽
主要涉及到的 View 或 ViewGroup 的方法有:
dispatchTouchEvent,該方法封裝了事件分發(fā)的整個(gè)過程。是事件分發(fā)的 調(diào)度者 和 指揮官 。的核心過程均在該方法中。下面的 onInterceptTouchEvent 和 onTouchEvent 的回調(diào)的調(diào)用就在該方法體中。是否傳遞事件到
onInterceptTouchEvent 和 onTouchEvent 由 dispatchTouchEvent 決定。
onInterceptTouchEvent,該方法決定了是否攔截事件。只有 ViewGroup 有該回調(diào)。返回 true 表示攔截,返回 false 表示不攔截。自定義 View 的時(shí)候,可以重載該方法,通過一些特定的邏輯來決定是否攔截事件。如果攔截,接下來會(huì)調(diào)用該 ViewGroup 的 onTouchEvent 來處理事件。
onTouchEvent,該方法處理了事件,并決定是否繼續(xù)消費(fèi)后續(xù)事件。該方法調(diào)用的前置條件:
- 該 View 攔截了事件
- 子 View 都不消費(fèi)事件
- 沒有子 View
該方法正式處理 MotionEvent。返回 true 表示消費(fèi),返回 false 不消費(fèi)。如果消費(fèi),接下來的事件還會(huì)傳遞到該 View 的 dispatchTouchEvent 中;如果不消費(fèi),后面的事件不會(huì)再傳過來。
onTouchListener 的 onTouch 回調(diào),和 onTouchEvent 一樣,優(yōu)先級(jí)比 onTouchEvent 高,如果有設(shè)置該監(jiān)聽,并且 onTouch 返回 true,就不會(huì)再調(diào)用 onTouchEvent 了。如果返回 false,事件還是會(huì)傳遞到 onTouchEvent 中。
<h4 id="1.2.2"> 1.2.2. dispatchTouchEvent 方法中的一些細(xì)節(jié)處理:</h4>
大部分手勢(shì)的起點(diǎn)為 down 事件,dispatchTouchEvent 如果收到 down 事件,會(huì)重新設(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();
}
實(shí)際的源碼中,ViewGroup 繼承于 View。 當(dāng)子 View 不消費(fèi)事件或者 ViewGroup 攔截了事件會(huì)傳空值到 dispatchTransformedTouchEvent 中,內(nèi)部會(huì)調(diào)用 super.dispatchTouchEvent,最終把事件傳給 onTouchEvent 進(jìn)行處理。
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 中
<h4 id="1.2.3"> 1.2.3 方法的主要關(guān)系 </h4>
對(duì)于一個(gè) ViewGroup 來說,幾個(gè)重要方法的關(guān)系如下
幾個(gè)重要方法關(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ā)過程的簡單描述,具體遠(yuǎn)比這復(fù)雜的多。
1.3. 核心行為
View 或 ViewGroup 有兩個(gè)核心的行為:攔截(intercept) 和 消費(fèi)(consume)。這兩者是相互獨(dú)立的,攔截不一定消費(fèi)。是否要攔截看 onIntercepTouchEvent。是否要消費(fèi)看 onTouchEvent。
注意:是否攔截還有其他因素影響。如果不是 down 事件,并且 mFirstTouchTarget 為空值,就會(huì)直接攔截事件。
在 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 為空的情況下,不會(huì)走 onInterceptTouchEvent 而是直接攔截。如果滿足了,還會(huì)看 FLAG_DISALLOW_INTERCEPT 標(biāo)記,如果不允許攔截(disallowIntercept 為 true),也不會(huì)走 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,會(huì)先調(diào)用 onTouch。在 onTouch 返回 false 時(shí)才會(huì)繼續(xù)調(diào)用 onTouchEvent。
onTouch 或者 onTouchEvent 的處理結(jié)果有:
- 返回 true,會(huì)繼續(xù)消費(fèi)后續(xù)事件。意味著,后面的事件將會(huì)繼續(xù)傳遞到該 View 的
dispatchTouchEvent方法中進(jìn)行調(diào)度。父 View 會(huì)為該 View 創(chuàng)建一個(gè) TouchTarget 實(shí)例加入鏈表中,鏈表的第一項(xiàng)為 mFirstTouchTarget。后續(xù)的 move 和 up 事件會(huì)直接交給該 View 的dispatchTouchEvent。 - 返回 false,不再消費(fèi)后續(xù)事件。意味著,后面的事件將會(huì)被父 View 攔截,而不再傳遞下來。
1.4. 特殊情況
比較特殊的情況有,子 View 可以使用 requestDisallowInterceptTouchEvent 影響去父 View 的分發(fā),可以決定父 View 是否要調(diào)用 onInterceptTouchEvent 。比如,requestDisallowInterceptTouchEvent(true),父 View 就不用調(diào)用 onInterceptTouchEvent 來判斷攔截,而就是不攔截。
該方法可以用來解決手勢(shì)沖突。比如子 View 先消費(fèi)了事件,但是后面父 View 也滿足了手勢(shì)觸發(fā)的條件而攔截事件,導(dǎo)致子 View 手勢(shì)執(zhí)行一半后無法繼續(xù)響應(yīng)??梢允褂?requestDisallowInterceptTouchEvent(true),這樣后面的事件,父 View 不會(huì)走 onInterceptTouchEvent 回調(diào)來判斷是否要攔截事件,而是直接把事件繼續(xù)傳下來。
2. 案例分析
下面舉三個(gè)簡單的例子,三個(gè)類 ViewGroup1st,ViewGroup2nd 和 View0,層級(jí)關(guān)系為
<ViewGroup1st>
<ViewGroup2nd>
<View0 />
</ViewGroup2nd>
</ViewGroup1st>
這三個(gè)類有兩層 ViewGroup,最底層為 View,這幾個(gè)例子主要理解 消費(fèi) 行為,所以不做事件的攔截。
2.1. 案例1:均不消費(fèi) down 事件
在觸摸屏幕中 View0 的區(qū)域后,輸出 log 信息如下
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/View0: dispatchTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: onTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:false
12-30 14:06:03.694 31323-31323/lyn.demo D/ViewGroup1st: onTouchEvent return:false
12-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ā)策略。因?yàn)槎疾粩r截,所以調(diào)用了 View0 的 dispatchTouchEvent。
View0 收到事件,而 View0 不是 ViewGroup 類型,所以把事件直接交給了 onTouchEvent。
View0 不消費(fèi)事件,onTouchEvent 返回 false,dispatchTouchEvent 方法因此也返回 false。
ViewGroup2nd 因?yàn)?View0 的 dispatchTouchEvent 返回 false,確定了子類不消費(fèi)事件,于是把事件傳遞給 onTouchEvent。但本身也不消費(fèi)事件,所以 onTouchEvent 也返回 false,繼續(xù)把事件上拋到 ViewGroup1st。
ViewGroup1st 重復(fù)了 ViewGroup2nd 的過程。
隨后,move 事件不會(huì)再往下傳了,而是直接被 Activity 攔截。
2.2. 案例2:View0 消費(fèi) down 事件
首先是 down 事件的傳遞,log 如下
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: onTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:14:09.384 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
ViewGroup1st 和 ViewGroup2st 的傳遞和案例1一樣。區(qū)別在于 View0 onTouchEvent 返回 true 消費(fèi)后續(xù)事件后,View0 的 dispatchTouchEvent 也返回 true,ViewGroup2nd 和 ViewGroup1st 不執(zhí)行 onTouchEvent 也直接返回 true
然后稍微移動(dòng)一下手指,move 事件往下傳遞
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: onTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/View0: dispatchTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:14:09.484 7350-7350/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
過程和 down 事件的傳遞一樣。因?yàn)橥瑯訒?huì)經(jīng)過 ViewGroup2nd 的 onInterceptTouchEvent,如果這時(shí)候 ViewGroup2nd 有攔截行為,move 事件就不會(huì)傳到 View0 了。要避免這種情況發(fā)生,需要調(diào)用 View0 的requestDisallowInterceptTouchEvent,可見 1.4 部分。
2.3. 案例3:ViewGroup2nd 消費(fèi) down 事件
首先是 down 事件的傳遞,log 如下
12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:25:30.074 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onInterceptTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent before
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: onTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/View0: dispatchTouchEvent return:false
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:25:30.084 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
由于 View0 不消費(fèi)事件,dispatchTouchEvent 返回 false,所以執(zhí)行了 ViewGroup2nd 的 onTouchEvent 方法。
ViewGroup2nd 消費(fèi)事件,onTouchEvent 返回 true,之后 ViewGroup2nd 和 ViewGroup1st 的 dispatchTouchEvent 均返回 true。
動(dòng)一下手指,move 事件接著傳
2-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent before
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: onInterceptTouchEvent return:false
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent before
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: onTouchEvent return:true
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup2nd: dispatchTouchEvent return:true
12-30 14:25:30.174 18848-18848/lyn.demo D/ViewGroup1st: dispatchTouchEvent return:true
這時(shí)候,ViewGroup2nd 直接攔截了 move 事件,不再經(jīng)過 onInterceptTouchEvent,也不再向 View0 分發(fā),而是直接調(diào)用 onTouchEvent 進(jìn)行處理。
3. down 事件分發(fā)圖
在每個(gè) View 都不攔截 down 事件的情況下,down 事件是這樣傳遞的
