Android View 的事件體系 -- 事件分發機制
文章是閱讀了《Android 開發藝術探索》這本書較詳細的理解了事件分發機制後,再通過研究原始碼,系統性的總結事件分發機制,主要目的是為了更好的理解對同一系列事件(DOWN --> MOVE --> UP),它的分發過程是怎麼樣的,所以文章的講解要比書上的細,要更深入些(^_^)。
文中原始碼使用的是 API 26,因為新版本的 Studio 不能開啟 API 19 的原始碼了,所以就直接用 API 26 了(^_^)。
文中配有流程圖,結合流程圖,大家可以更好的理解,和記憶(對我而言後者更重要)(^_^)。另外本文對原始碼的解析顆粒度會很小,所以流程圖並不會把所有原始碼包含進來。
本來是想直接寫在《Android View 的事件體系》這篇部落格中,但發現這部分內容太多了,就獨立出來了。如果對 Android 事件完全沒有接觸的話,可以先看下事件體系部落格(只要看到事件分發機制部分即可)。
前言
所謂的點選事件分發,其實就是對 MotionEvent 事件的分發過程,即一個 MotionEvent 產生以後,系統需要把這個事件傳遞給具體的View,而這個傳遞過程就是分發過程。
目錄(相容稀土掘金)
- 一、三個重要方法
- 二、事件分發機制簡略過程圖
- 三、從 Activity 到 View 的事件傳遞
- 四、View 的事件傳遞
- 流程圖
- dispatchTouchEvent:全部原始碼
- onTouchEvent:整體原始碼(簡述)
- onTouchEvent:clickable 判定
- onTouchEvent:View 不可用
- onTouchEvent:存在代理
- onTouchEvent:點選事件
- performClick:全部原始碼
- 五、ViewGroup 的事件傳遞
- 流程圖
- onInterceptTouchEvent:決定 ViewGroup 是否攔截事件。
- onTouchEvent:ViewGroup 自身嘗試消費事件。
- dispatchTouchEvent:整體原始碼(簡述)
- dispatchTouchEvent:攔截判定模組(核心)
- dispatchTouchEvent:ACTION_DOWN 重置
- dispatchTouchEvent:ViewGroup 自身消費事件(核心)
- dispatchTouchEvent:子 View 消費事件(核心)
- dispatchTouchEvent:遍歷所有子 View(簡述)
- dispatchTouchEvent:子 View 不能接受事件判定
- dispatchTouchEvent:尋找方式一:直接從連結串列中找到可消費該事件的子 View
- dispatchTouchEvent:尋找方式二:事件被子 View 消費(DOWN 事件)
- 六、滑動衝突
- requestDisallowInterceptTouchEvent:修改 mGroupFlags
一、三個重要方法
// Activity
boolean dispatchTouchEvent(MotionEvent event) // 事件分發
boolean onTouchEvent(MotionEvent event) // 事件處理
// View:
boolean dispatchTouchEvent(MotionEvent event) // 事件分發
boolean onTouchEvent(MotionEvent event) // 事件處理
// ViewGoup:
boolean onInterceptTouchEvent(MotionEvent event) // 事件攔截
boolean dispatchTouchEvent(MotionEvent event) // 事件分發
boolean onTouchEvent(MotionEvent event) // 事件處理
複製程式碼
返回值說明:
- true: 事件被攔截,事件處理到此為止,不再繼續分發
- false: 事件未被攔截,繼續往下/上一個控制元件傳遞
二、事件分發機制簡略過程圖
通過上圖,可得:
- View:OnTouchListener --> onTouchEvent --> OnClickListener
- View 是可點選的,則事件就會被該 View 消費
三、從 Activity 到 View 的事件傳遞
/**
* Activity 類
*/
public boolean dispatchTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
onUserInteraction();
}
if (getWindow().superDispatchTouchEvent(ev)) {
return true;
}
return onTouchEvent(ev);
}
public boolean onTouchEvent(MotionEvent event) {
if (mWindow.shouldCloseOnTouch(this, event)) {
finish();
return true;
}
return false;
}
/**
* Window 類
*/
public abstract boolean superDispatchTouchEvent(MotionEvent event);
/**
* PhoneWindow extends Window 類
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
/**
* DecorView 類
*/
public boolean superDispatchTouchEvent(MotionEvent event) {
return super.dispatchTouchEvent(event);
}
複製程式碼
四、View 的事件傳遞
從下面的流程圖可知:
- OnTouchListener --> onTouchEvent --> OnClickListener
- View 可點選,無論 View 是否可用,都會消費事件
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;
}
複製程式碼
onTouchEvent:整體原始碼(簡述)
從原始碼可知:View 可點選,無論 View 是否可用,都會消費事件
public boolean onTouchEvent(MotionEvent event) {
...
// clickable 判定
final boolean clickable = ...
// View 不可用處理
if ((viewFlags & ENABLED_MASK) == DISABLED) {
...
return clickable;
}
// 存在代理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
// View 可點選
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!clickable) {
...
break;
}
...
// 點選事件處理
if(...)
performClick();
...
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
return false;
}
複製程式碼
onTouchEvent:clickable 判定
final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
|| (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
|| (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
複製程式碼
通過 setOnClickListener、setOnLongClickListener、setOnContextClickListener 等方法設定 View 的點選事件,都會使 View 的 clickable 屬性等於 true
public void setOnClickListener(@Nullable OnClickListener l) {
if (!isClickable()) {
setClickable(true);
}
getListenerInfo().mOnClickListener = l;
}
複製程式碼
public void setContextClickable(boolean contextClickable) {
setFlags(contextClickable ? CONTEXT_CLICKABLE : 0, CONTEXT_CLICKABLE);
}
複製程式碼
void setFlags(int flags, int mask) {
...
mViewFlags = (mViewFlags & ~mask) | (flags & mask);
...
}
複製程式碼
onTouchEvent:View 不可用
從原始碼可知: 一旦 View 不可用,流程就會結束,返回值為 clickable
if ((viewFlags & ENABLED_MASK) == DISABLED) {
if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
setPressed(false);
}
mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
// A disabled view that is clickable still consumes the touch
// events, it just doesn't respond to them.
return clickable;
}
複製程式碼
onTouchEvent:存在代理
if (mTouchDelegate != null) {
if (mTouchDelegate.onTouchEvent(event)) {
return true;
}
}
複製程式碼
onTouchEvent:點選事件
從原始碼可知:
- MotionEvent.ACTION_UP 事件觸發 performClick() 方法的執行
- View 只要可點選,那麼當前 View 就會消費掉這個事件
if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
switch (action) {
case MotionEvent.ACTION_UP:
...
if (!clickable) {
...
break;
}
boolean prepressed = (mPrivateFlags & PFLAG_PREPRESSED) != 0;
if ((mPrivateFlags & PFLAG_PRESSED) != 0 || prepressed) {
// take focus if we don't have it already and we should in
// touch mode.
boolean focusTaken = false;
...
if (!mHasPerformedLongPress && !mIgnoreNextUpEvent) {
// This is a tap, so remove the longpress check
removeLongPressCallback();
// Only perform take click actions if we were in the pressed state
if (!focusTaken) {
// Use a Runnable and post this rather than calling
// performClick directly. This lets other visual state
// of the view update before click actions start.
if (mPerformClick == null) {
mPerformClick = new PerformClick();
}
if (!post(mPerformClick)) {
performClick();
}
}
}
...
}
mIgnoreNextUpEvent = false;
break;
case MotionEvent.ACTION_DOWN:
...
break;
case MotionEvent.ACTION_CANCEL:
...
break;
case MotionEvent.ACTION_MOVE:
...
break;
}
return true;
}
複製程式碼
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;
}
複製程式碼
五、ViewGroup 的事件傳遞
必要說明:
- 一般情況下,ViewGroup 會遍歷所有子 View,嘗試讓某個子 View 消費事件。僅當所有子 View 都不消費該事件時,ViewGroup 自身才會嘗試消費該事件
- 攔截指 ViewGroup 不再讓事件遍歷經過子 View,直接由自身嘗試消費該事件
圖片待補充:
onInterceptTouchEvent:決定 ViewGroup 是否攔截事件。
由原始碼可知:預設情況下 ViewGroup 不攔截事件(除特殊情況)
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
&& ev.getAction() == MotionEvent.ACTION_DOWN
&& ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
&& isOnScrollbarThumb(ev.getX(), ev.getY())) {
return true;
}
return false;
}
複製程式碼
onTouchEvent:ViewGroup 自身嘗試消費事件。
從原始碼可知,ViewGroup 並沒有重寫該方法。
dispatchTouchEvent:整體原始碼(簡述)
先讓大家對整體原始碼有個劃分
public boolean dispatchTouchEvent(MotionEvent ev) {
// ACTION_DOWN 重置標誌(程式碼見下)
// 攔截判定模組(程式碼見下)
// ACTION_DOWN 事件時,遍歷所有子 View,尋找能消費該事件的子 View(程式碼見下)
// ViewGroup 自身消費事件,或子 View 消費事件(程式碼見下)
}
複製程式碼
dispatchTouchEvent:攔截判定模組(核心)
注:說明很長,請先看下原始碼,在對照說明一起看。
必要說明(以下說明具體原因見之後的原始碼):
- 1)boolean intercepted = true 表示當前事件被 ViewGroup 攔截,不再傳遞給子 View,交由 ViewGroup 自身處理該事件。
- 2)當 ViewGroup 的某個子 View 消費了事件後,mFirstTouchTarget 就指向該子 View。否則為 null
- 3)ACTION_DOWN 事件會先對 mGroupFlags、mFirstTouchTarget 等進行重置,再執行後續邏輯
由上述可知,對同一系列事件(DOWN、MOVE、UP):
- 4)mFirstTouchTarget != null,表示上一個事件由某個子 View 消費
- 5)mFirstTouchTarget == null,表示上一個事件由 ViewGroup 自身處理,出現有場景:
- ViewGroup 沒有子 View
- ViewGroup 所有子 View 都不消費事件
- ViewGroup 的 onInterceptTouchEvent 方法一直返回 true
以下原始碼可知:
- 6)當事件不是 ACTION_DOWN,且 mFirstTouchTarget == null 時,表示 ViewGroup 繼續由自身處理事件(關聯第2點、第5點)
- 7)boolean disallowIntercept = false,表示 ViewGroup 可能會攔截事件。反之則表示 ViewGroup 不會攔截事件
- 8)事件為 ACTION_DOWN 時:
- onInterceptTouchEvent 必然被執行(關聯第3點)
- 該事件要麼被某個可點選的子 View 消費,要麼 ViewGroup 自身消費(關聯 View 的事件傳遞)
- 一旦該事件被某個可點選的子 View 消費,則 mFirstTouchTarget != null,反之 mFirstTouchTarget == null(關聯第2點)
- 9)連結串列 mFirstTouchTarget != null 時:
- mGroupFlags 不發生改變的情況下,當前事件將繼續被連結串列中的某個子 View 消費(假設該子 View 會繼續消費掉該事件)
-
- mGroupFlags 可以通過 requestDisallowInterceptTouchEvent(boolean) 進行設定
- 一旦 mGroupFlags 被設定,則 disallowIntercept = true,那麼 ViewGroup 將不會攔截除 ACTION_DOWN 以外的事件(關聯第3點)
從上述可知:
- 11)ACTION_DOWN 事件被 ViewGroup 自身消費,則後續的一系列事件,仍然由 ViewGroup 自身消費
- 12)ACTION_DOWN 事件被某個子 View 消費,則後續的一系列事件,仍然該子 View 消費(假設該子 View 會消費掉該事件)
上述證明見:
-
ACTION_DOWN 重置:第3點
-
ViewGroup 自身消費事件:第1點、第6點
-
事件被子 View 消費(DOWN 事件):第2點
-
本章節:第7點
-
ACTION_DOWN 重置、ViewGroup 自身消費事件、事件被子 View 消費(DOWN 事件)組合證明:第8點
-
子 View 消費事件、遍歷所有子 View(簡略)組合證明:第9點
-
滑動衝突:第10點
// 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;
}
複製程式碼
dispatchTouchEvent:ACTION_DOWN 重置
該段程式碼在攔截判斷之前,可以看出以下資訊被重置了:
- mGroupFlags
- mFirstTouchTarget
該部分原始碼證明上述攔截判定模組的
- 第3點
- 第8點部分證明
// 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();
}
複製程式碼
/**
* Resets all touch state in preparation for a new cycle.
*/
private void resetTouchState() {
clearTouchTargets();
resetCancelNextUpFlag(this);
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
mNestedScrollAxes = SCROLL_AXIS_NONE;
}
/**
* Clears all touch targets.
*/
private void clearTouchTargets() {
TouchTarget target = mFirstTouchTarget;
if (target != null) {
do {
TouchTarget next = target.next;
target.recycle();
target = next;
} while (target != null);
mFirstTouchTarget = null;
}
}
複製程式碼
dispatchTouchEvent:ViewGroup 自身消費事件(核心)
很明顯 mFirstTouchTarget == null 的時候,ViewGroup 自身嘗試消費事件,講解:
- dispatchTransformedTouchEvent:實際就是呼叫子 View 的 dispatchTouchEvent 方法
- 很明顯 ViewGroup 自身消費事件時,child == null,意味著 ViewGroup 會走一遍 ViewGroup 作為 View 層次的事件傳遞過程
該部分原始碼證明上述攔截判定模組的
- 第1點
- 第6點
- 第8點部分證明
// ACTION_DOWN 重置(mFirstTouchTarget = null)
// 攔截判定
boolean intercepted = ...
if (!canceled && !intercepted) {
...
}
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
// 子 View 消費 ACTION_DOWN 之外的事件
...
}
複製程式碼
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
...
}
return handled;
}
複製程式碼
dispatchTouchEvent:子 View 消費事件(核心)
必要說明(以下說明具體原因見之後的原始碼):
- 13)DOWN 事件被某個子 View 消費,則 alreadyDispatchedToNewTouchTarget == true
- 14)DOWN 事件被某個子 View 消費,則 mFirstTouchTarget != null,且 mFirstTouchTarget 包含該子 View。
- 15)對於同一系列事件(DOWN、MOVE、UP)只有 ACTION_DOWN 才能遍歷所有子 View,其他事件不會遍歷
結合上述以及由下述原始碼可知:
- 當前事件為 ACTION_DOWN,且被消費時,事件不再重複分發。
- 事件不為 ACTION_DOWN 時,不會遍歷所有子 View,而是直接通過 mFirstTouchTarget 連結串列遍歷查詢消費過 ACTION_DOWN 事件的子 View,讓這些子 View 嘗試消費事件,注意【target.pointerIdBits】屬性,該屬性自行研究
該部分原始碼證明上述攔截判定模組的
- 第1點
- 第6點
- 第9點部分證明
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// ViewGroup 自身消費事件
...
} 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;
if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
// 事件被子 View 消費(DOWN 事件)
handled = true;
} else {
// DOWN 之外事件處理邏輯
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
複製程式碼
dispatchTouchEvent:遍歷所有子 View(簡述)
這部分原始碼是 ViewGroup 遍歷所有子 View,讓子 View 嘗試消費事件的整體原始碼。從下面的原始碼可知:
- 對於同一系列事件(DOWN、MOVE、UP):只有 ACTION_DOWN 才能遍歷所有子 View,其他事件不會遍歷
- 子 View 不能接受事件的條件:該子 View 正在播放動畫、焦點不在子 View 的區域內
- 跳出遍歷的方式有兩種:
- 直接從 mFirstTouchTarget 連結串列中找到可消費該事件的子 View
- 事件被某個子 View 消費
該部分原始碼講解證明上述攔截判定模組的
- 第9點部分證明
該部分原始碼證明上述子 View 消費事件章節的
- 第15點
TouchTarget newTouchTarget = null;
if (!canceled && !intercepted) {
// 尋找方式:指尋找能夠消費本事件的子 View 的方式
// 優化尋找(上):按官方註釋講解,是直接尋找可以接受該焦點的子 View
View childWithAccessibilityFocus = ...;
// 對於同一系列事件(ACTION_DOWN、ACTION_MOVE、ACTION_UP):只有 ACTION_DOWN 才能遍歷所有子 View
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
...
if (newTouchTarget == null && childrenCount != 0) {
...
for (int i = childrenCount - 1; i >= 0; i--) {
...
// 優化尋找(下):直接找到能接受該焦點的子 View
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
...
}
// 不能接受事件:正在播放動畫,或焦點不在子 View 的區域內
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 尋找方式一:直接從 mFirstTouchTarget 連結串列中找到可消費該事件的子 View
newTouchTarget = getTouchTarget(child);
if (newTouchTarget != null) {
...
break;
}
// 尋找方式二:事件被子 View 消費
resetCancelNextUpFlag(child);
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
break;
}
...
}
if (preorderedList != null) preorderedList.clear();
}
...
}
}
複製程式碼
dispatchTouchEvent:子 View 不能接受事件判定
注意:對於同一系列事件(DOWN、MOVE、UP):只有 ACTION_DOWN 才能遍歷所有子 View,其他事件不會遍歷
自行閱讀相關方法:
- canViewReceivePointerEvents == true:正在播放動畫
- isTransformedTouchPointInView == true:焦點不在子 View 的區域內
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
...
}
複製程式碼
/**
* Returns true if a child view can receive pointer events.
* @hide
*/
private static boolean canViewReceivePointerEvents(@NonNull View child) {
return (child.mViewFlags & VISIBILITY_MASK) == VISIBLE
|| child.getAnimation() != null;
}
複製程式碼
/**
* Returns true if a child view contains the specified point when transformed
* into its coordinate space.
* Child must not be null.
* @hide
*/
protected boolean isTransformedTouchPointInView(float x, float y, View child, PointF outLocalPoint) {
...
}
複製程式碼
dispatchTouchEvent:尋找方式一:直接從連結串列中找到可消費該事件的子 View
注意:對於同一系列事件(DOWN、MOVE、UP):只有 ACTION_DOWN 才能遍歷所有子 View,其他事件不會遍歷
如下原始碼可知:可以直接從 mFirstTouchTarget 連結串列中找到可消費該事件的子 View,並跳出迴圈
for (int i = childrenCount - 1; i >= 0; i--) {
...
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;
}
...
}
}
複製程式碼
/**
* Gets the touch target for specified child view.
* Returns null if not found.
*/
private TouchTarget getTouchTarget(@NonNull View child) {
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
if (target.child == child) {
return target;
}
}
return null;
}
複製程式碼
dispatchTouchEvent:尋找方式二:事件被子 View 消費(DOWN 事件)
注意:對於同一系列事件(DOWN、MOVE、UP):只有 ACTION_DOWN 才能遍歷所有子 View,其他事件不會遍歷
原始碼講解:
- dispatchTransformedTouchEvent:實際就是呼叫子 View 的 dispatchTouchEvent 方法
- 事件被子 View 消費後,跳出迴圈
- addTouchTarget 將當前子 View 追加到 mFirstTouchTarget 連結串列的頭部,注意一開始 mFirstTouchTarget == null
該部分原始碼證明上述攔截判定模組的
- 第2點
- 第8點部分證明
該部分原始碼證明上述子 View 消費事件章節的
- 第13點
- 第14點
for (int i = childrenCount - 1; i >= 0; i--) {
...
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
...
newTouchTarget = addTouchTarget(child, idBitsToAssign);
alreadyDispatchedToNewTouchTarget = true;
break;
}
...
}
複製程式碼
/**
* Transforms a motion event into the coordinate space of a particular child view,
* filters out irrelevant pointer ids, and overrides its action if necessary.
* If child is null, assumes the MotionEvent will be sent to this ViewGroup instead.
*/
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel, View child, int desiredPointerIdBits) {
...
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
...
handled = child.dispatchTouchEvent(event);
...
}
return handled;
}
複製程式碼
/**
* Adds a touch target for specified child to the beginning of the list.
* Assumes the target child is not already present.
*/
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
複製程式碼
同一系列事件分發簡述
對簡單 ViewGroup 而言
ACTION_DOWN 事件:
- 遍歷子 View,事件被某個子 View 消費,將該子 View 存入連結串列中
- 遍歷子 View,所有子 View 都沒有消費,則連結串列為空,然後事件被 ViewGroup 消費
- 子 View 和 ViewGroup 都沒有消費
ACTION_MOVE 事件(與上述一一對應):
- 不再遍歷子 View,直接從連結串列中查詢子 View,讓連結串列中儲存的子 View 消費事件
- 不再遍歷子 View,因為連結串列為空,所以直接由 ViewGroup 嘗試消費
- 同上
ACTION_UP 事件(與上述一一對應):
- 同 ACTION_MOVE 事件
- 同 ACTION_MOVE 事件
- 同 ACTION_MOVE 事件
對於複雜 ViewGroup 而言(比如 RecyclerView),ACTION_DOWN 事件被子 View 消費後,ACTION_MOVE 時會根據情況將連結串列清空。這樣子 View 即不影響子 View 的點選事件,也不影響自身的滑動
六、滑動衝突
延期,待補充吧
requestDisallowInterceptTouchEvent:修改 mGroupFlags
結論:
- disallowIntercept == true:不允許 ViewGroup 直接攔截事件,必須先遍歷子 View。
- disallowIntercept == false:mGroupFlags 修改為預設值
@Override
public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
if (disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0)) {
// We're already in this state, assume our ancestors are too
return;
}
if (disallowIntercept) {
mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
} else {
mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
}
// Pass it up to our parent
if (mParent != null) {
mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
}
}
複製程式碼
參考
- 《Android 開發藝術探索》(任玉剛 著) 第三章 View 的事件體系
- Android 26 原始碼(主要)
版本
- 2017-12-17:未完成版本
- 2017-12-23:完成對事件分發機制的說明
- ???:完成對滑動衝突的說明
宣告
限於作者水平有限,出錯難免,請積極拍磚! 歡迎任何形式的轉載,轉載請保留本文原文連結:juejin.im/post/5c1764…
結言
斷斷續續的把這篇部落格一點點補全,感覺還是挺尷尬的,不過目前整體已經搞定了,剩下的邊邊角角,有時間再補充吧,捂臉。