ViewGroup事件分發總結-TouchTarget

前言

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方法中,派發流程大致可分爲三部分:

  1. 派發前準備
  2. 派發目標查找
  3. 執行派發

派發前準備

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。

發佈了27 篇原創文章 · 獲贊 2 · 訪問量 8505
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章