Based on Android 28 source code analysis

The event distribution of click events is actually the distribution process of MotionEvent events, that is, when a MotionEvent is generated, the system needs to transmit the event to a specific View, and the delivery process is the distribution process.

Three important methods

First we need to introduce three important methods in the click event distribution process:

dispatchTouchEvent

Used for event distribution. This method must be called if the event can be passed to the current View, and the return result is affected by the current View’s onTouchEvent and the lower View’s dispatchTouchEvent methods, indicating whether the current event is consumed.

onInterceptTouchEvent

Called within dispatchTouchEvent to determine whether to intercept an event. If the current View intercepted an event, this method is not called again within the same sequence of events and returns the result indicating whether to intercept the current event.

onTouchEvent

Called within dispatchTouchEvent to handle click events and returns a result indicating whether the current event is consumed. If not, the current View cannot receive the event again in the same sequence of events.

In fact, their relationship can be expressed as the following pseudocode:

public boolean dispatchTouchEvent(MotionEvent ev) {

    if (onInterceptTouchEvent(ev)) {
        return onTouchEvent(ev);
    }
    
    return child.dispatchTouchEvent(ev);
}
Copy the code

For a root ViewGroup, a click event is first passed to its dispatchTouchEvent method. If the onInterceptTouchEvent returns true, it intercepts the current event. The event is then handed to the ViewGroup’s onTouchEvent method. If onInterceptTouchEvent returns false, it does not intercept the current event, which is passed to its children, whose dispatchTouchEvent handles the click event, and so on until the event is finally handled.

Source code analysis of event distribution

When a click event occurs, it is passed in the following order: Activity -> Window -> View. That is, the event is always passed to the Activity, the Activity to the Window, and finally the Window to the top-level View. After receiving the event, the top-level View distributes the event according to the event distribution mechanism.

Activity Distributes click events

// Activity.java

    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }
Copy the code

Click events are represented by MotionEvent. When a click occurs, the dispatchTouchEvent of the current Activity is used to distribute the event. The specific work is done by the Window inside the Activity. If true is returned, the event loop is complete. Returning false means that the event is not being handled. All View onTouchEvents return false, and the Activity’s onTouchEvent will be called.

Window distribution of click events

Now look at how the Window passes events to the ViewGroup. If you look at the source code, you’ll see that Window is an abstract class, and Window’s superDispatchTouchEvent method is an abstract method, so you have to find the Window implementation class. You can see from the comments that the only implementation of Window is PhoneWindow, so let’s take a look at how PhoneWindow handles click events.

// PhoneWindow.java

    @Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
Copy the code

PhoneWindow passes the event directly to the DecorView, GetDecorView ().findViewById(Android.r.i.C.Ontent)).getChildat (0) The mDecor is clearly the View returned by getWindow().getDecorView(), and the View we set with setContentView is a child of it. Since the DecorView inherits child FrameLayout and is the parent View, the final event is passed to the View. From here, the event is passed to the top-level View, that is, the View set by setContentView in the Activity, which is generally a ViewGroup

topViewDistribution of click events

First, the ViewGroup dispatchTouchEvent distribution process is mainly implemented in the ViewGroup dispatchTouchEvent method. This method has a lot of code and is explained in sections.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...// Check for interception.
            final boolean intercepted;
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) { // Determine whether to intercept the current event
                    
                // Use the FLAG_DISALLOW_INTERCEPT bit to determine whether to intercept
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                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; }... }Copy the code

As you can see from the above code, when the event type is ACTION_DOWN or mFirstTouchTarget! = null to determine whether to intercept the current event. The ACTION_DOWN event is easy to understand, so mFirstTouchTarget! What does = null mean? MFristTouchTarget is assigned to the child element when the event is successfully handled by the child element of the ViewGroup. That is, when the event is intercepted by the current ViewGroup and not handled by the child element, MFristTouchTarget == null, then when ACTION_MOVE and ACTION_UP events arrive, Because of the (actionMasked = = MotionEvent. ACTION_DOWN | | mFirstTouchTarget! If this condition is false, the onInterceptTouchEvent of the ViewGroup will not be called again, and all other events in the same sequence will be assigned to the ViewGroup by default.

There is a special case, that is, FLAG_DISALLOW_INTERCEPT tag, the tag bit is set by requestDisallowInterceptTouchEvent method, commonly used in child View. FLAG_DISALLOW_INTERCEPT Once set, ViewGroup cannot block click events other than ACTION_DOWN. Why events other than ACTION_DOWN? This is because the ViewGroup will reset the FLAG_DISALLOW_INTERCEPT bit if it is ACTION_DOWN when distributing the event, invalidating the bit set in the child View.

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...// Handle an initial down.
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // 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(); // Reset the FLAG_DISALLOW_INTERCEPT flag
            }

            // Check for interception.
            final booleanintercepted; . }Copy the code

