preface

A complete event dispatch process in ViewGroup includes a complete event sequence dispatch. A complete event sequence starts from ACTION_DOWN and ends with ACTION_UP/ACTION_CANCEL.

In the case of multi-touch, ACTION_POINTER_DOWN and ACTION_POINTER_UP events appear, indicating that there is a new finger on the ViewGroup, and that there is a new finger off the ViewGroup, representing a subsequence of events.

Normally, all events in this sequence will trigger the ViewGroup’s dispatchTouchEvent method for dispatch (unless the ViewGroup’s parent intercepts the event or neither the ViewGroup nor any child consumes the event).

We know that the ViewGroup traverses the child during event dispatch, asking in turn whether to consume the event. So for all of these types of events, should we iterate over the Child query each time? So if you have a child consuming an event, how do you pass it to that child when the next event comes? The key to the answer is TouchTarget.

The source code to explore

The source code is based on Android 10.0

TouchTarget instructions

The action scenario of TouchTarget is used to record the dispatch target in the event dispatch process, that is, the child view that consumes the event. There is a member variable in the ViewGroup, mFirstTouchTarget, that holds the TouchTarget and acts as the head of the TouchTarget list.

// First touch target in the linked list of touch targets.
@UnsupportedAppUsage
private TouchTarget mFirstTouchTarget;
Copy the code

Important member variable

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;
    
    / /...
}
Copy the code
  • Child: the child view of the consumption event
  • PointerIdBits: The set of ids of touch points received by the Child
  • Next: Points to the next node in the list

The TouchTarget holds the child view that responds to the touch event and the set of touch point ids on that child view, representing a touch event dispatch target. As you can see from the Next member, it supports storage as a linked list node.

Touch point ID store

The member pointerIdBits is used to store the ID of these touch points for multitouch. PointerIdBits is an int with 32 bits. Each bit can represent a touch point ID. A maximum of 32 touch point ids can be stored.

How does pointerIdBits store ids in bits? Assume that touch point ID value for x (x range from 0 ~ 31), storage when 1 first left x, then pointerIdBits perform | = operation, which is set to the corresponding bit pointerIdBits.

The purpose of pointerIdBits is to record the ID of the touch points received by the TouchTarget. It may drop only one touch point on the TouchTarget, or it may drop multiple touch points at the same time. When all touchpoints are gone, the pointerIdBits are cleared and the TouchTarget itself is removed from the mFirstTouchTarget.

Object acquisition and reclamation

The TouchTarget constructor is private and cannot be created directly. Because a large number of TouchTargets are created and destroyed during application use, TouchTarget encloses a cache pool of objects, which is obtained through the touchTarget. obtain method and reclaimed by the TouchTarget.recycle method.

Event distribution Process

The dispatching entry of ViewGroup is in the dispatchTouchEvent method. The dispatching process can be roughly divided into three parts:

  1. Pre-distribution preparation
  2. Dispatched target search
  3. Distributed execution

Pre-distribution preparation

