“This is my fourth day of the November Gwen Challenge.The final text challenge in 2021”


preface

In Android, the event distribution mechanism is a very important knowledge, master this mechanism can help you in the peacetime development to solve a lot of View event conflict problem, this question is also asked in the interview a lot of questions, this article will summarize the knowledge.

Event Distribution Cause

In Android, the View on the page is displayed in a tree structure, and the View will overlap. When we click on a place where there are multiple views that can respond, who should the click event be given? In order to solve this problem, an event distribution mechanism is needed

Event dispatch object

Touch event, which is passed to the View for each Touch event (MotionEvent), depending on the logic of the receiver

When the user touches the screen, Touch events (encapsulated as MotionEvent objects) are generated, which can be divided into the following categories

  • Motionevent. ACTION_DOWN: This event is generated when the finger clicks on the screen. It is the start of all events
  • MotionEvent.ACTION_MOVE: This event is generated when a finger is swiped across the screen
  • Motionevent. ACTION_CANCLE: Terminates the current event for a non-human cause
  • Motionevent. ACTION_UP: This event is generated when a finger leaves the screen

A complete Touch event is the process from the user’s finger touching the screen (accompanied by an ACTIONDOWN event) to the user’s finger leaving the screen (accompanied by an ACTIONUP event)

ACTIONDOWN (once) –> ACTIONMOVE (N times) –> ACTION_UP (once)

Event distribution method

  • DispatchTouchEvent (MotionEvent EV) : dispatchTouchEvent(MotionEvent EV) : dispatchTouchEvent(MotionEvent EV) When an event is detected by the underlying driver, it is reported and ultimately handled by the Activity’s method to decide whether to consume it or pass it on
  • OnInterceptTouchEvent (MotionEvent EV) : When an event is distributed to a ViewGroup, it can decide whether to intercept the event. Only the ViewGroup has this method
  • OnTouchEvent (MotionEvent Event) : This is the last method in the event distribution process, that is, whether or not the event is consumed

Event distribution participant

  • Activity: Contains ViewGroup and View
  • ViewGroup: contains viewgroups and views
  • View: Contains no other views, only itself

The usual event distribution direction is Activity –> ViewGroup –>…… –> View

Note:

  • Child View by requestDisallowInterceptTouchEvent methods of intervention in the parent View events distribution (except ACTION_DOWN events), and this is what we deal with conflict of sliding key method in common use
  • If the View sets onTouchListener and returns true in the overridden onTouch method, its onTouchEvent method will not be called, Because onTouch takes precedence over onTouchEvent in the dispatchTouchEvent of the View; The onClick method is also not called because onClick is called back in onTouchEvent

Event Distribution Process

  1. The underlying Input driver reads and writes events from the /dev/inpu/path of the hardware Input device node named event[NUMBER]. (You can use adb shell getevent to view the node under your device.) Android also gets the raw data from these nodes, encapsulates it and provides it to developers; The dispatchTouchEvent method is passed to the DecorView after a series of calls
  2. In the DecorView, events are passed through the Window’s internal interface, Callback, because the Activity implements that interface and the story is dispatched to the Activity; After the Activity gets the event, it dispatches the event to the window where the Activity is located in the dispatchTouchEvent method. The actual type is PhoneWindow. The window in turn passes the event to its top-level view, the DecorView
  3. DecorView is a subclass of FrameLayout, a subclass of ViewGroup, that does not process the event itself, but continues to submit the event to the ViewGroup; An event goes from the Activity to the ViewGroup
  4. The ViewGroup dispatches the event in the dispatchTouchEvent method. If its onInterceptTouchEvent method intercepts the event, it hands it off to its onTouchEvent method. Instead, iterate over its child views and continue to distribute events, stopping as soon as one of the child views consumes the event
  5. The event is passed to the child View’s dispatchTouchEvent method. If OnTouchListener is registered with the child View and returns true, the event distribution ends there. Otherwise it will continue passing the event to the child View’s onTouchEvent method
  6. The child View will call back the View’s onClick listener in the ACTION_UP event. If the child View does not consume the event, it will be passed back to the Activity according to the distribution process. If no one else consumes the Activity (including the Activity itself), the event is destroyed

