[toc]
問題
在進行正文之前,我們帶著以下幾個問題有目的的進行,然後最後再做問題的解決。
- 問題 1:activity、 ViewGroup和 View 都不消費 ACTION_DOWN,那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
- 問題 2:在 ViewGroup 中的 onTouchEvent 中消費 ACTION_DOWN 事件(onInterceptTouch 預設設定),那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
- 在一個列表中,同時對父 View 和子 View 設定點選方法,優先響應哪個?為什麼會這樣?
- 為什麼子 View 不消費 ACTION_DOWN,之後的所有事件都不會向下傳遞了。
基礎認識
事件分發的物件
首先我們要清楚,事件分發的物件是什麼?其實就 MotionEvent,這個 MotionEvent 可以有 ACTION_DOWN,ACTION_UP,ACTION_MOVE,ACTION_CANCEL 等事件型別。
事件(MotionEvent)分發是在哪些物件中進行的?
Activity -> Window -> ViewGroup -> View
重點關注的方法
- dispatchTouchEvent
- onInterceptTouchEvent
- onTouchEvent
ACTION_DOWN 事件在 Activity、ViewGroup、View 中根據不同函式不同的返回值的走向?
關於事件的走向,我覺的以下這張圖可以很清晰的看出事件的最終走向,該圖來自Kelin
注:流程圖來之 Kelin以下是使用文字對事件走向的描述,幫助對流程圖的理解。
方法 | Activity | ViewGroup | View |
---|---|---|---|
dispatchTouchEvent | (1) return false 或者是 return true,表示 Activity 已經消費 (2) return 父類的 super.dispatchTouchEvent() 表示向下傳遞,最後結果由下一級決定 |
(1) return false,表示 ViewGroup 不消費事件,返回給上一級的 ViewGroup 或者 Activity 的 onTouchEvent 進行處理; (2) return true,表示當前 ViewGroup 已經消費了事件,事件在此終止; (3) return super.dispatchTouchEvent(),表示事件繼續,根據 onInterceptTouchEvent 判斷是否自己處理事件(是否呼叫 onTouchEvent) |
(1) return false,表示 View 不消費事件,返回給上一級的 ViewGroup onTouchEvent 進行處理; (2) return true,表示當前 View 已經消費了事件,事件在此終止; (3) return super.dispatchTouchEvent(),表示事件繼續,並呼叫自己的方法 onTouchEvent |
onInterceptTouchEvent | 沒有此方法 | (1) return true,表示 ViewGroup 攔截該事件;之後 onInterceptTouchEvent 不會再被呼叫 (2) return false 或者 return super.onInterceptTouchEvent 表示不攔截事件,將事件交給子 view 處理。 |
沒有此方法 |
onTouchEvent | (1) return false,表示所有的 ViewGroup 和 View 都不消費事件,Activity 也不消費; (2) return true,表示消費該事件,事件在此終止。 |
(1) return false 或者是 return super.onTouchEvent 表示不消費事件,返回給上一級的 onTouchEvent 處理 (2) return true 表示當前 ViewGroup 自己消費了,事件在此終止,不會往上傳也不會往下傳了。 |
(1) return false 或者是 return super.onTouchEvent 表示不消費事件,返回給上一級的 ViewGroup 進行護理; (2) return true 表示當前 view 進行處理事件,事件在此終止 |
結論
- dispatchTouchEvent 和 onTouchEvent 中返回 true,ACTION_DOWN 事件就在此終止,不會再往上傳也不會往下傳了。
- dispatchTouchEvent 和 onTouchEvent 中返回 false,ACTION_DOWN 事件交給父控制元件的 onTouchEvent 進行處理
- onInterceptTouchEvent 一旦返回 true,那麼 ViewGroup 之後不會再呼叫 onInterceptTouchEvent
事件分發原始碼分析
Activity
一旦事件產生,那麼首先被呼叫的是 Activity 中的 dispatchTouchEvent 方法。我們來看看這個方法的實現。
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
複製程式碼
首先,如果是 ACTION_DOWN,那麼 onUserInteraction 方法就會被呼叫,onUserInteraction 是個空的方法,當事件產生時,那麼這個方法就會被呼叫,如果在 activity 執行的時候,我們想要知道使用者和裝置的互動,那麼我們就可以實現這個方法。
接著 window 中的 superDispatchTouchEvent 方法被呼叫,事件傳遞到 window 中。Window 是個抽象類,他的唯一實現是 PhoneWindow。在 Window.superDispatchTouchEvent 中呼叫了 DecorView 的 superDispatchTouchEvent,而這個 DecorView 就是我們在 activity 中通過呼叫 setContentView 設定的佈局的頂層 View。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
複製程式碼
最後,如果 Activity 中的所有下級事件承載物件沒有處理事件,最後 Activity 中的 onTouchEvent 就會被呼叫,當事件超出邊界或者事件為 ACTION_DOWN 時,mWindow.shouldCloseOnTouch(this, event) 為 true,onTouchEvent 預設是返回 false 的。
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
複製程式碼
onTouchEvent 方法 當所有的 view 都沒有消費事件的時候,activity 的 onTouchEvent() 就會被呼叫
我們這邊看一下 getWindow() 裡面的實現,其實也就是返回一個 Window 例項
public Window getWindow() {
return mWindow;
}
複製程式碼
Window(PhoneWindow)
Window 在事件分發的過程中就類似於一箇中間的橋接一樣,是沒有做什麼操作的,只是將事件傳遞給 DecorView 中。
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
return mDecor.superDispatchTouchEvent(event);
}
複製程式碼
ViewGroup
- 要理解 ViewGroup 對於事件的處理,我們先來個簡化的程式碼邏輯來幫助我們理解
void dispathTouchEvent(Event event){
boolean consume = false;
if(OnInterceptTouchEvent(event)){
consume = onTouchEvent(event);
}else{
consume = childView.dispatchTouchEvent(event);
}
return consume;
}
複製程式碼
對於一個 ViewGroup 來說,點選事件產生之後,dispatchTouchEvent 就會被呼叫,如果這個 ViewGroup 的 onInterceptTouchEvent 返回 true 就表示它要攔截當前的事件,接著事件就會給這個 ViewGroup 處理,即它的 onTouchEvent 會被呼叫;如果 ViewGroup 的 onInterceptTouchEvent 返回 false,就表示它不攔截事件,這時當前事件就會被傳遞給它的子 view,接著呼叫子元素的 dispatchTouchEvent 方法就會被呼叫,如此反覆,直到事件被最終處理。
- 大致流程圖
- 我們再看看 ViewGroup.dispatchTouchEvent() 還原度比較高的原始碼
// 省略部分程式碼
// 如果是 ACTION_DOWN 事件,重置標誌位 mGroupFlags 為 非 FLAG_DISALLOW_INTERCEPT,這個標誌位關係到 ViewGroup 的 onInterceptTouch 是否有效。
if (actionMasked == MotionEvent.ACTION_DOWN) {
cancelAndClearTouchTargets(ev);
resetTouchState();
}
// 檢查是否攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 處理條件為:
// 1. 事件為 ACTION_DOWN
// 2. 有下級的 View 處理事件
// 判斷攔截是否失效?mGroupFlags = FLAG_DISALLOW_INTERCEPT 時,onInterceptTouchEvent 是不會被呼叫的
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 {
// 非 ACTION_DOWN 事件且沒有其他的下級處理該事件的時候,不會再呼叫 onInterceptTouchEvent
intercepted = true;
}
// 正常事件分發
// 如果 ViewGroup 決定攔截或者已經有 子 view 處理事件,那麼就開始正常的事件分發流程
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
// 不攔截事件
if (!canceled && !intercepted) {
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final View[] children = mChildren;
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// 找到接收該事件的子 View 上,如果找到,則直接跳出迴圈
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
// 判斷事件是否落在子 view 上,如果是,則跳出迴圈
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// Child wants to receive touch within its bounds.
mLastTouchDownTime = ev.getDownTime();
if (preorderedList != null) {
// childIndex points into presorted list, find original index
for (int j = 0; j < childrenCount; j++) {
if (children[childIndex] == mChildren[j]) {
mLastTouchDownIndex = j;
break;
}
}
} else {
mLastTouchDownIndex = childIndex;
}
mLastTouchDownX = ev.getX();
mLastTouchDownY = ev.getY();
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
// The accessibility focus didn't handle the event, so clear
// the flag and do a normal dispatch to all children.
ev.setTargetAccessibilityFocus(false);
}
if (preorderedList != null) preorderedList.clear();
}
}
}
複製程式碼
從以上的程式碼中可以看出,當事件傳遞到 ViewGroup 中的 dispatchTouchEvent 的時候,這次經歷了以下的幾個重要步驟:
- step 1: 重置標誌位 mGroupFlags,將 mGroupFlags 標誌位設定為非 FLAG_DISALLOW_INTERCEPT,如果 mGroupFlags = FLAG_DISALLOW_INTERCEPT,那麼 ViewGroup 將不再呼叫 onInterceptTouch(),預設 ViewGroup 不攔截任何事件。
- step 2: 通過判斷事件是否為 ACTION_DOWN 或 mFirstTouchTarget != null 來決定是否向下分發 ACTION_DOWN 之外的事件;
- 若是子 View 消費 ACTION_DOWN,那麼 mFirstTouchTarget 會被賦值,mFirstTouchTarget != null 不成立
- 若是子 View 不消費 ACTION_DOWN,那麼 mFirstTouchTarget 則為 null,ViewGroup 預設攔截 ACTION_DOWN 之後的所有事件,不向下傳遞。
- step 3:通過 mGroupFlags 標誌位判斷攔截是否有效,若是 mGroupFlags = FLAG_DISALLOW_INTERCEPT,則 ViewGroup 預設不攔截任何事件。
- step 4: 迴圈所有的子 view
- (1)判斷是否有子 view 已經處理改事件了,如果有則跳出迴圈,直接向下級子 view 分發事件。
- (2)判斷點選是否落在某個子 view;
- step 5: 如果子 view 消費了事件,那麼將 mFirstTouchTarget 進行賦值,mFirstTouchTarget(連結串列)。
View
View 對於事件的處理要稍微簡單一點,注意這裡的 View 並不包含 ViewGroup。我們先看看 dispatchTouchEvent 方法。
public boolean dispatchTouchEvent(MotionEvent event) {
...
boolean result = false;
if (onFilterTouchEventForSecurity(event)) {
if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
result = true;
}
//noinspection SimplifiableIfStatement
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;
}
}
...
return result;
}
複製程式碼
View 對於點選事件的處理就比較簡單了,因為 View 是個單獨的元素,它沒有子元素,因此無法向下傳遞事件,所以它只能自己處理。
從上面的原始碼中,我們可以看出 View 對點選事件的處理過程,首先會判斷有沒有設定 onTouchListener,如果 onTouchListener 中的 onTouch 返回了 true,那麼 onTouchEvent 就不會再被呼叫,可見 onTouchListener 的優先順序要高於 onTouchEvent,這樣的處理是方便點選事件在外界進行處理。
- View 中的 onTouchEvent 的實現
public boolean onTouchEvent(MotionEvent event) {
// 不可用狀態下,View 依然會消耗點選事件
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
return clickable;
}
// 如果設定了代理,那麼就設定代理的方法
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
//
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
if (!focusTaken) {
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
....
}
break;
}
return true;
}
return false;
}
複製程式碼
從上面的程式碼中可以看出,只要 View 的 CLICKABLE 和 LONG_CLICK 有一個為 true,那麼它就會消耗這個事件,即 onTouchEvent 方法返回 true,不管它是不是 DISABLE 狀態。
然後當 ACTION_UP 事件發生的時候,會觸發 performClick 方法,如果 View 設定了 onClickListener,那麼 performClick 方法內部就會呼叫它的 onClick 方法。如下所示:
public boolean performClick() {
final boolean result;
final ListenerInfo li = mListenerInfo;
if (li != null && li.mOnClickListener != null) {
playSoundEffect(SoundEffectConstants.CLICK);
li.mOnClickListener.onClick(this);
result = true;
} else {
result = false;
}
sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
notifyEnterOrExitForAutoFillIfNeeded(true);
return result;
}
複製程式碼
到這裡,點選事件的原始碼分析就結束了。
問題解答
問題 1:activity、 ViewGroup和 View 都不消費 ACTION_DOWN,那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
- 首先,如果大家都不消費 ACTION_DOWN,那麼 ACTION_DOWN 的事件傳遞流程是這樣的:
-> Activity.dispatchTouchEvent()
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent()
-> view1.onTouchEvent()
-> ViewGroup1.onTouchEvent()
-> Activity.onTouchEvent();
複製程式碼
- 接著,由於大家都不消費 ACTION_DOWN,對於 ACTION_MOVE 和 ACTION_UP 的事件傳遞是這樣的
-> Activity.dispatchTouchEvent()
-> Activity.onTouchEvent();
-> 消費
複製程式碼
- 完整的事件分發走向
問題 2:在 ViewGroup 中的 onTouchEvent 中消費 ACTION_DOWN 事件(onInterceptTouch 預設設定),那麼 ACTION_MOVE 和 ACTION_UP 事件是怎麼傳遞的?
- 首先,我們先分析一下 ACTION_DOWN 的事件走向,由於 ViewGroup 中的 onInterceptTouch 是預設設定的,那麼 ACTION_DOWN 的事件最終在 ViewGroup 中的 onTouchEvent 方法中停止了,事件走向是這樣的:
-> Activity.dispatchTouchEvent()
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onInterceptTouchEvent()
-> view1.dispatchTouchEvent()
-> view1.onTouchEvent()
-> ViewGroup1.onTouchEvent()
複製程式碼
- 接著 ACTION_MOVE 和 ACTION_UP 的事件分發流程,之後 onInterceptTouch 和 View 中的方法都不會被呼叫了,事件分發如下:
-> Activity.dispatchTouchEvent()
-> ViewGroup1.dispatchTouchEvent()
-> ViewGroup1.onTouchEvent()
複製程式碼
- 完整的事件分發走向
在一個列表中,同時對父 View 和子 View 設定點選方法,優先響應哪個?為什麼會這樣?
答案是優先響應子 view,原因很簡單,如果先響應父 view,那麼子 view 將永遠無法響應,父 view 要優先響應事件,必須先呼叫 onInterceptTouchEvent 對事件進行攔截,那麼事件不會再往下傳遞,直接交給父 view 的 onTouchEvent 處理。
為什麼子 View 不消費 ACTION_DOWN,之後的所有事件都不會向下傳遞了。
答案是:mFirstTouchTarget。當子 view 對事件進行處理的時,那麼 mFirstTouchTarget 就會被賦值,若是子 view 不對事件進行處理,那麼 mFirstTouchTarget 就為 null,之後 VIewGroup 就會預設攔截所有的事件。我們可以從 dispatchTouchEvent 中找到如下程式碼,可以看出來,若是子 View 不處理 ACTION_DOWN,那麼之後的事件也不會給到它了。
// 檢查是否攔截
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 省略和問題無關程式碼
} else {
// 預設攔截
intercepted = true;
}
複製程式碼
作者介紹
- 陳堅潤:廣州蘆葦科技 APP 團隊 Android 開發工程師
內推資訊
- 我們正在招募小夥伴,有興趣的小夥伴可以把簡歷發到 app@talkmoney.cn,備註:來自掘金社群
- 詳情可以戳這裡--> 廣州蘆葦資訊科技