public boolean dispatchTouchEvent(MotionEvent ev) {
    / /...
    
    // Mark whether the ViewGroup or child consumed the event
    boolean handled = false;
    / / onFilterTouchEventForSecurity security check, whether the current window is part of the cover is still out.
    if (onFilterTouchEventForSecurity(ev)) {
            // Get the event type. An action value that is 8 bits higher contains the event touchpoint index information. ActionMasked is an event type that is clean.
            // There is no difference in action and actionMasked in single touch cases.
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // ACTION_DOWN indicates that a new sequence of events is started, so the old one is cleared
                TouchTarget normally clears at the end of the last round of events
                // If the touchTargets still exist, you need to send ACTION_CANCEL to the touchtargets first
                // and then clear), reset touch scrolling and other related state and flag bits.
                
                // 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.
            // Mark whether the ViewGroup intercepts the event (when a new event sequence starts).
            final boolean intercepted;
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
                / / call the requestDisallowInterceptTouchEvent method to determine whether a child first
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                if(! disallowIntercept) {OnInterceptTouchEvent = onInterceptTouchEvent = 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.
            // Indicates whether to dispatch ACTION_CANCEL events
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;
    }
        
    / /...
}
Copy the code

Before sending the event, it is judged that if the ev is ACTION_DOWN, it means that a new event sequence starts for the current ViewGroup. Therefore, it is necessary to ensure that the old TouchTarget list is cleared to ensure that the next mFirstTouchTarget can correctly save the dispatch target.

Dispatched target search

public boolean dispatchTouchEvent(MotionEvent ev) {
    / /...
    
    // Update list of touch targets for pointer down, if needed.
    // The split flag indicates whether event splitting is required
    final booleansplit = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! =0;
    // newTouchTarget is used to save the new dispatch target
    TouchTarget newTouchTarget = null;
    // Mark whether newTouchTarget has been distributed during target lookup
    boolean alreadyDispatchedToNewTouchTarget = false;
    // Perform target lookup only if it is not cancele and does not intercept, otherwise skip to the dispatch step. If it is
    // The ViewGroup will handle the event itself.
    if(! canceled && ! intercepted) {/ /...

        if (actionMasked == MotionEvent.ACTION_DOWN
                || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
            // When ev is ACTION_DOWN or ACTION_POINTER_DOWN, it indicates the current ViewGroup
            // To start a new sequence of events, a target lookup is required. (Hover gesture operation is not considered)
            final int actionIndex = ev.getActionIndex(); // always 0 for down
            // Get the ID of the touch point by the touch point index, and then shift it to the left x bit (x=ID value)
            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.
            // Iterate over the mFirstTouchTarget list to clean it up. If there's a TouchTarget that sets this touch ID,
            // Remove this ID from the TouchTarget. If the TouchTarget has no ID left, remove it again
            // The TouchTarget.
            removePointersFromTouchTargets(idBitsToAssign);

            final int childrenCount = mChildrenCount;
            if (newTouchTarget == null&& childrenCount ! =0) {
                // Obtain the position of the corresponding touch point by the touch point index
                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;
                // Check the subview in reverse order
                for (int i = childrenCount - 1; i >= 0; i--) {
                    final int childIndex = getAndVerifyPreorderedIndex(
                            childrenCount, i, customOrder);
                    final View child = getAndVerifyPreorderedView(
                            preorderedList, children, childIndex);

                    / /...
                    
                    // Determine whether the child can receive touch events and whether the click location hits within the child range.
                    if(! child.canReceivePointerEvents() || ! isTransformedTouchPointInView(x, y, child,null)) {
                        ev.setTargetAccessibilityFocus(false);
                        continue;
                    }

                    // Iterate through the mFirstTouchTarget list to find the TouchTarget corresponding to the child.
                    // If a touch has already fallen on the child and consumed the event, this time the new touch will also fall on the child.
                    // Then the saved TouchTarget will be found.
                    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.
                        
                        // The target already exists, just add a new one to the TouchTarget's set of touchpoint ids
                        // exit the subview traversal.
                        newTouchTarget.pointerIdBits |= idBitsToAssign;
                        break;
                    }

                    resetCancelNextUpFlag(child);
                    / / dispatchTransformedTouchEvent method in the event will be sent to the child,
                    // If the child consumed the event, return 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();
                        // Create a TouchTarget for the child and add it to the head of the mFirstTouchTarget list.
                        // and set it to the new header node.
                        newTouchTarget = addTouchTarget(child, idBitsToAssign);
                        // Indicates that the event has been distributed
                        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();
            }
            // The child view is iterated
            
            // Check whether the target is found
            if (newTouchTarget == null&& mFirstTouchTarget ! =null) {
                // Did not find a child to receive the event.
                // Assign the pointer to the least recently added target.
                
                // If no distribution target is found (no hit child or hit child does not consume), but exists
                // The old TouchTarget, then send the event to the first TouchTarget that was added,
                // In the case of multi-touch it is possible that this event is what it wants.
                newTouchTarget = mFirstTouchTarget;
                while(newTouchTarget.next ! =null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; }}}/ /...
}
Copy the code

First, when the event is not cancelled and not intercepted, and then it must be ACTION_DOWN or ACTION_POINTER_DOWN, that is, the start of a new event sequence or subsequence, the event lookup will be dispatched.

In the search process, the child view will be traversed in reverse order to find the hit range of the child first. If the child’s TouchTarget is already in the mFirstTouchTarget list, it means that a touch point has already fallen on the child and consumed the event, so just add the touch point ID to the child and end the child view traversal. If not find corresponding TouchTarget, explains how the child is a new event, then through dispatchTransformedTouchEvent method, distributed on it. If the child consumer events, Creates a TouchTarget and adds it to the mFirstTouchTarget list, marking that the event has been dispatched. Note: the presence of TouchTarget previously does not perform dispatchTransformedTouchEvent, because need to find time for events, to transformation of ACTION_POINTER_DOWN type, so leave behind the distributed execution phase, the reunification of processing.

When the subview is traversed, if no target is found, but the mFirstTouchTarget list is not empty, the earliest added TouchTarget is considered as the target found.

It can be seen that for the ACTION_DOWN type event, an event dispatch will be performed during the target lookup phase of dispatch.

  • The getTouchTarget method describes finding the corresponding TouchTarget against the child
private TouchTarget getTouchTarget(@NonNull View child) {
    // Traverse the list
    for(TouchTarget target = mFirstTouchTarget; target ! =null; target = target.next) {
        // Compare child members
        if (target.child == child) {
            returntarget; }}return null;
}
Copy the code
  • The addTouchTarget method describes how to save child and pointerIdBits to a linked TouchTarget list
private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
    // Get the available TouchTarget instance from the object cache pool and save the Child and pointerIdBits.
    final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
    // Add to the list and set as the new head node.
    target.next = mFirstTouchTarget;
    mFirstTouchTarget = target;
    return target;
}
Copy the code

Distributed execution

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.
            // If the mFirstTouchTarget list is empty, there is no target to be dispatched
            / / (dispatchTransformedTouchEvent third parameter pass null, invokes the ViewGroup own dispatchTouchEvent method)
            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;
            // Traverse the list
            while(target ! =null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    // If an event has already been distributed to newTouchTarget, mark the event for consumption.
                    handled = true;
                } else {
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    / / events through dispatchTransformedTouchEvent dispatched to the child
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        // If the child consumed the event, then the flag "HANDLED" is true
                        handled = true;
                    }
                    if (cancelChild) {
                        // If the child is cancelled, the corresponding TouchTarget is removed from the list and the TouchTarget is removed
                        // Reclaim TouchTarget into the object cache pool.
                        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) {
            // If the event is cancelled or the event sequence ends, the TouchTarget list is cleared and other states and markers are reset.
            resetTouchState();
        } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
            // Remove the touch ID from all touchTargets if the event subsequence for a touch point ends.
            // If there is a TouchTarget and the ID is empty, remove the TouchTarget again.
            final int actionIndex = ev.getActionIndex();
            final int idBitsToRemove = 1<< ev.getPointerId(actionIndex); removePointersFromTouchTargets(idBitsToRemove); }}if(! handled && mInputEventConsistencyVerifier ! =null) {
        mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
    }
    return handled;
}
Copy the code

