Interpreting the event distribution mechanism of View in Android

Posted by tony_l on Tue, 12 Oct 2021 22:08:20 +0200

Sequence of event distribution

Activity->Window->DecorView->ViewGroup->View

Type of event

ACTION_DOWN,ACTION_MOVE,ACTION_UP,ACTION_CANCEL
Usually an event sequence is such an action_ The down event is the starting point of an event, followed by multiple actions_ Down event, followed by ACTION_DOWN, an action may be received in the middle_ Down event

Event distribution for Activity

// dispatchTouchEvent method of Activity
public boolean dispatchTouchEvent(MotionEvent ev) {
    if (ev.getAction() == MotionEvent.ACTION_DOWN) {
        onUserInteraction();
    }
    if (getWindow().superDispatchTouchEvent(ev)) {
        return true;
    }
    return onTouchEvent(ev);
}

It can be seen that the Activity actually calls the superDispatchTouchEvent method of Window, and the implementation class of Window is PhoneWindow. Therefore, we can directly view the superDispatchTouchEvent method of PhoneWindow

Event distribution for Window

// superDispatchTouchEvent method of PhoneWindow
@Override
public boolean superDispatchTouchEvent(MotionEvent event) {
    return mDecor.superDispatchTouchEvent(event);
}

It is found that the superDispatchTouchEvent method of DecorView is called directly, and then check it further

//superDispatchTouchEvent method of DecorView
public boolean superDispatchTouchEvent(MotionEvent event) {
    return super.dispatchTouchEvent(event);
}

Originally, the dispatchTouchEvent method of ViewGroup is called here, that is, the events on the interface are directly passed to the dispatchTouchEvent method of the root layout

Analyze how the ViewGroup distributes events. Take a look at the dispatchTouchEvent method of the ViewGroup

Before analyzing the dispatchTouchEvent method, take a look at the dispatchtransformatedtouchevent method. The dispatchtransformatedtouchevent method will be called many times inside the dispatchTouchEvent method

private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // If the operation is cancelled, the cancellation event will be distributed directly
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        // If the passed in child is not empty, call the dispatchTouchEvent method of the child; otherwise, call its own dispatchTouchEvent method
        if (child == null) {
            //It is equivalent to calling the dispatchTouchEvent method of the View class
            handled = super.dispatchTouchEvent(event);
        } else {
            //Continue distributing events to child views
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    ......

    // If the passed in child is not empty, call the dispatchTouchEvent method of the child; otherwise, call its own dispatchTouchEvent method
    if (child == null) {
        handled = super.dispatchTouchEvent(transformedEvent);
    } else {
        ......
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    ......
    return handled;
}

As you can see, the dispatchTransformedTouchEvent method mainly does two things

  • If the incoming event is action_ If cancel or cancel parameter is true, action will be distributed directly_ Cancel event
  • During the distribution process, if the child is empty, the super.dispatchTouchEvent method of the current View will be called because the dispatchTouchEvent method of ViewGroup will be overridden. At this time, calling the super method is to call the dispatchTouchEvent method of View; If child is not empty, the dispatchTouchEvent method of this child View will be called.

Let's analyze the core code of dispatchTouchEvent

@Override
public boolean dispatchTouchEvent(MotionEvent ev) {
    ......
    boolean handled = false;
    if (onFilterTouchEventForSecurity(ev)) {
        final int action = ev.getAction();
        final int actionMasked = action & MotionEvent.ACTION_MASK;
       
        // 1. Initialize the down event, clear TouchTargets and touchstate, mfirsttouchtarget = null, disallowIntercept=false
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            cancelAndClearTouchTargets(ev);
            resetTouchState();
        }

        // 2. Check whether it is intercepted
        final boolean intercepted;
        if (actionMasked == MotionEvent.ACTION_DOWN
                || mFirstTouchTarget != null) {
            // Whether to force not to allow interception. The child View can set the parent to force not to allow interception. The default is false
            final boolean disallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) != 0;
            if (!disallowIntercept) {
                //Ask yourself whether to intercept the event
                intercepted = onInterceptTouchEvent(ev);
                ev.setAction(action); // restore action in case it was changed
            } else {
                intercepted = false;
            }
        } else {
            intercepted = true;
        }

        ......
        // 3. If it is not intercepted, handle the DOWN event first, mainly assigning TouchTarget
        TouchTarget newTouchTarget = null;
        boolean alreadyDispatchedToNewTouchTarget = false;
        if (!canceled && !intercepted) {
            ......
            if (actionMasked == MotionEvent.ACTION_DOWN
                    || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                ......
                final int childrenCount = mChildrenCount;
                if (newTouchTarget == null && childrenCount != 0) {
                    ......
                    for (int i = childrenCount - 1; i >= 0; i--) {
                        final int childIndex = getAndVerifyPreorderedIndex(
                                childrenCount, i, customOrder);
                        final View child = getAndVerifyPreorderedView(
                                preorderedList, children, childIndex);
                        ......
                        // Find the child View that is Visible and in the click range
                        if (!canViewReceivePointerEvents(child)
                                || !isTransformedTouchPointInView(x, y, child, null)) {
                            ev.setTargetAccessibilityFocus(false);
                            continue;
                        }
                        ......
                        // This is equivalent to calling the dispatchTouchEvent method of the child View
                        if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                            ......
                            // Assign TouchTarget and refresh the flag bit mFirstTouchTarget
                            newTouchTarget = addTouchTarget(child, idBitsToAssign);
                            alreadyDispatchedToNewTouchTarget = true;
                            break;
                        }
                        ......
                    }
                    ......
                }
                ......
            }
        }

        // 4. Do you want to handle the event yourself or let the child View handle the event
        if (mFirstTouchTarget == null) {
            // If there is no child View consumption event, it will be consumed by itself, which is equivalent to calling the super.dispatchTouchEvent method
            handled = dispatchTransformedTouchEvent(ev, canceled, null,
                    TouchTarget.ALL_POINTER_IDS);
        } else {
            TouchTarget predecessor = null;
            TouchTarget target = mFirstTouchTarget;
            while (target != null) {
                final TouchTarget next = target.next;
                // If it is a DOWN event, the dispatchTouchEvent method of the child View has been called above, and nothing needs to be done
                if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                    handled = true;
                } else {
                    // Decide whether to force the event to CANCEL event according to intercepted
                    final boolean cancelChild = resetCancelNextUpFlag(target.child)
                            || intercepted;
                    // This is equivalent to calling the dispatchTouchEvent method of the child View. If intercepted=true, the action will be forcibly changed to CANCEL; If intercepted=false, then
                    if (dispatchTransformedTouchEvent(ev, cancelChild,
                            target.child, target.pointerIdBits)) {
                        handled = true;
                    }
                    // If intercepted=true, set mFirstTouchTarget to null
                    if (cancelChild) {
                        if (predecessor == null) {
                            mFirstTouchTarget = next;
                        } else {
                            predecessor.next = next;
                        }
                        target.recycle();
                        target = next;
                        continue;
                    }
                }
                predecessor = target;
                target = next;
            }
        }
        ......
    }
    ......
    return handled;
}