Event distribution source

The following source code is based on API24

Corresponding to the above process, when there is a Touch event, the steps are as follows

DecorView.dispatchTouchEvent

@Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        final Window.Callback cb = mWindow.getCallback();
        returncb ! =null && !mWindow.isDestroyed() && mFeatureId < 0
                ? cb.dispatchTouchEvent(ev) : super.dispatchTouchEvent(ev);
    }
Copy the code

Cb refers to the Callback interface inside the window, which is implemented by the Activity

Activity.dispatchTouchEvent

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

This method is used by the Activity to handle touch screen events. We can override this method and return true/false so that the event can be intercepted before it is sent to the window. The ViewGroup or View inside the Activity will not receive the event

A touch screen event that starts with ACTION_DOWN must enter the onUserInteraction() method

public void onUserInteraction(a) {}Copy the code

This is an empty method that is called when a button event, touch screen event, or trackball event is dispatched to the Activity; If you want to know how the user interacts with the device while the Activity is running, you can override this method; Note that this method only responds to touch-down gestures, not to subsequent touch-move and touch-up gestures

The counterpart to this method is onUserLeaveHint, which is also an empty method and is called when:

This method is called as part of the Activity lifecycle when the Activity goes into the background while the user is doing something; For example, if the user presses the home button, the current Activity goes into the background and is invoked before onPause. However, this method will not be called if an incoming call causes the Activity to passively enter the background

Next, enter the second if statement

getWindow().superDispatchTouchEvent

GetWindow () gets a Window object, but it is instantiated in the Attach method of the Activity. The actual type is PhoneWindow, and this is where the Callback interface is implemented

final void attach(Context context, ActivityThread aThread,
            Instrumentation instr, IBinder token, int ident,
            Application application, Intent intent, ActivityInfo info,
            CharSequence title, Activity parent, String id,
            NonConfigurationInstances lastNonConfigurationInstances,
            Configuration config, String referrer, IVoiceInteractor voiceInteractor,
            Window window) {... mWindow =new PhoneWindow(this, window);
            mWindow.setCallback(this); . }Copy the code

Here I go to PhoneWindow, as follows

PhoneWindow.superDispatchTouchEvent

// This is the top view of the window
private DecorView mDecor
@Override
    public boolean superDispatchTouchEvent(MotionEvent event) {
        return mDecor.superDispatchTouchEvent(event);
    }
Copy the code

DecorView .superDispatchTouchEvent

public boolean superDispatchTouchEvent(MotionEvent event) {
        return super.dispatchTouchEvent(event);
    }
Copy the code

So, DecorView is a subclass of FrameLayout, and FrameLayout is a subclass of ViewGroup, so you go to the ViewGroup

