點選事件分發機制 關鍵原始碼筆記
請注意,涉及到的原始碼 SDK 版本為 27,不同版本可能存在偏差,一切以具體的原始碼為準。
宣告: 文字部分主要參考自 《Android 開發藝術探索》,原始碼部分的解讀主要摘抄自 Android 觸控事件機制(三) View中觸控事件詳解 、 Android 觸控事件機制(四) ViewGroup中觸控事件詳解,但是都加入了自己的思考。
首先,需要明確的就是,同一個事件序列
,是指從手指接觸螢幕的那一刻起到手指離開螢幕的那一刻結束,這個過程所產生的一系列事件,即一個連續的 ACTION_DOWN -> ACTION_MOVE (0 個或者多個)-> ACTION_UP
對於 ACTION_DOWN
、ACTION_MOVE
、ACTION_UP
,在下面簡稱為 DOWN
、MOVE
、UP
。
-
dispatchTouchEvent (MotionEvent ev)
用來進行事件分發的,在 View 和 ViewGroup 中(雖然 ViewGroup 繼承自 View,但是重寫了此方法)的實現會有不同。
如果事件能夠傳遞給當前 View 或 ViewGroup,則該方法一定會被呼叫。
其返回結果表示是否消耗當前事件(消耗的含義是指返回 true,只要返回 true 就表示消耗了,而不管有沒有利用事件進行某種邏輯的處理),其受到當前 View/ViewGroup 的
onTouchEvent
dispatchTouchEvent
方法的影響。 -
onInterceptTouchEvent (MotionEvent ev)
在 ViewGroup 的
dispatchTouchEvent()
方法內部進行呼叫, 用來判斷是否攔截某個事件。在 View 中沒有該方法,只存在於 ViewGroup 中。如果當前 ViewGroup 攔截了 DOWN 事件則後續的 MOVE、UP 事件來時都不會呼叫
onInterceptTouchEvent()
了,如果攔截的是 DOWN 後的 MOVE 事件,那麼 UP 事件來的時候還是可能會呼叫onInterceptTouchEvent()
-
onTouchEvent (MotionEvent ev)
該方法在 View 的
dispatchTouchEvent()
方法內部直接被呼叫;在 ViewGroup 中是間接被呼叫 。其用來處理事件,返回 true 表示消耗當前事件。
如果當前 View/ViewGroup 對於傳遞過來的 DOWN 事件沒有消耗,則無法被(因為一般情況下是由上級 ViewGroup 主動傳遞的)接收後續的 MOVE、UP 事件。而如果是沒有消耗 MOVE 事件(前提是消耗了 DOWN 事件),則還是可以接收後續的 UP 事件。
提前總結部分:
摘抄自 《Android 開發藝術探索》,但是內容加入了自己的見解。
(1)正常情況下,一個事件序列只能被一個 ViewGroup 攔截且消耗(或者被一個 View 消耗),因為一旦一個元素攔截消耗了某個事件,那麼同一個事件序列內的接下來的所有事件都會直接交給它處理。 因為同一個事件序列中的事件不能分別由兩個 View 同時處理,但是通過特殊手段可以實現,如一個 View 將本該自己處理的事件通過其他的View 的 onTouchEvent 強行傳遞給其處理。
(2)在一個事件序列中,某個 ViewGroup 一旦決定攔截該事件序列中的某一事件,那麼這一個事件序列之後的事件就只能由它來處理(如果事件序列能夠傳遞給它的話),並且它的 onInterceptTouchEvent()
方法不會再被呼叫。
(3)某個 View/ViewGroup 如果一旦開始處理事件,如果它不消耗 ACTION_DOWN
事件(onTouchEvent
返回了 false),那麼同一事件序列中的接下來的事件都不會再交給它來處理了,並且 ACTION_DOWN
事件將重新交由它的父控制元件處理,即父控制元件的 onTouchEvent()
會被呼叫。
(4)如果 View/ViewGroup 不消耗除 ACTION_DOWN
以外的某個事件,那麼該事件就會傳遞到該 View/ViewGroup 就截止了,此時父控制元件的 onTouchEvent()
並不會被呼叫(但是該事件會傳遞給 Activity 處理,因為 Activity 會根據最終返回的 true/false 進行相應的處理),並且當前 View/ViewGroup 可以持續收到後續的事件。
(5)ViewGroup 預設不攔截任何事件。Android 原始碼中的 ViewGroup 的 onInterceptTouchEvent()
預設返回 false。
(6)View 沒有 onInterceptTouchEvent()
方法,一旦事件傳給它其 onTouchEvent()
就會被呼叫。
(7)View 的 onTouchEvent 預設都會消耗事件(返回 true),除非是不可點選的(clickable == false && longClickable == true
)。View 的 longClickable
預設為 fasle,2而 clickable
則要分情況,比如 Button 的 clickable
預設為 true,而 TextView 則為 false。
(8)View 的 enable 屬性不影響 onTouchEvent()
的返回值,即使 View 是 disable 狀態的,只要它的 clickable / longClickable
有一個為 true,則 onTouchEvent()
返回 true。
(9)onClick 會發生的前提是當前 View 是可點選的,且接收到了 down 和 up 事件(對於 up 事件接收到了但不一定要消耗,但是對於 down 事件一般情況下不消耗就無法接收後續事件)。更準確的說,是要經過 View 自身的 onTouchEvent()
方法,因為在該方法裡面,當傳遞了 down 和 up 事件之後,就會達到某個條件觸發 onClick。(後面的 View 的 onTouchEvent()
的原始碼分析會說明)
(10)事件傳遞過程是由外向內的,即事件總是先傳遞給父元素,再由父元素分發給子元素,通過 requestDisallowInterceptTouchEvent()
方法可以在子元素中干預父元素的事件分發過程,但是 ACTION_DOWN
除外。
原始碼解讀
包含
- ViewGroup 的 dispatchTouchEvent(MotionEvent ev)
- ViewGroup 的 dispatchTransformedTouchEvent(MotionEvent ev)
- View(不含 ViewGroup) 的 dispatchTouchEvent(MotionEvent ev)
- View 的 onTouchEvent(MotionEvent ev)
- ViewGroup 的
dispatchTouchEvent(MotionEvent ev)
原始碼
public boolean dispatchTouchEvent(MotionEvent ev) {
// mInputEventConsistencyVerifier是除錯用的,不會理會
if (mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
}
// If the event targets the accessibility focused view and this is it, start
// normal event dispatch. Maybe a descendant is what will handle the click.
if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
ev.setTargetAccessibilityFocus(false);
}
/* 第1步:判斷是否要分發該觸控事件 */
// onFilterTouchEventForSecurity() 表示是否要分發該觸控事件
// 如果該 View 不是位於頂部,並且有設定屬性使該 View 不在頂部時不響應觸控事件,則不分發該觸控事件,即返回false
// 否則,則對觸控事件進行分發,即返回true
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
/* 第2步:檢測是否需要清空目標和狀態 */
// 如果是 ACTION_DOWN(即按下事件) ,則清空之前的觸控事件處理目標和狀態。
// 這裡的情況狀態包括:
// (01) 清空 mFirstTouchTarget 連結串列,並設定 mFirstTouchTarget 為 null。
// mFirstTouchTarget 是"接受觸控事件的 View" 所組成的單鏈表
// (02) 清空 mGroupFlags 的 FLAG_DISALLOW_INTERCEPT 標記
// 如果設定了 FLAG_DISALLOW_INTERCEPT ,則不允許 ViewGroup 對觸控事件進行攔截。
// (03) 清空 mPrivateFlags 的 PFLAG_CANCEL_NEXT_UP_EVENT 標記
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();
}
/* 第3步:檢查當前 ViewGroup 是否想要攔截觸控事件(這裡只是單純的檢查是不是要攔截) */
// 是的話,設定 intercepted 為 true ;否則 intercepted 為 false。
// 如果是"按下事件(ACTION_DOWN)" 或者 mFirstTouchTarget 不為 null 就執行 if 程式碼塊裡面的內容。
// 否則的話,設定 intercepted 為 true。
// mFirstTouchTarget != null 表示有子控制元件接收消耗了 DOWN 事件,因此當 MOVE、UP 事件來的時候能夠進入到 if 程式碼塊中
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 檢查禁止攔截標記:FLAG_DISALLOW_INTERCEPT
// 如果呼叫了 requestDisallowInterceptTouchEvent() 標記的話,則 FLAG_DISALLOW_INTERCEPT 會為 true。
// 例如,ViewPager在處理觸控事件的時候,就會呼叫 requestDisallowInterceptTouchEvent(),禁止它的父類對觸控事件進行攔截
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 如果禁止攔截標記為 false 的話,則呼叫 onInterceptTouchEvent() 並返回攔截狀態。
//如果攔截了 DOWN 則後續的 MOVE、UP 事件時都不會呼叫 onInterceptTouchEvent() 了
//如果攔截的是 DOWN 後的 MOVE 事件,那麼 UP 事件來的時候還是會呼叫 onInterceptTouchEvent() 方法
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;
}
// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if (intercepted || mFirstTouchTarget != null) {
ev.setTargetAccessibilityFocus(false);
}
/* 第4步:檢查當前的觸控事件是否被取消 */
// (01) 對於 ACTION_DOWN 而言,mPrivateFlags 的 PFLAG_CANCEL_NEXT_UP_EVENT 位肯定是 0;因此,canceled=false。
// (02) 當前的 View/ViewGroup 要被從父View中 detach 時, PFLAG_CANCEL_NEXT_UP_EVENT 就會被設為 true;
// 此時,它就不再接受觸控事情。
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
/* 第5步:將觸控事件分發給"當前 ViewGroup 的 子 View/ViewGroup" */
// 如果觸控"沒有被取消",同時也"沒有被攔截"的話,則將觸控事件分發給它的子View和子ViewGroup。
// 如果當前 ViewGroup 的孩子能接受觸控事件的話,則將該孩子新增到 mFirstTouchTarget 連結串列中。
// Update list of touch targets for pointer down, if needed.
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
TouchTarget newTouchTarget = null;
boolean alreadyDispatchedToNewTouchTarget = false;
if (!canceled && !intercepted) {
// If the event is targeting accessiiblity focus we give it to the
// view that has accessibility focus and if it does not handle it
// we clear the flag and dispatch the event to all children as usual.
// We are looking up the accessibility focused host to avoid keeping
// state since these events are very rare.
View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
? findChildWithAccessibilityFocus() : null;
//對於 DOWN、MOVE、UP三個事件,只有 DOWN 事件才有可能進入到判斷語句中,對子控制元件進行遍歷分發
//而 MOVE、UP 則是在第6步中,直接遍歷 mFirstTouchTarget 連結串列,查詢之前接受 DOWN 事件的孩子,並將觸控事件分配給這些孩子
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 這是獲取觸控事件的序號 以及 觸控事件的id資訊。
// (01) 對於 ACTION_DOWN,actionIndex 肯定是0
// (02) 而 getPointerId() 是獲取的該觸控事件的id,並將該id資訊儲存到 idBitsToAssign 中。
// 這個觸控事件的 id 是為多指觸控而新增的;對於單指觸控,getActionIndex() 返回的肯定是0;
// 而對於多指觸控,第一個手指的 id 是 0,第二個手指的 id 是1,...依次類推。
final int actionIndex = ev.getActionIndex(); // always 0 for down
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// 清空這個手指之前的 TouchTarget 連結串列。
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
removePointersFromTouchTargets(idBitsToAssign);
// 獲取該 ViewGroup 包含的 View/ViewGroup 的數目,
// 然後遞迴遍歷該 ViewGroup 的孩子,對觸控事件進行分發。
// 遞迴遍歷 ViewGroup 的孩子:是指對於當前 ViewGroup 的所有孩子,都會逐個遍歷,並分發觸控事件;
// 對於逐個遍歷到的每一個孩子,若該孩子是 ViewGroup 型別的話,則會遞迴到呼叫該孩子的孩子,...
final int childrenCount = mChildrenCount;
if (newTouchTarget == null && childrenCount != 0) {
final float x = ev.getX(actionIndex);
final float y = ev.getY(actionIndex);
// Find a child that can receive the event.
// Scan children from front to back.
final ArrayList<View> preorderedList = buildTouchDispatchChildList();
final boolean customOrder = preorderedList == null
&& isChildrenDrawingOrderEnabled();
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);
// If there is a view that has accessibility focus we want it
// to get the event first and if not handled we will perform a
// normal dispatch. We may do a double iteration but this is
// safer given the timeframe.
if (childWithAccessibilityFocus != null) {
if (childWithAccessibilityFocus != child) {
continue;
}
childWithAccessibilityFocus = null;
i = childrenCount - 1;
}
// 如果 child 可以接受觸控事件,
// 並且觸控座標 (x,y) 在 child 的可視範圍之內的話;
// 則繼續往下執行。否則,呼叫continue。
// child可接受觸控事件:是指child的是可見的(VISIBLE);或者雖然不可見,但是位於動畫狀態。
if (!canViewReceivePointerEvents(child)
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// getTouchTarget() 的作用是查詢 child 是否存在於 mFirstTouchTarget 的單鏈表中。
// (如果是後來有為 ViewGroup 新新增子 View/ViewGroup,則有可能還沒有存在於 mFirstTouchTarget 的單鏈表中,此時就會達到 __標記_1,且如果符合條件就會被新加進 mFirstTouchTarget 的單鏈表中)
// 是的話,返回對應的 TouchTarget 物件(此時就會跳出迴圈,因為已經找到可以接收 DOWN 事件的子 View/ViewGroup);否則,返回 null。
newTouchTarget = getTouchTarget(child);// newTouchTarget 第一次被賦值
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;
//馬上跳出迴圈,會在第 6 步將事件進一步分發給 mFirstTouchTarget 中的子 View/Group
break;
}
// 重置 child的mPrivateFlags 變數中的 PFLAG_CANCEL_NEXT_UP_EVENT 位。
resetCancelNextUpFlag(child);
// 呼叫 dispatchTransformedTouchEvent() 將觸控事件分發給child。 __標記_1
if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
// 如果 child 能夠接受該觸控事件,即 child 消費或者攔截了該觸控事件的話;
// 則呼叫 addTouchTarget() 將 child 新增到 mFirstTouchTarget 連結串列的表頭,並返回表頭對應的 TouchTarget
// 同時還設定 alreadyDispatchedToNewTouchTarget 為 true。
// 然後跳出迴圈
// 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();
//將接受觸控事件的 child 新增到 mFirstTouchTarget 連結串列的表頭
newTouchTarget = addTouchTarget(child, idBitsToAssign);// newTouchTarget 第二次被賦值
alreadyDispatchedToNewTouchTarget = true;
//此時會馬上跳出遍歷孩子的迴圈,之後即使還有能夠接收 DOWN 事件的子 View/ViewGroup 也不會管了
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();
}
// 在 for 迴圈外
// 如果 newTouchTarget 為 null(即newTouchTarget第一次被賦值時為null且沒有經歷第二次賦值),並且 mFirstTouchTarget 不為 null;
// 則設定 newTouchTarget 為 mFirstTouchTarget 連結串列中第一個不為空的節點。
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
/* 第6步:進一步的對觸控事件進行分發 */
// (01) 如果 mFirstTouchTarget 為 null,意味著還沒有任何View來接受該觸控事件;
// 此時,將當前 ViewGroup 看作一個 View;
// 將會呼叫"當前的 ViewGroup 的父類 View 的 dispatchTouchEvent() "對觸控事件進行分發處理。
// (02) 如果mFirstTouchTarget 不為 null,意味著 ViewGroup 的子 View/ViewGroup 中
// 有可以接受觸控事件的。那麼,就將觸控事件分發給這些可以接受觸控事件的子 View/ViewGroup。
if (mFirstTouchTarget == null) {
// 注意:這裡的第3個引數是 null
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} else {
//只要 mFirstTouchTarget 不 null,就一定會經過這一步,但是也會根據第 5 步的執行的結果來決定之後邏輯(因為第 5 步中只 break、continue 掉 for 迴圈,並沒有直接 return )
// 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) {
handled = true;
} else {
//(1) 當 cancelChild == true 此時在 dispatchTransformedTouchEvent() 方法內部會給 child 分發 ACTION_CANCEL 事件
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
handled = true;
}
//(2) 當 cancelChild 為 true 時,就會把子 View/ViewGroup 從原本的 mFirstTouchTarget 的單鏈表中剔除掉,
// 所以之後該子 View/ViewGroup 就無法再接收後續事件了
if (cancelChild) {
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
//因此,整個迴圈下來,會把 mFirstTouchTarget 清空
}
predecessor = target;
target = next;
}
}
/* 第7步:再次檢查取消標記,並進行相應的處理 */
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
// mInputEventConsistencyVerifier是除錯用的,不會理會
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
- ViewGroup 的
dispatchTransformedTouchEvent(MotionEvent ev)
原始碼
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
View child, int desiredPointerIdBits) {
final boolean handled;
// 檢測是否需要傳送 ACTION_CANCEL。(這裡不是針對 DOWN、MOVE、UP 事件進行分發)
// 如果 cancel 為 true 或者 action 是 ACTION_CANCEL; // 則設定訊息為 ACTION_CANCEL,並將 ACTION_CANCEL 訊息分發給對應的物件,並返回。
// (01) 如果 child 是空,則將 ACTION_CANCEL 訊息分發給當前 ViewGroup;
// 只不過會將 ViewGroup 看作它的父類 View,呼叫 View 的 dispatchTouchEvent() 介面。
// (02) 如果 child 不是空,呼叫 child的dispatchTouchEvent()。
final int oldAction = event.getAction();
if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
event.setAction(MotionEvent.ACTION_CANCEL);
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
handled = child.dispatchTouchEvent(event);
}
event.setAction(oldAction);
return handled;
}
// 計算觸控事件的id資訊
final int oldPointerIdBits = event.getPointerIdBits();
final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;
// 如果新的id資訊為0,則返回false。
if (newPointerIdBits == 0) {
return false;
}
// 如果計算得到的前後觸控事件id資訊相同,則執行不需要重新計算MotionEvent,直接執行if語句塊進行消費分發;
// 否則,就重新計算MotionEvent之後,再進行訊息分發。
final MotionEvent transformedEvent;
if (newPointerIdBits == oldPointerIdBits) {
if (child == null || child.hasIdentityMatrix()) {
// 這裡才是正常的對 DOWN、MOVE、UP 事件進行分發
// (01) 如果 child 是空,則將 ViewGroup 看作它的父類 View,呼叫 View 的 dispatchTouchEvent() 介面。
// (02) 如果 child 不是空,呼叫 child 的 dispatchTouchEvent()。
if (child == null) {
handled = super.dispatchTouchEvent(event);
} else {
final float offsetX = mScrollX - child.mLeft;
final float offsetY = mScrollY - child.mTop;
event.offsetLocation(offsetX, offsetY);
handled = child.dispatchTouchEvent(event);
event.offsetLocation(-offsetX, -offsetY);
}
return handled;
}
transformedEvent = MotionEvent.obtain(event);
} else {
transformedEvent = event.split(newPointerIdBits);
}
// 這裡也是正常的對 DOWN、MOVE、UP 事件進行分發
// (01) 如果 child 是空,則將 ViewGroup 看作它的父類 View,呼叫 View 的 dispatchTouchEvent() 介面。
// (02) 如果 child 不是空,呼叫 child的dispatchTouchEvent()。
if (child == null) {
handled = super.dispatchTouchEvent(transformedEvent);
} else {
final float offsetX = mScrollX - chil