In the distribution phase, the TouchTarget linked list is distributed. In the previous process of finding the dispatch target, the TouchTarget will be saved in the linked list with mFirstTouchTarget as the head node. Therefore, it is only necessary to traverse the linked list for dispatch.

MFirstTouchTarget instructions

Instead of saving the child that consumes the event in a single TouchTarget, the ViewGroup saves multiple TouchTargets in a linked mFirstTouchTarget list, because in the case of multi-touch, the event needs to be split and sent to different children.

Assume that both childA and childB can respond to events:

  • When touch point 1 falls on childA, the event ACTION_DOWN is generated, and the ViewGroup generates a TouchTarget for childA to which subsequent slide events are sent.
  • When touch point 2 falls on childA, an ACTION_POINTER_DOWN event is generated, which can reuse the TouchTarget and add the ID of touch point 2 to it.
  • When touch point 3 falls on childB, an ACTION_POINTER_DOWN event is generated, and the ViewGroup is regenerated as a TouchTarget. At this point, there are two Touchtargets in the ViewGroup, and then a sliding event is generated, which will split the event according to the touch point information. The split event is then sent to the corresponding child.

conclusion

In the event dispatching process of ViewGroup, only when the event sequence starts or the subsequence starts (ACTION_DOWN or ACTION_POINTER_DOWN), the subview will be traversed to find the dispatching target. The target is encapsulated as a TouchTarget and saved in the mFirstTouchTarget linked list. After the target lookup is completed, the TouchTarget linked list is traversed again and events are distributed in turn.

To answer the first question, the ViewGroup does not need to iterate over the Child query every time an event arrives. The ViewGroup will save the view of the consuming event in the TouchTarget list, and the next event can be sent directly to the target view through the linked list.