ViewGroup.dispatchTouchEvent

        @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {

        // Conformance validator for debugging purposes
        if(mInputEventConsistencyVerifier ! =null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        // If the event targets the accessibility focused view and this is it, start
        // normal event dispatch. Maybe a descendant is what will handle the click.
        if (ev.isTargetAccessibilityFocus() && isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        // This variable is used to mark whether the event is consumed
        boolean handled = false;

        Filter touch events according to the application security policy
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & MotionEvent.ACTION_MASK;

            // Handle initialization after initial Down occurs
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // The new ACTION_DOWN event is coming, and you need to cancel and clear the previous touch Targets
                // Clear the mFirstTouchTarget
                cancelAndClearTouchTargets(ev);
                // Reset the touch state
                resetTouchState();
            }

            // Flags whether to intercept events
            final boolean intercepted;

            // When ACTION_DOWN occurs or ACTION_DOWN has already occurred and mFirstTouchTarget is assigned, the ViewGroup is checked to see if it needs to intercept the event.
            // Only ACTION_DOWN event occurs, mFirstTouchTarget! = null
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {

                / / child View can be set by calling the parent View requestDisallowInterceptTouchEvent method mGroupFlags values
                // This tells the parent View whether to intercept the event
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                // If the child view does not tell the parent not to intercept the event, the parent view determines whether it needs to intercept the event
                if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action);// Resume the action in case it has been changed
                } else {
                        // The child View tells the parent View not to intercept events
                    intercepted = false; }}else {
                // When mFirstTouchTarget=null (no child views are assigned to handle) and it is not an Initial Down event (the event has already been initialized), the ViewGroup continues to intercept touches
                // Continue to set to true
                intercepted = true;
            }



            // If the current event is ACTION_CANCEL, or view.mprivateFlags is set to PFLAG_CANCEL_NEXT_UP_EVENT
            // Then the current event is cancelled
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            //split indicates whether the current ViewGroup supports splitting motionEvents into different views
            final booleansplit = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! =0;
            / / new TouchTarget
            TouchTarget newTouchTarget = null;
            // Whether the event is distributed to the new TouchTarget
            boolean alreadyDispatchedToNewTouchTarget = false;
            // Enter the zone without canceling the event and without intercepting the event
            if(! canceled && ! intercepted) {// Distribute events to all subviews, looking for views that can get focus
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                // If there are three kinds of events, we need to traverse the subview
                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;

                    // Clear earlier touch targets for this PointerId
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    // If the current ViewGroup has child Views and newTouchTarget=null
                    if (newTouchTarget == null&& childrenCount ! =0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);

                        // Scan the View from front to back to get the child views that can receive events
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;

                        // Go through all the child views and find one to receive the event
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

                            // If the current child View does not get focus, skip the child View
                            if(childWithAccessibilityFocus ! =null) {
                                if(childWithAccessibilityFocus ! = child) {continue;
                                }
                                childWithAccessibilityFocus = null;
                                i = childrenCount - 1;
                            }

                            // If the current subview is not visible and does not animate or is not within the range of the touch point, skip this subview
                            if(! canViewReceivePointerEvents(child) || ! isTransformedTouchPointInView(x, y, child,null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            // if the TouchTarget corresponding to the child View is found in the TouchTarget list, the View is receiving the event, no need to iterate, exit directly
                            newTouchTarget = getTouchTarget(child);
                            if(newTouchTarget ! =null) {
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }


                            resetCancelNextUpFlag(child);

                            If the child view returns true, it consumes the event and jumps out of the loop
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // Get the TouchDown time
                                mLastTouchDownTime = ev.getDownTime();
                                // Get the TouchDown Index
                                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;
                                }
                                // Get the x and y coordinates of TouchDown
                                mLastTouchDownX = ev.getX();
                                mLastTouchDownY = ev.getY();
                                // Add to the list of touch targets while assigning the mFirstTouchTarget value
                                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) {
                        // No child View received the event, so assign the last touch target to newTouchTarget
                        newTouchTarget = mFirstTouchTarget;
                        while(newTouchTarget.next ! =null) { newTouchTarget = newTouchTarget.next; } newTouchTarget.pointerIdBits |= idBitsToAssign; }}}// mFirstTouchTarget is assigned by the addTouchTarget method;
            // The addTouchTarget method is entered only when ACTION_DOWN is handled.
            // This is why a View that does not consume ACTION_DOWN events does not receive MOVE,UP, etc
            if (mFirstTouchTarget == null) {
                // Then the ViewGroup can handle the event itself
                handled = dispatchTransformedTouchEvent(ev, canceled, null,
                        TouchTarget.ALL_POINTER_IDS);
            } else {
                ACTION_DOWN is received by a sub-view, and subsequent move up events are sent to the touch target
                TouchTarget predecessor = null;
                TouchTarget target = mFirstTouchTarget;
                while(target ! =null) {

                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
                        handled = true;
                    } else {
                        // If view.mPrivateFlags is set to PFLAG_CANCEL_NEXT_UP_EVENT or the event is intercepted by ViewGroup
                        // The child View needs to cancel the event
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;

                        // Continue to distribute events to child views
                        if (dispatchTransformedTouchEvent(ev, cancelChild,
                                target.child, target.pointerIdBits)) {
                            handled = true;
                        }
                        if (cancelChild) {
                            if (predecessor == null) {
                                mFirstTouchTarget = next;
                            } else {
                                predecessor.next = next;
                            }
                            target.recycle();
                            target = next;
                            continue; } } predecessor = target; target = next; }}// Update the touch target list when a lift or cancel event occurs
            if (canceled
                    || actionMasked == MotionEvent.ACTION_UP
                    || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                resetTouchState();
            } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
                // Remove Pointer from TouchTarget according to idBit if it is a finger lift event under multi-touch
                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