The dispatchTouchEvent method is mainly composed of four modules

  • Initialize the DOWN event, clear TouchTargets and touchstate, mfirsttouchtarget = null, disallowIntercept=false
  • Check for interception
  • If it is not intercepted, handle the DOWN event first, mainly assigning TouchTarget
  • Do you want to handle the events yourself or let the child View handle the events

At this point, the dispatchTouchEvent of ViewGroup is finished. Let's summarize

The down event ViewGroup will clear mFirstTouchTarget and reset disallowuncept flag to false, so every time the down event comes, the parent container will call onInterceptTouchEvent to ask whether it is intercepted. If the down event is intercepted, the down event will be handed over to ViewGroup for processing. At this time, mFirstTouchTarget=null, so when the next move/up event comes, The parent container directly intercepted=true, so once the ViewGroup intercepts the down event, the subsequent events will be handled by itself, and the child View will not receive any events

When the down event ViewGroup is not intercepted, the ViewGroup will traverse the child views to find the View that can handle the event. If it is not found, the same event will still be handled by ViewGroup, and subsequent events will be handled by him. When the ViewGroup finds the child View that handles the down event, it will assign mFirstTouchTarget, that is, mFirstTouchTarget when the next move event comes= Null

When the move event comes, the ViewGroup still calls back to use onInterceptTouchEvent to ask whether it is intercepted. If it is intercepted, mFirstTouchTarget=null. According to the above analysis, this time the child View will receive a cancel event, and the ViewGroup will clear mFirstTouchTarget=null. Since mFirstTouchTarget=null, all subsequent events will be handled by the ViewGroup itself.

Analyze how the View distributes events. dispatchTouchEvent(MotionEvent ev)

public boolean dispatchTouchEvent(MotionEvent event) {
        boolean result = false;
        //Here, you can judge whether the mtouchlistener is set, whether it is ENABLED, and the return value of onTouch
        //So the priority of ontouch > ontouchevent
        if (onFilterTouchEventForSecurity(event)) {
            ListenerInfo li = mListenerInfo;
            if (li != null && li.mOnTouchListener != null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }
            //onTouchEvent will be called here
            if (!result && onTouchEvent(event)) {
                result = true;
            }
        }
        return result;
    }