In the above code, ViewGroup resets the status of an ACTION_DOWN event, and in resetTouchState resets FLAG_DISALLOW_INTERCEPT, So the child View call requestDisallowInterceptTouchEvent method will not affect ViewGroup ACTION_DOWN event processing.

It can be concluded from the above that when the ViewGroup decides to intercept an event, subsequent click events are assigned to it by default and its onInterceptTouchEvent method is not called. So onIntecepterTouchEvent is not going to be called every time, so if we want to process all the clicks ahead of time, we have to select dispatchTouchEvent, which is the only method that’s guaranteed to be called every time, That is, if the event can be passed to the current ViewGroup.

Now, when a ViewGroup is not intercepting an event, the event is propagated down to its child views for processing

// ViewGroup.java

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...if(! canceled && ! intercepted) {// If the event is targeting accessibility focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    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.
                    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;
                        for (int i = childrenCount - 1; i >= 0; i--) { // Iterates through all the children of the ViewGroup to determine whether the children can receive the click event
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If there is a view that has accessibility focus we want it
                            // to get the event first and if not handled we will perform a
                            // normal dispatch. We may do a double iteration but this is
                            // safer given the timeframe.
                            if(childWithAccessibilityFocus ! =null) {
                                if(childWithAccessibilityFocus ! = child) {continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            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.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) { // The dispatchTouchEvent method of the child element is actually called
                                // 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();
                                // mFirstTouchTarget is assigned and the for loop is broken
                                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();
                    }

                    if (newTouchTarget == null&& mFirstTouchTarget ! =null) {
                        // Did not find a child to receive the event.
                        // Assign the pointer to the least recently added target.
                        newTouchTarget = mFirstTouchTarget;
                        while(newTouchTarget.next ! =null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; }}}... }Copy the code

The logic of the above code is to first iterate through all the children of the ViewGroup and then determine whether the children can receive the click event. The acceptability of click events is mainly measured by two points:

  • Whether the child element is playing an animation
  • Click whether the coordinates of the event fall within the region of the child element

If the child element satisfies these two conditions, the event is passed to it for processing. Passed by dispatchTransformedTouchEvent method to complete

// ViewGroup.java

    private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
            View child, int desiredPointerIdBits) {
        final booleanhandled; .// Perform any necessary transformations and dispatch.
        if (child == null) {
            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()); } handled = child.dispatchTouchEvent(transformedEvent); }...return handled
    }
Copy the code

You can see that if the child passes something other than NULL, it calls the child’s dispatchTouchEvent method directly, so that the event is handled by the child, completing a round of event distribution.

If the child’s dispatchTouchEvent returns true, then the mFirstTouchTarget is assigned and the for loop is broken, The actual assignment of mFirstTouchTarget is done by the addTouchTarget function.

// ViewGroup.java

    private TouchTarget addTouchTarget(@NonNull View child, int pointerIdBits) {
        final TouchTarget target = TouchTarget.obtain(child, pointerIdBits);
        target.next = mFirstTouchTarget;
        mFirstTouchTarget = target;
        return target;
    }
Copy the code

As you can see from the code, mFirstTouchTarget is a single linked list data structure. If mFirstTouchTarget is null, then the ViewGroup intercepts all subsequent clicks in the same sequence by default. This point has been analyzed above.

If the event is not handled properly after iterating through all child elements, there are two cases:

  1. ViewGroupThere are no children
  2. The child element handles the click event, but indispatchTouchEventIn the backfalseThat’s usually because the child element is inonTouchEventIn the backfalse

In both cases, the ViewGroup handles the click itself

// ViewGroup.java

    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {...// Dispatch to touch targets.
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                handled = dispatchTransformedTouchEvent(ev, canceled, null, TouchTarget.ALL_POINTER_IDS); }... }Copy the code

DispatchTransformedTouchEvent in the code in the incoming child is null, the signature analysis can know, it will be called super. DispatchTouchEvent (event), obviously, So we’re going to go to the View’s dispatchTouchEvent method, which is clicking on the event and sending it to the View.

View handles the click event

// View.java

    public boolean dispatchTouchEvent(MotionEvent event) {...if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            if(li ! =null&& li.mOnTouchListener ! =null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if(! result && onTouchEvent(event)) { result =true; }}...return result;
    }
Copy the code

The View’s handling of the click event is simpler because the View (excluding the ViewGroup) is a single element that has no child elements and therefore cannot pass events down, so it has to handle the event itself. If the onTouchListener method returns true, then the onTouchEvent will not be called. As you can see, onTouchListener takes precedence over onTouchEvent, which makes it easier to process the click event from the outside.