This method is a bit verbose and needs to be broken down

Step 1: Event initialization

The first thing that comes in is the ACTION_DOWN event, which requires some initialization:

  • The first thing to do is to clear all touchTargets and set mFirstTouchTarget to null; MFirstTouchTarget is also of type TouchTarget, which is an inner class of ViewGroup that describes the view of a touch and the ID of the pointer it captures; MFirstTouchTarget means that if the event is handled by the child View, the mFirstTouchTarget will be assigned and will point to the child View
  • The second thing is to reset the status value, via FLAGDISALLOWINTERCEPT resets mGroupFlags value
ViewGroup.cancelAndClearTouchTargets
/**
      * 取消和清空所有的 touch targets.
      */
    private void cancelAndClearTouchTargets(MotionEvent event) {
        if(mFirstTouchTarget ! =null) {
            boolean syntheticEvent = false;
            if (event == null) {
                final long now = SystemClock.uptimeMillis();
                event = MotionEvent.obtain(now, now,
                        MotionEvent.ACTION_CANCEL, 0.0 f.0.0 f.0);
                event.setSource(InputDevice.SOURCE_TOUCHSCREEN);
                syntheticEvent = true;
            }

            for(TouchTarget target = mFirstTouchTarget; target ! =null; target = target.next) {
                resetCancelNextUpFlag(target.child);
                dispatchTransformedTouchEvent(event, true, target.child, target.pointerIdBits);
            }
            clearTouchTargets();

            if(syntheticEvent) { event.recycle(); }}}/** * Clear all the 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; }}/** * resets all touch states in preparation for the new cycle. */
    private void resetTouchState(a) {
        clearTouchTargets();
        resetCancelNextUpFlag(this);
        mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        mNestedScrollAxes = SCROLL_AXIS_NONE;
    }
Copy the code

Step 2: Intercept judgment

Next, you need to determine whether you need to intercept events:

So let’s first look at the conditions

            // Flags whether to intercept events
            final boolean intercepted;

            // When ACTION_DOWN occurs or ACTION_DOWN has already occurred and mFirstTouchTarget is assigned, the ViewGroup is checked to see if it needs to intercept the event.
            if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {

                / / child View can be set by calling the parent View requestDisallowInterceptTouchEvent method mGroupFlags values
                // This tells the parent View whether to intercept the event
                final booleandisallowIntercept = (mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0;
                // If the child view does not tell the parent not to intercept the event, the parent view determines whether it needs to intercept the event
                if(! disallowIntercept) { intercepted = onInterceptTouchEvent(ev); ev.setAction(action);// Resume the action in case it has been changed
                } else {
                    // The child View tells the parent View not to intercept events
                    intercepted = false; }}else {
                // When mFirstTouchTarget=null (no child views are assigned to handle) and it is not an Initial Down event (the event has already been initialized), the ViewGroup continues to intercept touches
                // Continue to set to true
                intercepted = true;
            }
Copy the code
  • When the event is an ACTIONDOWN or mFirstTouchTarget! If the event is ACTION = null, the interception will be determined by the first stepOn DOWN, mFirstTouchTarget must be null, so only two cases are entered: ACTIONThe DOWN event comes in and you need to determine the intercept; ACTIONIf a child of the DOWN event receives the event (so that mFirstTouchTarget is assigned), then subsequent events need to determine whether to intercept the event
  • The reverse logic of the above condition is that the event is an ACTIONAn event (such as move or Up) after a DOWN event and mFirstTouchTarget is null indicates that the event is in ACTIONThe DOWN event determines that the event needs to be intercepted or that there is no child View to handle the event, so subsequent events need not be distributed and continue to be intercepted

The first if statement contains the interception judgment logic

  • First get mGroupFlags value, through calculation and child view can be set by calling the parent view requestDisallowInterceptTouchEvent method mGroupFlags values, tell the parent view don’t intercept events
  • If disallowIntercept is true, the child view asks the parent view not to intercept and sets intercepted to false
  • If disallowIntercept is false, the child view does not say do not intercept, so call onInterceptTouchEvent to see if you need to intercept the event
ViewGroup.requestDisallowInterceptTouchEvent
@Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

        if(disallowIntercept == ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) ! =0)) {
            // Return if it has already been set
            return;
        }

        if (disallowIntercept) {
            mGroupFlags |= FLAG_DISALLOW_INTERCEPT;
        } else {
            mGroupFlags &= ~FLAG_DISALLOW_INTERCEPT;
        }

        // Tell the parent view in turn
        if(mParent ! =null) { mParent.requestDisallowInterceptTouchEvent(disallowIntercept); }}Copy the code
ViewGroup.onInterceptTouchEvent
 /** * ViewGroup can block all touch events in this method, the default is not to block events, developers can override this method to decide whether to block events * the following four conditions are true, return true, block events * the first: touch events from the mouse pointer device * the second: Is the touch event ACTION_DOWN * Third: checks if the mouse or stylus button (or combination of buttons) is pressed, i.e. the user must actually press * fourth: is the touch point on the scrollbar */
    public boolean onInterceptTouchEvent(MotionEvent ev) {
        if (ev.isFromSource(InputDevice.SOURCE_MOUSE)
                && ev.getAction() == MotionEvent.ACTION_DOWN
                && ev.isButtonPressed(MotionEvent.BUTTON_PRIMARY)
                && isOnScrollbarThumb(ev.getX(), ev.getY())) {
            return true;
        }
        return false;
    }
Copy the code

Step 3: ACTION_DOWN event distribution

The next step is to iterate through the child View and distribute the ACTION_DOWN event to the child View that can receive the event

  • If the current child View does not get focus, the child View is skipped
  • If the current child View is not visible and does not animate or is not within the range of the touch point, skip this child View
  • If the TouchTarget corresponding to the child View is found in the TouchTarget list, the View is receiving events and does not need to iterate again
  • If the child view in the touch position, dispatchTransformedTouchEvent method is invoked by the event distribution to the view, if this method returns true, prove the view consumption this event, then don’t need to look for the child view receive events, jump out of the traverse