To sum up, first of all, if the onotouchlistener is set, its priority is greater than the onTouchEvent. onTouchEvent will not be called when its onTouch method returns true

Analyze how View consumes onTouchEvent(MotionEvent ev) of events

public boolean onTouchEvent(MotionEvent event) {
        final float x = event.getX();
        final float y = event.getY();
        final int viewFlags = mViewFlags;
        final int action = event.getAction();

        //Here you can judge whether the view is clickable
        final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        //If the view status is disabled, events can be consumed as long as it is clickable, but no response is made
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if (action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) != 0) {
                setPressed(false);
            }
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            // A disabled view that is clickable still consumes the touch
            // events, it just doesn't respond to them.
            return clickable;
        }
        //How to set mTouchDelegate here? mTouchDelegate.onTouchEvent(event) will be called first
        if (mTouchDelegate != null) {
            if (mTouchDelegate.onTouchEvent(event)) {
                return true;
            }
        }

        //As long as it can be clicked, it will come in, and eventually return true consumption event
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                      ...
                        //Click event onClick is handled here
                         performClickInternal();
                    break;

                case MotionEvent.ACTION_DOWN:
                    ....
                    if (!clickable) {
                        checkForLongClick(0, x, y);
                        break;
                    }
                    ....
                    if (isInScrollingContainer) {
                        ....
                        postDelayed(mPendingCheckForTap,
                    } else {
                        ....
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    ....
                case MotionEvent.ACTION_MOVE:
                    ....
                    break;
            }

            return true;
        }

        return false;
    }

To sum up, events can be consumed as long as the View can be clicked. If mTouchDelegate is set, the onTouchEvent method of mTouchDelegate will be executed first, and onLongClick will be executed in action_down, finally to
In the performLongClick method, see the following

public boolean performLongClick() {
        return performLongClickInternal(mLongClickX, mLongClickY);
    }
   
 private boolean performLongClickInternal(float x, float y) {
        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_LONG_CLICKED);

        boolean handled = false;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnLongClickListener != null) {
            handled = li.mOnLongClickListener.onLongClick(View.this);
        }
        if (!handled) {
            final boolean isAnchored = !Float.isNaN(x) && !Float.isNaN(y);
            handled = isAnchored ? showContextMenu(x, y) : showContextMenu();
        }
        if ((mViewFlags & TOOLTIP) == TOOLTIP) {
            if (!handled) {
                handled = showLongClickTooltip((int) x, (int) y);
            }
        }
        if (handled) {
            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
        }
        return handled;
    }

Get called. onClick is executed in action_up, finally to
In the performClick method, see the following

public boolean performClick() {
        ....
        final boolean result;
        final ListenerInfo li = mListenerInfo;
        if (li != null && li.mOnClickListener != null) {
            playSoundEffect(SoundEffectConstants.CLICK);
            li.mOnClickListener.onClick(this);
            result = true;
        } else {
            result = false;
        }

        return result;
    }

, priority onlongclick > onclick

Solutions to incident conflicts

When an event comes, more than one View can handle the event. At this time, the parent container does not know who will distribute the event for processing. At this time, an event conflict occurs. Through the above analysis, we can get two ideas. The first external interception method is to change the return value of the onIntercept method of ViewGroup. The second one interferes with the distribution process of ViewGroup by changing the disallowIntercept flag

Idea 1: external interception method template code
public boolean onInterceptTouchEvent(MotionEvent ev) {
        boolean intercepted = false;
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                intercepted = false;
                break;
            case MotionEvent.ACTION_MOVE:
                if (Event required by parent container) {
                    intercepted = true;
                } else {
                    intercepted = false;
                }
                break;
            case MotionEvent.ACTION_UP:
                intercepted = false;
                break;
            default:
                break;
        }
        return intercepted;
    }

Idea 2: internal interception method template code

public boolean dispatchTouchEvent(MotionEvent ev) {
        int action = ev.getAction();
        switch (action) {
            case MotionEvent.ACTION_DOWN:
                getParent().requestDisallowInterceptTouchEvent(true);
                break;
            case MotionEvent.ACTION_MOVE:
                if (Event required by parent container) {
                    getParent().requestDisallowInterceptTouchEvent(false);
                }
                break;
            case MotionEvent.ACTION_UP:
                break;
            default:
                break;
        }
        return super.dispatchTouchEvent(ev);
    }

Topics: Android view