ViewGroup中一個完整的事件派發流程是包含一個完整的事件序列的派發,一個完整的事件序列是從ACTION_DOWN開始,ACTION_UP/ACTION_CANCEL結束。java
在多點觸摸狀況下,會出現ACTION_POINTER_DOWN和ACTION_POINTER_UP事件,分別表示在這個ViewGroup上有新的手指按下和離開,表示一個事件子序列。緩存
正常狀況下,這個事件序列中的全部事件都會觸發ViewGroup的dispatchTouchEvent方法進行派發(除非該ViewGroup的上級攔截了事件或該ViewGroup和全部child都不消費事件)。安全
咱們知道ViewGroup在進行事件派發的過程當中會遍歷child,依次詢問是否消費該事件。那麼針對這些全部類型的事件,是否每次都要遍歷child詢問呢?其中有child消費事件後,下個事件來臨時如何傳遞給這個child呢?答案的關鍵就是TouchTarget。markdown
文中源碼基於Android 10.0app
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; // ··· } 複製代碼
TouchTarget保存了響應觸摸事件的子view和該子view上的觸摸點ID集合,表示一個觸摸事件派發目標。經過next成員能夠看出,它支持做爲一個鏈表節點儲存。ui
成員pointerIdBits用於存儲多點觸摸的這些觸摸點的ID。pointerIdBits爲int型,有32bit位,每一bit位能夠表示一個觸摸點ID,最多可存儲32個觸摸點ID。this
pointerIdBits是如何作到在bit位上存儲ID呢?假設觸摸點ID取值爲x(x的範圍可從0~31),存儲時先將1左移x位,而後pointerIdBits與之執行|=操做,從而設置到pointerIdBits的對應bit位上。spa
pointerIdBits的存在乎義是記錄TouchTarget接收的觸摸點ID,在這個TouchTarget上可能只落下一個觸摸點,也可能同時落下多個。當全部觸摸點都離開時,pointerIdBits就已被清0,那麼TouchTarget自身也將被從mFirstTouchTarget中移除。rest
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類型的事件來講,在派發目標查找階段,就會進行一次事件派發。
private TouchTarget getTouchTarget(@NonNull View child) { // 遍歷鏈表 for (TouchTarget target = mFirstTouchTarget; target != null; target = target.next) { // 比較child成員 if (target.child == child) { return target; } } return null; } 複製代碼
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做爲頭節點的鏈表中,所以,只須要遍歷該鏈表進行派發便可。
ViewGroup不用單個TouchTarget保存消費了事件的child,而是經過mFirstTouchTarget鏈表保存多個TouchTarget,是由於存在多點觸摸狀況下,須要將事件拆分後派發給不一樣的child。
假設childA、childB都能響應事件:
在ViewGroup的事件派發流程中,只有在事件序列開始或子序列開始時(ACTION_DOWN或ACTION_POINTER_DOWN),會遍歷子view,進行派發目標查找,並將目標封裝成TouchTarget保存在mFirstTouchTarget鏈表中。完成派發目標查找後,再遍歷TouchTarget鏈表,依次進行事件派發。
此時能夠回答開頭的問題,ViewGroup無需每次事件來臨都遍歷child查詢。ViewGroup會將消費事件的view保存在TouchTarget鏈表中,下次事件來臨只需經過該鏈表便可直接派發給目標view。