ViewGroup.dispatchTransformedTouchEvent
private boolean dispatchTransformedTouchEvent(MotionEvent event, boolean cancel,
        View child, int desiredPointerIdBits) {
    final boolean handled;

    // When a cancel operation occurs, no further operations are performed
    final int oldAction = event.getAction();
    if (cancel || oldAction == MotionEvent.ACTION_CANCEL) {
        event.setAction(MotionEvent.ACTION_CANCEL);
        if (child == null) {
            handled = super.dispatchTouchEvent(event);
        } else {
            handled = child.dispatchTouchEvent(event);
        }
        event.setAction(oldAction);
        return handled;
    }

    final int oldPointerIdBits = event.getPointerIdBits();
    final int newPointerIdBits = oldPointerIdBits & desiredPointerIdBits;

    // For some reason, an inconsistent operation occurs, and the event is discarded
    if (newPointerIdBits == 0) {
        return false;
    }

    // The main area of distribution
    final MotionEvent transformedEvent;
    // Check whether the expected pointer ID is equal to the event pointer ID
    if (newPointerIdBits == oldPointerIdBits) {
        if (child == null || child.hasIdentityMatrix()) {
            if (child == null) {
                // When no subview exists, ViewGroup calls view.dispatchTouchEvent to distribute the event and viewGroup.onTouchEvent to handle the event
                handled = super.dispatchTouchEvent(event); 
            } else {
                final float offsetX = mScrollX - child.mLeft;
                final float offsetY = mScrollY - child.mTop;
                event.offsetLocation(offsetX, offsetY);
                // Distribute touch events to child viewGroups or views;
                handled = child.dispatchTouchEvent(event);

                event.offsetLocation(-offsetX, -offsetY); // Adjust the position of the event
            }
            return handled;
        }
        transformedEvent = MotionEvent.obtain(event); // Copy the event to create a new MotionEvent
    } else {
        // Separate the event to get the MotionEvent containing newPointerIdBits
        transformedEvent = event.split(newPointerIdBits);
    }

    if (child == null) {
        // When no subview exists, ViewGroup calls view.dispatchTouchEvent to distribute the event and viewGroup.onTouchEvent to handle the event
        handled = super.dispatchTouchEvent(transformedEvent); 
    } else {
        final float offsetX = mScrollX - child.mLeft;
        final float offsetY = mScrollY - child.mTop;
        transformedEvent.offsetLocation(offsetX, offsetY);
        if (! child.hasIdentityMatrix()) {
            // Convert the view's matrix
            transformedEvent.transform(child.getInverseMatrix());
        }
        // Distribute touch events to child viewGroups or views;
        handled = child.dispatchTouchEvent(transformedEvent);
    }

    / / recycling transformedEvent
    transformedEvent.recycle();
    return handled;
}
Copy the code

This method is where the ViewGroup actually handles events, distributing sub-views to consume events, and filtering out irrelevant pointer ids. When the subview is null, the MotionEvent will be sent to the ViewGroup; Not null, the view.dispatchTouchEvent method is finally called to distribute the event.

This method calls and return to ViewGroup. DispatchTouchEvent will call addTouchTarget method

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

You can see that mFirstTouchTarget is assigned here

When a child control consumes an event, mFirstTouchTarget is not null; MFirstTouchTarget is null when the child control does not consume an event or is intercepted

Step 4: ACTIONMOVE ACTIONUP Event Distribution

After step 3, the ViewGroup might find a child View consumption event

  • If the event is intercepted, mFirstTouchTarget== NULL, then the subsequent event eventually calls the view.dispatchTouchEvent method to distribute the event
  • If the ViewGroup has no child views, mFirstTouchTarget==null, repeat as above
  • If there is a child View, but the child View does not consume the event, mFirstTouchTarget== NULL, then repeat as above
  • If there is a child View, and the child View consumption ACTION_DOWN event, but the dispatchTouchEvent returns false (i.e. dispatchTransformedTouchEvent returns false, Then addTouchTarget will not be called), mFirstTouchTarget==null, then do the same
  • Now that mFirstTouchTarget is not null, we need to distribute subsequent events to the View consuming ACTION_DOWN events

Through the ViewGroup. DispatchTouchEvent methods of analysis, we know that regardless of the child View consumer events, will enter the final events dispatchTouchEvent method, that we continue to explore

