前言
ViewGroup中一個完整的事件派發流程是包含一個完整的事件序列的派發,一個完整的事件序列是從ACTION_DOWN開始,ACTION_UP/ACTION_CANCEL結束。
在多點觸摸情況下,會出現ACTION_POINTER_DOWN和ACTION_POINTER_UP事件,分別表示在這個ViewGroup上有新的手指按下和離開,表示一個事件子序列。
正常情況下,這個事件序列中的所有事件都會觸發ViewGroup的dispatchTouchEvent方法進行派發(除非該ViewGroup的上級攔截了事件或該ViewGroup和所有child都不消費事件)。
我們知道ViewGroup在進行事件派發的過程中會遍歷child,依次詢問是否消費該事件。那麼針對這些所有類型的事件,是否每次都要遍歷child詢問呢?其中有child消費事件後,下個事件來臨時如何傳遞給這個child呢?答案的關鍵就是TouchTarget。
源碼探究
文中源碼基於Android 10.0
TouchTarget說明
TouchTarget的作用場景在事件派發流程中,用於記錄派發目標,即消費了事件的子view。在ViewGroup中有一個成員變量mFirstTouchTarget,它會持有TouchTarget,並且作爲TouchTarget鏈表的頭節點。
// First touch target in the linked list of touch targets.
@UnsupportedAppUsage
private TouchTarget mFirstTouchTarget;
重要成員變量
private static final class TouchTarget {
// ···
// The touched child view.
@UnsupportedAppUsage
public View child;
// The combined bit mask of pointer ids for all pointers captured by the target.
public int pointerIdBits;
// The next target in the target list.
public TouchTarget next;
// ···
}
- child:消費事件的子view
- pointerIdBits:child接收的觸摸點的ID集合
- next:指向鏈表下一個節點
TouchTarget保存了響應觸摸事件的子view和該子view上的觸摸點ID集合,表示一個觸摸事件派發目標。通過next成員可以看出,它支持作爲一個鏈表節點儲存。
觸摸點ID存儲
成員pointerIdBits用於存儲多點觸摸的這些觸摸點的ID。pointerIdBits爲int型,有32bit位,每一bit位可以表示一個觸摸點ID,最多可存儲32個觸摸點ID。
pointerIdBits是如何做到在bit位上存儲ID呢?假設觸摸點ID取值爲x(x的範圍可從0~31),存儲時先將1左移x位,然後pointerIdBits與之執行|=操作,從而設置到pointerIdBits的對應bit位上。
pointerIdBits的存在意義是記錄TouchTarget接收的觸摸點ID,在這個TouchTarget上可能只落下一個觸摸點,也可能同時落下多個。當所有觸摸點都離開時,pointerIdBits就已被清0,那麼TouchTarget自身也將被從mFirstTouchTarget中移除。
對象獲取和回收
TouchTarget的構造函數爲私有,不允許直接創建。因爲應用在使用過程中會涉及到大量的TouchTarget創建和銷燬,因此TouchTarget封裝了一個對象緩存池,通過TouchTarget.obtain方法獲取,TouchTarget.recycle方法回收。
事件分發流程
ViewGroup的派發入口在dispatchTouchEvent方法中,派發流程大致可分爲三部分:
- 派發前準備
- 派發目標查找
- 執行派發
派發前準備
public boolean dispatchTouchEvent(MotionEvent ev) {
// ···
// 標記ViewGroup或child是否有消費該事件
boolean handled = false;
// onFilterTouchEventForSecurity中會進行安全校驗,判斷當前窗口被部分遮蔽的情況下是否仍然派發事件。
if (onFilterTouchEventForSecurity(ev)) {
// 獲取事件類型。action的值高8位會包含該事件觸摸點索引信息,actionMasked爲乾淨的事件類型,
// 在單點觸摸情況下action和actionMasked無差別。
final int action = ev.getAction();
final int actionMasked = action & MotionEvent.ACTION_MASK;
// Handle an initial down.
if (actionMasked == MotionEvent.ACTION_DOWN) {
// ACTION_DOWN表示一次全新的事件序列開始,那麼清除舊的
// TouchTarget(正常情況下TouchTarget在上一輪事件序列結束時會清
// 空,若此時仍存在,則需要先給這些TouchTarget派發ACTION_CANCEL事
// 件,然後再清除),重置觸摸滾動等相關的狀態和標識位。
// 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();
}
// Check for interception.
// 標記ViewGroup是否攔截該事件(全新事件序列開始時判斷)。
final boolean intercepted;
if (actionMasked == MotionEvent.ACTION_DOWN
|| mFirstTouchTarget != null) {
// 判斷child是否搶先調用了requestDisallowInterceptTouchEvent方法
final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
if (!disallowIntercept) {
// 再通過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);
}
// Check for cancelation.
// 標記是否派發ACTION_CANCEL事件
final boolean canceled = resetCancelNextUpFlag(this)
|| actionMasked == MotionEvent.ACTION_CANCEL;
}
// ···
}
在派發事件前,會先判斷若當次ev是ACTION_DOWN,則對當前ViewGroup來說,表示是一次全新的事件序列開始,那麼需要保證清空舊的TouchTarget鏈表,以保證接下來mFirstTouchTarget可以正確保存派發目標。
派發目標查找
public boolean dispatchTouchEvent(MotionEvent ev) {
// ···
// Update list of touch targets for pointer down, if needed.
// split標記是否需要進行事件拆分
final boolean split = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) != 0;
// newTouchTarget用於保存新的派發目標
TouchTarget newTouchTarget = null;
// 標記在目標查找過程中是否已經對newTouchTarget進行過派發
boolean alreadyDispatchedToNewTouchTarget = false;
// 只有當非cancele且不攔截的情況才進行目標查找,否則直接跳到執行派發步驟。如果是
// 因爲被攔截,那麼還沒有派發目標,則會由ViewGroup自己處理事件。
if (!canceled && !intercepted) {
// ···
if (actionMasked == MotionEvent.ACTION_DOWN
|| (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 當ev爲ACTION_DOWN或ACTION_POINTER_DOWN時,表示對於當前ViewGroup
// 來說有一個新的事件序列開始,那麼需要進行目標查找。(不考慮懸浮手勢操作)
final int actionIndex = ev.getActionIndex(); // always 0 for down
// 通過觸摸點索引取得觸摸點ID,然後左移x位(x=ID值)
final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
: TouchTarget.ALL_POINTER_IDS;
// Clean up earlier touch targets for this pointer id in case they
// have become out of sync.
// 遍歷mFirstTouchTarget鏈表,進行清理。若有TouchTarget設置了此觸摸點ID,
// 則將其移除該ID,若移除後的TouchTarget已經沒有觸摸點ID了,那麼接着移除
// 這個TouchTarget。
removePointersFromTouchTargets(idBitsToAssign);
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;
// 逆序遍歷子view,即先查詢上面的
for (int i = childrenCount - 1; i >= 0; i--) {
final int childIndex = getAndVerifyPreorderedIndex(
childrenCount, i, customOrder);
final View child = getAndVerifyPreorderedView(
preorderedList, children, childIndex);
// ···
// 判斷該child能否接收觸摸事件和點擊位置是否命中child範圍內。
if (!child.canReceivePointerEvents()
|| !isTransformedTouchPointInView(x, y, child, null)) {
ev.setTargetAccessibilityFocus(false);
continue;
}
// 遍歷mFirstTouchTarget鏈表,查找該child對應的TouchTarget。
// 如果之前已經有觸摸點落於該child中且消費了事件,這次新的觸摸點也落於該child中,
// 那麼就會找到之前保存的TouchTarget。
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.
// 派發目標已經存在,只要給TouchTarget的觸摸點ID集合添加新的
// ID即可,然後退出子view遍歷。
newTouchTarget.pointerIdBits |= idBitsToAssign;
break;
}
resetCancelNextUpFlag(child);
// dispatchTransformedTouchEvent方法中會將事件派發給child,
// 若child消費了事件,將返回true。
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();
// 爲該child創建TouchTarget,添加到mFirstTouchTarget鏈表的頭部,
// 並將其設置爲新的頭節點。
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();
}
// 子view遍歷完畢
// 檢查是否找到派發目標
if (newTouchTarget == null && mFirstTouchTarget != null) {
// Did not find a child to receive the event.
// Assign the pointer to the least recently added target.
// 若沒有找到派發目標(沒有命中child或命中的child不消費),但是存在
// 舊的TouchTarget,那麼將該事件派發給最開始添加的那個TouchTarget,
// 多點觸摸情況下有可能這個事件是它想要的。
newTouchTarget = mFirstTouchTarget;
while (newTouchTarget.next != null) {
newTouchTarget = newTouchTarget.next;
}
newTouchTarget.pointerIdBits |= idBitsToAssign;
}
}
}
// ···
}
首先當次事件未cancel且未被攔截,然後必須是ACTION_DOWN或ACTION_POINTER_DOWN,即新的事件序列或子序列的開始,纔會進行派發事件查找。
在查找過程中,會逆序遍歷子view,先找到命中範圍的child。若該child對應的TouchTarget已經在mFirstTouchTarget鏈表中,則意味着之前已經有觸摸點落於該child且消費了事件,那麼只需要給其添加觸摸點ID,然後結束子view遍歷;若沒有找到對應的TouchTarget,說明對於該child是新的事件,那麼通過dispatchTransformedTouchEvent方法,對其進行派發,若child消費事件,則創建TouchTarget添加至mFirstTouchTarget鏈表,並標記已經派發過事件。
注意:這裏先前存在TouchTarget的情況下不執行dispatchTransformedTouchEvent,是因爲需要對當次事件進行事件拆分,對ACTION_POINTER_DOWN類型進行轉化,所以留到後面執行派發階段,再統一處理。
當遍歷完子view,若沒有找到派發目標,但是mFirstTouchTarget鏈表不爲空,則把最早添加的那個TouchTarget當作查找到的目標。
可見,對於ACTION_DOWN類型的事件來說,在派發目標查找階段,就會進行一次事件派發。
- getTouchTarget方法說明
根據child查找對應的TouchTarget
private TouchTarget getTouchTarget(@NonNull View child) {
// 遍歷鏈表
for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) {
// 比較child成員
if (target.child == child) {
return target;
}
}
return null;
}
- addTouchTarget方法說明
將child和pointerIdBits保存到TouchTarget鏈表中
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
// 通過對象緩存池獲取可用的TouchTarget實例,同時保存child和pointerIdBits。
final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
// 添加到鏈表中,並設置成新的頭節點。
target.next = mFirstTouchTarget;
mFirstTouchTarget = target;
return target;
}
執行派發
public boolean dispatchTouchEvent(MotionEvent ev) {
// ···
boolean handled = false;
if (onFilterTouchEventForSecurity(ev)) {
// ···
// Dispatch to touch targets.
if (mFirstTouchTarget == null) {
// No touch targets so treat this as an ordinary view.
// 若mFirstTouchTarget鏈表爲空,說明沒有派發目標,那麼交由ViewGroup自己處理
// (dispatchTransformedTouchEvent第三個參數傳null,會調用ViewGroup自己的dispatchTouchEvent方法)
handled = dispatchTransformedTouchEvent(ev, canceled, null,
TouchTarget.ALL_POINTER_IDS);
} 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) {
// 若已經對newTouchTarget派發過事件,則標記消費該事件。
handled = true;
} else {
final boolean cancelChild = resetCancelNextUpFlag(target.child)
|| intercepted;
// 通過dispatchTransformedTouchEvent派發事件給child
if (dispatchTransformedTouchEvent(ev, cancelChild,
target.child, target.pointerIdBits)) {
// 若child消費了事件,則標記handled爲true
handled = true;
}
if (cancelChild) {
// 若取消該child,則從鏈表中移除對應的TouchTarget,並將
// TouchTarget回收進對象緩存池。
if (predecessor == null) {
mFirstTouchTarget = next;
} else {
predecessor.next = next;
}
target.recycle();
target = next;
continue;
}
}
predecessor = target;
target = next;
}
}
// Update list of touch targets for pointer up or cancel, if needed.
if (canceled
|| actionMasked == MotionEvent.ACTION_UP
|| actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
// 若是取消事件或事件序列結束,則清空TouchTarget鏈表,重置其他狀態和標記位。
resetTouchState();
} else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
// 若是某個觸摸點的事件子序列結束,則從所有TouchTarget中移除該觸摸點ID。
// 若有TouchTarget移除ID後,ID爲空,則再移除這個TouchTarget。
final int actionIndex = ev.getActionIndex();
final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
removePointersFromTouchTargets(idBitsToRemove);
}
}
if (!handled && mInputEventConsistencyVerifier != null) {
mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
}
return handled;
}
執行派發階段,即是對TouchTarget鏈表進行派發。在前面查找派發目標過程中,會將TouchTarget保存在以mFirstTouchTarget作爲頭節點的鏈表中,因此,只需要遍歷該鏈表進行派發即可。
mFirstTouchTarget說明
ViewGroup不用單個TouchTarget保存消費了事件的child,而是通過mFirstTouchTarget鏈表保存多個TouchTarget,是因爲存在多點觸摸情況下,需要將事件拆分後派發給不同的child。
假設childA、childB都能響應事件:
- 當觸摸點1落於childA時,產生事件ACTION_DOWN,ViewGroup會爲childA生成一個TouchTarget,後續滑動事件將派發給它。
- 當觸摸點2落於childA時,產生ACTION_POINTER_DOWN事件,此時可以複用TouchTarget,並給它添加觸摸點2的ID。
- 當觸摸點3落於childB時,產生ACTION_POINTER_DOWN事件,ViewGroup會再生成一個TouchTarget,此時ViewGroup中有兩個TouchTarget,後續產生滑動事件,將根據觸摸點信息對事件進行拆分,之後再將拆分事件派發給對應的child。
總結
在ViewGroup的事件派發流程中,只有在事件序列開始或子序列開始時(ACTION_DOWN或ACTION_POINTER_DOWN),會遍歷子view,進行派發目標查找,並將目標封裝成TouchTarget保存在mFirstTouchTarget鏈表中。完成派發目標查找後,再遍歷TouchTarget鏈表,依次進行事件派發。
此時可以回答開頭的問題,ViewGroup無需每次事件來臨都遍歷child查詢。ViewGroup會將消費事件的view保存在TouchTarget鏈表中,下次事件來臨只需通過該鏈表即可直接派發給目標view。