// View.java

    public boolean onTouchEvent(MotionEvent event) {...final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
                || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

        if ((viewFlags & ENABLED_MASK) == DISABLED) { // Unusable views consume click events as well
            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.
            returnclickable; }...// If one of the View's CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE, or TOOLTIP is true, the event will be consumed
        if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if(! clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress =false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
                    if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed. Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

                        if(! mHasPerformedLongPress && ! mIgnoreNextUpEvent) {// This is a tap, so remove the longpress check
                            removeLongPressCallback();

                            // Only perform take click actions if we were in the pressed state
                            if(! focusTaken) {// Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if(! post(mPerformClick)) { performClickInternal(); }}}if (mUnsetPressedState == null) {
                            mUnsetPressedState = new UnsetPressedState();
                        }

                        if (prepressed) {
                            postDelayed(mUnsetPressedState,
                                    ViewConfiguration.getPressedStateDuration());
                        } else if(! post(mUnsetPressedState)) {// If the post failed, unpress right now
                            mUnsetPressedState.run();
                        }

                        removeTapCallback();
                    }
                    mIgnoreNextUpEvent = false;
                    break; . }return true;
        }

        return false;
    }
Copy the code

In the above code, the event is consumed whenever one of the View’s CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE, or TOOLTIP is true. The onTouchEvent method returns true regardless of whether it is disabled. Then, when the ACTION_UP event occurs, the performClickInternal method is fired, and finally the performClick method is called.

// View.java

    public boolean performClick(a) {
        // We still need to call this method to handle the cases where performClick() was called
        // externally, instead of through performClickInternal()
        notifyAutofillManagerOnClick();

        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;
        }

        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);

        notifyEnterOrExitForAutoFillIfNeeded(true);

        return result;
    }
Copy the code

As you can see from the above code, if the View has OnClickListener set, its onClick method will be called inside the performClick method

conclusion

  1. The same event sequence refers to a series of events generated in the process from the moment the finger touches the screen to the moment the finger leaves the screendownThe event starts with an indefinite number of intermediatemoveEvent, finally toupEnd of the event
  2. aViewOnce a decision is made to intercept, the sequence of events can only be handled by it (if the sequence of events can be passed to it), and itsonIntercepetTouchEventWill not be called again. This is also easy to understand, that is, when aViewOnce you decide to intercept an event, the system hands all other methods in the same sequence of events directly to it, so you don’t have to call it againViewonIntercepterTouchEventAsk it if it’s going to intercept
  3. Normally, a sequence of events can only be oneViewIntercept and consume. The reason for this one can refer to the previous one,Because once an element intercepts this event, all other events in the same sequence of events will be handled by it, so the same sequence of events cannot be handled by twoViewSimultaneous processingBut it can be done through special means, such as oneViewGet things through that you should be handling yourselfonTouchEventForce pass to othersViewTo deal with.
  4. aViewOnce started processing the event, if it does not consumeACTION_DOWNEvent (onTouchEvent returns false), then no other events in the same sequence of events are assigned to it and the event is reassigned to its parent, that is, the parent elementonTouchEventWill be called. It means that once the event is handed to aViewProcess, then it must consume, otherwise the rest of the same sequence of events will not be left for it to process.
  5. ifViewDon’t consume exceptACTION_DOWNOther events, then the click event will disappear, then the parent elementonTouchEventWill not be called, and is currentlyViewYou can continue to receive subsequent events, and eventually those missing click events will be delivered toActivityTo deal with
  6. ViewGroupDoes not intercept any events by default, Android source codeViewGrouponInterceptTouchEventMethod returns by defaultfalse
  7. ViewThere is noonInterceptTouchEventMethod, once a click event is passed to it, then itsonTouchEventThe method will be called
  8. ViewonTouchEventBy default, both consume events (returntrue), unless it is unclickable (both clickable and longClickable arefalse).ViewlongClickableProperty defaults tofalse.clickableProperties have to be cases, likeButtonclickableProperty defaults totrueAnd theTextViewclickableProperty defaults tofalse
  9. ViewenableAttributes do not affectonTouchEventThe default return value of Even if aViewdisableState of, as long as it’sclickableorlongClickableThere is a fortrueSo itsonTouchEventIt returnstrue
  10. onClickIt’s going to happen if it’s presentViewIt’s clickable, and it’s receiveddownupIn the event
  11. Event passing is outside-in, meaning that events are always passed to the parent and then to the childViewThrough therequestDisallowInteceptTouchEventMethod can intervene in the event distribution of a parent element in a child element, butACTION_DOWNExcept for the event
  12. toViewSet up theOnTouchListener, its priority ratioonTouchEventBe high, ifOnTouchListeneronTouchMethodtruethenonTouchEventMethod will not be called. If the returnfalse, the currentViewonTouchEventThe method is called back.

reference

  • Android development art exploration