View.dispatchTouchEvent
/** * Passes the touch event down to the target View, or the View is the target View. * *@returnReturn true if the event was consumed, or false */ otherwise
    public boolean dispatchTouchEvent(MotionEvent event) {...boolean result = false;

        final int actionMasked = event.getActionMasked();
        if (actionMasked == MotionEvent.ACTION_DOWN) {
            // Before the Down event, it stops if there is a scroll operation. If no, no operation is performed
            stopNestedScroll();
        }

        // Filter touch events to apply security policies
        if (onFilterTouchEventForSecurity(event)) {

            if ((mViewFlags & ENABLED_MASK) == ENABLED && handleScrollBarDragging(event)) {
                result = true;
            }

            ListenerInfo li = mListenerInfo;
            // If OnTouchListener is set to the View
            // The view is not disabled
            // and OnTouchListener. OnTouch returns true
            // The View consumes the event and returns true
            if(li ! =null&& li.mOnTouchListener ! =null
                    && (mViewFlags & ENABLED_MASK) == ENABLED
                    && li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

           Return true if onTouchListener. onTouch has no consumption event and the View's onTouchEvent method returns true
            if(! result && onTouchEvent(event)) { result =true; }}// If this is the end of the gesture, clean up after the nested scroll;
        // If we try ACTION_DOWN but we don't want the rest of the gesture, cancel it too.
        if(actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); }return result;
    }
Copy the code

There are two important points here

  • If the developer sets OnTouchListener to listen and returns true in the onTouch method, the view consumes the event
  • If no listener is set, the View’s onTouchEvent method is called to handle the event

Ontouchlistener. onTouch takes precedence over onTouchEvent. As long as onTouchListener. onTouch returns true, the onTouchEvent will not be executed

Now let’s look at the logic of onTouchEvent

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

        // If the view is disabled, you can set it to be disabled via setEnabled()
        if ((viewFlags & ENABLED_MASK) == DISABLED) {
            if(action == MotionEvent.ACTION_UP && (mPrivateFlags & PFLAG_PRESSED) ! =0) {
                setPressed(false);
            }
            // CONTEXT_CLICKABLE (CONTEXT_CLICKABLE, LONG_CLICKABLE, CONTEXT_CLICKABLE); // CONTEXT_CLICKABLE (CONTEXT_CLICKABLE, LONG_CLICKABLE)
            // The event is still consumed, but there is no response
            return (((viewFlags & CLICKABLE) == CLICKABLE
                    || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
                    || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE);
        }

         // When the View status is ENABLED
        / / and the view to satisfy CLICKABLE LONG_CLICKABLE CONTEXT_CLICKABLE one, consumption of this event
        if (((viewFlags & CLICKABLE) == CLICKABLE ||
                (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE) ||
                (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
                    if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
                        // Get focus in touchable 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 the Tap operation, which removes the long-press callback method
                            removeLongPressCallback();

                            // If you are in the pressed state, perform the click operation
                            if(! focusTaken) {// Use Runnable and publish instead of calling performClick directly
                                // This updates the other visual state of the view before the click begins
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                / / call the View. An OnClickListener
                                if(! post(mPerformClick)) { performClick(); }}}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;

                case MotionEvent.ACTION_DOWN:
                    mHasPerformedLongPress = false;

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // Determine if you are in a scrollable view
                    boolean isInScrollingContainer = isInScrollingContainer();

                    if (isInScrollingContainer) {
                        mPrivateFlags |= PFLAG_PREPRESSED;
                        if (mPendingCheckForTap == null) {
                            mPendingCheckForTap = new CheckForTap();
                        }
                        mPendingCheckForTap.x = event.getX();
                        mPendingCheckForTap.y = event.getY();
                        // When in scrollable view, delay TAP_TIMEOUT and feedback the press state to determine whether the user wants to scroll. The default delay is 100ms
                            postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());

                    } else {
                        // When no longer scrolling inside the view, feedback the pressing status immediately
                        setPressed(true, x, y);
                        According to long / / testing whether, if long press, callback OnLongClickListener. OnLongClick
                        checkForLongClick(0, x, y);
                    }
                    break;

                case MotionEvent.ACTION_CANCEL:
                    setPressed(false);
                    removeTapCallback();
                    removeLongPressCallback();
                    mInContextButtonPress = false;
                    mHasPerformedLongPress = false;
                    mIgnoreNextUpEvent = false;
                    break;

                case MotionEvent.ACTION_MOVE:
                    drawableHotspotChanged(x, y);

                    // Be lenient about moving outside of buttons
                    if(! pointInView(x, y, mTouchSlop)) {// Outside button
                        removeTapCallback();
                        if((mPrivateFlags & PFLAG_PRESSED) ! =0) {
                            // Remove any future long press/tap checks
                            removeLongPressCallback();

                            setPressed(false); }}break;
            }

            return true;
        }

        return false;
    }
