This is the second article in a series of Android Touch events.

  1. “Recursion” and “Return” of Android Touch Event Distribution (1)
  2. “Recursion” and “Return” of Android Touch Event Distribution (II)

Extend the story of the leader’s assignment in the previous chapter:

The big leader will go through a “handing” process when arranging tasks: the big leader first tells the task to the small leader, and then the small leader tells the task to Xiao Ming. There may also be a “return” process: Xiaoming tells the small leader that he can’t do it, and the small leader tells the big leader that he can’t finish the task. And then, there was no and… But if the task is completed this time, the leader will continue to assign later tasks to Xiao Ming.

The extended part of the story is similar to today’s ACTION_DONW post-sequence events, but we’ll start by answering another question left over from the previous post: “Intercepting events” :

Intercept events

The ViewGroup also has a bit of interception logic before iterating over the child distribution touch event:

public abstract class ViewGroup extends View implements ViewParent.ViewManager {

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...// Check for interception.
            // Check whether the ViewGroup intercepts the delivery of touch events
            final boolean intercepted;
            // The first condition intercepts ACTION_DOWN events
            // The second condition indicates that the intercepting ACTION_DOWN event has been distributed to the child, and now the sequenced event is intercepted
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
                // Check whether interception is allowed
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                // Intercept touch events distributed to children
                if(! disallowIntercept) { 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; }...// When the event is not intercepted, distribute it to the child
            if(! canceled && ! intercepted) {// Traverse the children and distribute events to them
                // If a child claims to consume the event, add it to the touch chain
                // This logic was analyzed in the previous article and omitted here}}// Distribute touch events to the touch chain
        if (mFirstTouchTarget == null) { // There is no touch chain
            // If the event is intercepted by the ViewGroup, the touch chain is empty and the ViewGroup consumes the event itself
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } else{... }}// Return true to intercept the event. False is returned by default
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        return false;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {...if (child == null) {
            // None of the ViewGroup children want to receive touch events or if touch events are intercepted it treats itself as a View (call view.dispatchTouchEvent ())
            handled = super.dispatchTouchEvent(transformedEvent); }... }}Copy the code

OnInterceptTouchEvent () is called when intercepting is allowed. If this method is overloaded and returns true, the ViewGroup intercepts the event. Instead of distributing the event to the child, the event is consumed by the child itself (eventually going to viewGroup.onTouchEvent () by calling view.dispatchTouchEvent ()).

To sum up:

  • The black arrow in the figure represents the path of touch event delivery and the gray arrow represents the backtracking path of touch event consumption.onInterceptTouchEvent()returntrue, resulting inonTouchEvent()Is called becauseonTouchEvent()returntrue, resulting indispatchTouchEvent()returntrue.
  • To be precise, the beneficiaries of touch interception are all at the topViewGroup(including yourself), because touch events no longer go downViewPass.

, ACTION_MOVE, ACTION_UP

The last article in reading the source code, buried a foreshadowing, now complete it:

public abstract class ViewGroup extends View implements ViewParent.ViewManager {
    // Touch the chain header
    privateTouchTarget mFirstTouchTarget; .@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if(! canceled && ! intercepted) { ...// The child who is looking for the consumption touch event is traversed when ACTION_DOWN, and if it is found, it is added to the touch chain
            if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // Walk through the child
                for (int i = childrenCount - 1; i >= 0; i--) {
                    ...
                    // Convert the touch coordinates and distribute them to the child (the child argument is not null)
                    if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                          ...
                          // There are children willing to consume touch events and insert them into the "touch chain"
                          newTouchTarget = addTouchTarget(child, idBitsToAssign);
                          // Indicates that the touch event has been distributed to the new touch target
                          alreadyDispatchedToNewTouchTarget = true;
                          break; }... }}}if (mFirstTouchTarget == null) {
                // If no child wants to consume the touch event, consume it (child argument is null)
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
        } 
        // The touch chain is not null, indicating that a child has consumed ACTION_DOWN
        else {
                // Will be foreshadowed to complete
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                // Traverse the touch chain to distribute the subsequent ACTION_DOWN events to the child
                while(target ! =null) {
                    final TouchTarget next = target.next;
                    // From the last analysis, ACTION_DOWN would go here
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        // Returns true if the touch event has already been distributed to the new touch target
                        handled = true;
                    } 
                    //ACTION_DONW goes here
                    else{...// Distributes touch events to touch targets on the touch chain
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true; }... } predecessor = target; target = next; }}...if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                // If it is an ACTION_UP event, the touch chain is cleared
                resetTouchState();
        }

        return handled;
    }
    
    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final booleanhandled; .// Perform any necessary transformations and dispatch.
        // Do the necessary coordinate transformations and then distribute the touch events
        if (child == null) {
            // None of the ViewGroup children want to consume touch events, so it treats itself as a View (call view.dispatchTouchEvent ())
            handled = super.dispatchTouchEvent(transformedEvent);
        } else {
            final float offsetX = mScrollX - child.mLeft;
            final float offsetY = mScrollY - child.mTop;
            transformedEvent.offsetLocation(offsetX, offsetY);
            if (! child.hasIdentityMatrix()) {
                transformedEvent.transform(child.getInverseMatrix());
            }

            // Distribute touch events to childrenhandled = child.dispatchTouchEvent(transformedEvent); }...return handled;
    }
    
    /** * Resets all touch state in preparation for a new cycle
    private void resetTouchState(a) {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
    
    The individual goes down all touch targets
    private void clearTouchTargets(a) {
        TouchTarget target = mFirstTouchTarget;
        if(target ! =null) {
            do {
                TouchTarget next = target.next;
                target.recycle();
                target = next;
            } while(target ! =null);
            mFirstTouchTarget = null; }}}Copy the code

A touch event is a sequence that always starts with ACTION_DOWN, followed by ACTION_MOVE and ACTION_UP. As ACTION_DOWN occurs, ViewGroup. DispatchTouchEvent () will be willing to consume children touch events are stored in a touch in the chain, after the sequence events will be distributed to touch the object on the chain.

To sum up:

  • Represented by the black arrowACTION_DOWNThe path of the event, represented by the gray arrowACTION_MOVEandACTION_UPThe path to the event. That is, as long as there is a view claiming consumptionACTION_DOWN, subsequent events are also passed to it, regardless of whether it claims to consumeACTION_MOVEandACTION_UPIf it does not consume, the subsequent event will look like the previous analysisACTION_DOWNSame way back up to the upper consumption.

  • Represented by the black arrowACTION_DOWNThe path of the event, represented by the gray arrowACTION_MOVEandACTION_UPThe path to the event. That is, none of the views consumeACTION_DOWN, the subsequent sequential events are only passed toActivity.onTouchEvent().

ACTION_CANCEL

The story of the task assigned by the leader continues to extend: the big leader assigned task 1 to the small leader, and the small leader passed him to Xiao Ming, and Xiao Ming completed it. Then the big leader assigned task 2 to the small leader, the small leader decided to deal with task 2 by himself, so he and Xiao Ming said after the sequence task I will take over, you can be busy with other things.

The story corresponds to the touch event delivery scenario: The Activity passes ACTION_DOWN to the ViewGroup, which passes it to the View, which claims to consume ACTION_DOWN. The Activity continues to pass ACTION_MOVE to the ViewGroup, but the ViewGroup intercepts it, and the ViewGroup sends the ACTION_CANCEL event to the View.

Take a look at the source:

public abstract class ViewGroup extends View implements ViewParent.ViewManager {
    public boolean dispatchTouchEvent(MotionEvent ev) {
        // Check whether the ViewGroup intercepts the delivery of touch events
        final boolean intercepted;
        if(actionMasked == MotionEvent.ACTION_DOWN|| mFirstTouchTarget ! =null) {
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                // Intercept touch events distributed to children
                if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action);// restore action in case it was changed
                } else {
                    intercepted = false; }}...// If the child consumes the ACTION_DOWN event, it will be added to the touch chain here
        if(! canceled && ! intercepted) { ... }// Distribute touch events to the touch chain
        if (mFirstTouchTarget == null) { // No touch chain indicates that no children in the current ViewGroup are willing to receive touch events
            // Distribute the touch event to yourself
        } 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 touch chain to distribute touch events to all children who want to receive them
            while(target ! =null) {
                final TouchTarget next = target.next;
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    CancelChild is true if the event is intercepted
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // Pass the ACTION_CANCEL event to the child
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // Remove the child from the touch chain if the ACTION_CANCEL event is sent
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue; } } predecessor = target; target = next; }}... }private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,View child, int desiredPointerIdBits) {
        final boolean handled;

        Canceling Canceling is a special case. We don't need to perform any anything
        // or filtering. The important part is the action, not the contents.
        final int oldAction = event.getAction();
        if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
            event.setAction(MotionEvent.ACTION_CANCEL);
            if (child == null) {
                handled = super.dispatchTouchEvent(event);
            } else {
                // Pass the ACTION_CANCEL event to the child
                handled = child.dispatchTouchEvent(event);
            }
            event.setAction(oldAction);
            returnhandled; }... }Copy the code

When the child consumes the ACTION_DOWN event, its reference is saved in the parent’s touch chain. When the father intercepts a post-sequence event, the father sends an ACTION_CANCEL event to the child on the touch chain and removes the child from the touch chain. The subsequent events pass to the father.

conclusion

After the analysis of two articles, we have a preliminary understanding of the distribution of Android touch events and draw the following conclusions:

  • ActivityWhen the touch event is received, it is passed toPhoneWindowAnd pass it toDecorViewBy theDecorViewcallViewGroup.dispatchTouchEvent()Distribute from the top downACTION_DOWNTouch events.
  • ACTION_DOWNEvents throughViewGroup.dispatchTouchEvent()fromDecorViewThrough a number ofViewGroupIt goes on and on until it gets thereView.
  • Each level can be passed inonTouchEvent()orOnTouchListener.onTouch()returntrueTo tell their parent control that the touch event was consumed. In the case that the parent control does not intercept events, the parent control has the opportunity to consume touch events itself only if the underlying control does not consume them.
  • The delivery of touch events is a top-down “recursive” process from the root view, while the consumption of touch events is a bottom-up “recursive” process.
  • ACTION_MOVEandACTION_UPIt’s going to followACTION_DOWNIs passed to the consumerACTION_DOWNIf the control does not declare consumption of these post-sequence eventsACTION_DOWNIt will also backtrack up to allow its parent control to consume.
  • The parent control can be accessed through theonInterceptTouchEvent()returntrueTo intercept the transmission of events to their children. If the child has consumedACTION_DOWNAfter the event is intercepted, the parent control is sentACTION_CANCELFor their children.