Copy the code

There are a few things to note here

  1. As LONG as this view satisfies CLICKABLE, LONGCLICKABLE ,CONTEXTOne type of CLICKABLE, whether disabled or enabled by the setEnabled() setting, returns true as a consumption event
  2. A View’s longClickable defaults to false. For example, a Button’s clickable defaults to true and a TextView’s clickable defaults to false. But the View’s setOnClickListener will set the View’s clickable to true by default, The View’s setOnLongClickListener also sets the View’s longClickable to true
  3. In ACTION_DOWN operations, if it is a long press, callback OnLongClickListener. OnLongClick
  4. In the ACTION_UP action, call onClickListener.onclick
Activity.OnTouchEvent

If there is no View consumption event, it will eventually return to activity.onTouchEvent

public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
            onUserInteraction();
        }
        // Loop to determine if there is a ViewGroup or View consumption event, if not, the event returns to the activity
        if (getWindow().superDispatchTouchEvent(ev)) {
            return true;
        }
        return onTouchEvent(ev);
    }

public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }
Copy the code

Event Distribution Flow Chart

Pay attention to the point

  1. Touch events by Activity. DispatchTouchEvent processing first; When none of the viewGroups in the middle consumes or intercepts, the View at the bottom level is processed by the OnTouchEvent at the bottom level. If it does not consume, it returns to activity.onTouchEvent
  2. Only ViewGroup has onInterceptTouchEvent; In the distribution process, any intermediate ViewGroup can intercept directly, so the distribution is not sent down, but is handled by the OnTouchEvent of the ViewGroup that intercepts
  3. Child View can invoke the parent ViewGroup requestDisallowInterceptTouchEvent method, to set up the disallowIntercept = true, This prevents the parent ViewGroup’s onInterceptTouchEvent from intercepting operations
  4. When OnTouchEvent bubbles from bottom to top, if OnTouchEvent of any middle layer consumes the event, it is no longer passed up, indicating that the event has been consumed
  5. If dispatchTouchEvent does not consume an ACTION when the event is dispatchedThe DOWN event, which returns true, is the subsequent ACTIONEvents such as MOVE cannot be received
  6. CLICKABLE, LONG_CLICKABLE consume events regardless of whether the View is DISABLED or ENABLED
  7. The View’s setOnClickListener sets the View’s clickable to true by default, The View’s setOnLongClickListener will also set the View’s longClickable to true; SetClickable and setLongClickable for all views are best called after two listener methods
  8. OnTouch takes precedence over onTouchEvent, onClick and onLongClick are called in onTouchEvent, and onLongClick takes precedence over onClick; If onTouch returns true, onTouchEvent is not executed; OnTouch This method is executed only if the View has OnTouchListener set to enable

This completes the event distribution mechanism and source code analysis.

Three things to watch ❤️

If you find this article helpful, I’d like to invite you to do three small favors for me:

  1. Like, forward, have your “like and comment”, is the motivation of my creation.
  2. Follow the public account “Xiaoxinchat Android” and share original knowledge from time to time
  3. Also look forward to the follow-up article ing🚀