Android Touch event distribution mechanism

Event Distribution Sequence

When the Android device is touched, the touch screen event response is sent down, but the processing is reversed. Activty will eventually send events to a ViewGroup or View and then pass them on without interception. Understand this mechanism by combing through the source code flow.

For those of you who are not familiar with the mechanism, you can look at the dispatchTouchEvent section of the View for a better understanding.

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

Just a quick overview of the dispatchTouchEvent method.

  • Function: Passes touch screen motion events down to the target view or the target itself.

  • Parameters: Movement events that need to be distributed

  • Return value: True if the event is consumed, False otherwise (not consumed does not mean passed)

The touch event is responded to by the current Activity, execute the dispatchTouchEvent method, determine whether the event is ACTION_DOWN, and execute the onUserInteraction (null) method, which is used for custom implementations and is usually used to debug device interactions. This is also where touch events are first received during application development.

“In reality, the onUserInteraction method is primarily used to manage status bar notifications and to cancel notifications when appropriate. Related to this method is another method, onUserLeaveHint. This method is called as part of the Activity lifecycle callback when the user puts the Activity in the background (for example, when the user clicks the Home button) and before the onPause method.”

The superDispatchTouchEvent method is a window method, window provides an empty method, implemented by a unique subclass PhoneWindow, PhoneWindow calls the DecorView’s superDispatchTouchEvent, where the real implementation is the DecorView’s dispatchTouchEvent. And the ViewGroup can distribute events to the View. Finally, we return to the Activity’s onTouchEvent, which is the Activity’s lowest consumption of events.

Finally, execute onTouchEvent

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

    return false;
}
Copy the code

Determine whether the Window should be closed after touch, and if so, end the Activity and consume the event. Otherwise, no consumption.

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023)
public boolean shouldCloseOnTouch(Context context, MotionEvent event) {
    final boolean isOutside =
            event.getAction() == MotionEvent.ACTION_UP && isOutOfBounds(context, event)
            || event.getAction() == MotionEvent.ACTION_OUTSIDE;
    if(mCloseOnTouchOutside && peekDecorView() ! =null && isOutside) {
        return true;
    }
    return false;
}
Copy the code

Window is used to check whether it is outside the window, and it is turned off.

ViewGroup::dispatchTouchEvent

You know from the Activity’s Dispatch method that the DecorView dispatches events to the ViewGroup that perform the dispatchTouchEvent. So step through the dispatchTouchEvent method of ViewGroup.

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);
}
Copy the code

The method starts with a self-consistent check, where there is a focus judgment. If the target of the event is an accessible focus, views with an accessible focus are looked for, and if the child views found do not process the event, they are sent to all child views as normal. ViewGroup is a collection of subviews, and you need to determine the priority of processing, so the focus usually has the highest priority, and you need to determine whether you need to prioritize processing. If this parameter is set to false, events are normally sent without special processing.

 if(childWithAccessibilityFocus ! =null) {
     if(childWithAccessibilityFocus ! = child) {continue;
     }
     childWithAccessibilityFocus = null;
     i = childrenCount - 1;
}
Copy the code

There is a focus priority judgment in Android Q, but it has been deleted in Android R. It may have been deleted by Google or moved to another place for processing. It is not clear here.

if (onFilterTouchEventForSecurity(ev)) {
    final int action = ev.getAction();
    final int actionMasked = action & MotionEvent.ACTION_MASK;

    // 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();
    }
Copy the code

Judge every time after the judge Touch events safety ACTION_DOWN, cancelAndClearTouchTargets initialization TouchTarget list, ensure that no children view is being pressed.

TouchTarget is a linked list of touch feedback sequences for sub-views, which can be complicated in multi-touch mode.

ResetTouchState is just like the method name. The main thing is to initialize a cycle of Touch events and remove the flags. These two methods reset the touch feedback.

// Check for interception.
final boolean intercepted;
if(actionMasked == MotionEvent.ACTION_DOWN || mFirstTouchTarget ! =null) {
    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;
}

// If intercepted, start normal event dispatch. Also if there is already
// a view that is handling the gesture, do normal event dispatch.
if(intercepted || mFirstTouchTarget ! =null) {
    ev.setTargetAccessibilityFocus(false);
}
Copy the code

MFirstTouchTarget is the head node of the list. If it is not null, it means that a child View can consume the event. FLAG_DISALLOW_INTERCEPT this flag represents the view does not want to be intercept events, child view through the method to realize requestDisallowInterceptTouchEvent said don’t want to be intercepted. If the flag is reset in the resetTouchState in ACTION_DOWN, so it means it’s not ACTION_DOWN and the child view says it doesn’t want to be blocked, the child view will force it to bypass the distribution order of the ViewGroup, Consider using this method in some sliding views to optimize the sliding experience.

After that, onInterceptTouchEvent is the onintercepting method, which is the opposite of onTouchEvent response order. The parent view will handle whether to intercept the TouchEvent first, which means that the child view can not use it. For example, when sliding, the child view can respond first. But if it slides, it has to be handed over to the parent view, and then recyclerView overrides that method, which only returns true once, and then calls the onTouchEvent of the child view, If the touchEvent of a subview is false and the touchEvent of a viewGroup is false, this method can be overridden, but it is recommended to return false when not necessary.

If there is no child view that can consume the event, the default is also interception. Set focus processing to false after interception.

if(! canceled && ! intercepted) { 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 =
                    isMouseEvent ? ev.getXCursorPosition() : ev.getX(actionIndex);
            final float y =
                    isMouseEvent ? ev.getYCursorPosition() : 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--) {
                final int childIndex = getAndVerifyPreorderedIndex(
                        childrenCount, i, customOrder);
                final View child = getAndVerifyPreorderedView(
                        preorderedList, children, childIndex);
                if(! child.canReceivePointerEvents() || ! isTransformedTouchPointInView(x, y, child,null)) {
                    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;
                }
Copy the code

Determine if the event was not cancelled or intercepted by the ViewGroup.

Get an array of views that can respond and iterate according to the rules of traversal (which can be interpreted as large to small and bottom to top in XML),

protected boolean canReceivePointerEvents() { return (mViewFlags & VISIBILITY_MASK) == VISIBLE || getAnimation() ! = null; }Copy the code

The child view needs visible(the visible in XML) and is not animating and is in view scope. After the view animation is shifted, the response to the touch event remains in its original position.

Then, view processing under multi-touch is judged by touchTarget linked list. NewTouchTarget just means it’s not just a child view.

                resetCancelNextUpFlag(child);
                if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                    // Child wants to receive touch within its bounds.
                    mLastTouchDownTime = ev.getDownTime();
                    if(preorderedList ! =null) {
                        // childIndex points into presorted list, find original index
                        for (int j = 0; j < childrenCount; j++) {
                            if (children[childIndex] == mChildren[j]) {
                                mLastTouchDownIndex = j;
                                break; }}}else {
                        mLastTouchDownIndex = childIndex;
                    }
                    mLastTouchDownX = ev.getX();
                    mLastTouchDownY = ev.getY();
                    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

This code looks for the TouchTarget to distribute the consumption to

DispatchTransformedTouchEvent this method is the method of ViewGroup to distribute the View, the View to the clause is empty, no call of dispatchTouchEvent View, here is the first call to distribute the down event to View, The second call is to distribute the mechanism to itself if the TouchTarget is empty, and the third call is to distribute the child view consumption and return whether the child View consumes the event. Handle events are then consumed alreadyDispatchedToNewTouchTarget labels.

If no child view can be found to consume the event, the child view that recently consumed the event is taken to consume the event.

// 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);
} 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;
    while(target ! =null) {
        final TouchTarget next = target.next;
        if (alreadyDispatchedToNewTouchTarget && target == newTouchTarget) {
            handled = true;
        } else {
            final boolean cancelChild = resetCancelNextUpFlag(target.child)
                    || intercepted;
            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; }}Copy the code

If there are no subclasses to consume or are blocked, consume yourself.

Behind the event if the distribution, by its alreadyDispatchedToNewTouchTarget handle = true.

    // Update list of touch targets for pointer up or cancel, if needed.
    if (canceled
            || actionMasked == MotionEvent.ACTION_UP
            || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
        resetTouchState();
    } else if (split && actionMasked == MotionEvent.ACTION_POINTER_UP) {
        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

Finally, the event is canceled on updating the linked list, and the event is not consumed in case of self-consistency checking. Returns the handle.

ViewGroup itself does not override onTouchEvent, has event distribution, event interception, but does not do event handling. Some subclasses of ViewGroup override onTouchEvent, like RecyclerView.

According to the above analysis, if the ViewGroup has child views, the general process will end up with a dispatchTouchEvent for the child View.

View::dispatchTouchEvent(MotionEvent event):

// If this event is processed as the first accessible focus
if (event.isTargetAccessibilityFocus()) {
    // We have no focus, or no virtual descendant has focus, so we do not process events.
    if(! isAccessibilityFocusedViewOrHost()) {return false;
    }
    // We have focus and get the event, then use normal event scheduling.
    event.setTargetAccessibilityFocus(false);
}
Copy the code

IsTargetAccessibilityFocus first to determine whether the event t as the focal point of the first access to be processed

public  boolean isTargetAccessibilityFocus(a) {
    final int flags = getFlags();
    return(flags & FLAG_TARGET_ACCESSIBILITY_FOCUS) ! =0;
}
Copy the code

FLAG_TARGET_ACCESSIBILITY_FOCUS = FLAG_TARGET_ACCESSIBILITY_FOCUS = FLAG_TARGET_ACCESSIBILITY_FOCUS = flag; Change the flag. This is used as the priority of the subview in the ViewGroup.

That is, if the target View doesn’t get focus (we’ll call focusable = false), it can still get the touch event, but skip processing

Focus cases are mostly EditText or devices like TVS.

Set the default to return Result false

if(mInputEventConsistencyVerifier ! =null) {
    mInputEventConsistencyVerifier.onTouchEvent(event, 0);
}
Copy the code

Self-consistent check, similar to whether ActionDown and Up match one by one

final int actionMasked = event.getActionMasked();
if (actionMasked == MotionEvent.ACTION_DOWN) {
    // Defensive cleanup for new gesture
    stopNestedScroll();
}
Copy the code

An Action is 32 bits, the high part represents the index of the pointer, the low part represents the event, and the eighth bit of the event is retrieved, which can be interpreted as a mask, and the event is represented by a smaller int. This means that the action_DOWN event is received again while sliding, so it stops nested scrolling in a non-side effect way

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; }}Copy the code

OnFilterTouchEventForSecurity first determines whether the event window shade, obscured it returns false

Check to see if OnTouchListener is added, the View needs to be added to handle Touch events, and check to see if enable (default true) and return true in the onTouch method, for example

button.setOnTouchListener(new OnTouchListener() {
  @Override
  public boolean onTouch(View v, MotionEvent event) {
    Log.i(TAG, "onTouch");
    return true; }});Copy the code

That’s why returning true here intercepts event distribution, because it sets the result of dispatchTouchEvent to true, okay

Of course, it’s not clear why True intercepts here.

The view’s onTouchEvent is then executed further, where the logic and operation are short-circuited to avoid unnecessary operations in the case of true above

if(actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); }Copy the code

Stop nested scrolling at the end of the event or when you do not want to continue the event.

The order of the event distribution mechanism is dispatchTouchEvent → onTouch → onTouchEvent.

You can see that when the onTouch is consumed, the onClick is not executed, and the onClick is inside the onTouchEvent.

So if true is returned in onTouch, the event is intercepted and no onTouchEvent is executed

Let’s take a look at the onTouchEvent of the View

The onTouchEvent function of a View is to distinguish whether the user is scrolling, clicking, long pressing, or accidentally touching the screen without being consumed by the touch event (return true if the View only wants touch, not click, etc.). For application development, onTouchEvent is the response interface to a set of View touch events.

final float x = event.getX();
final float y = event.getY();
final int viewFlags = mViewFlags;
final int action = event.getAction();

final boolean clickable = ((viewFlags & CLICKABLE) == CLICKABLE
        || (viewFlags & LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;

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;
}
if(mTouchDelegate ! =null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true; }}Copy the code

Initialize coordinates, viewFlags, Actions, and clickable. The clickable judgment means that if a View is unavailable, it returns true whenever it can be clicked or long pressed. Determine whether to hand over to the event distribution agent.

Then you go to switch, which is event handling. Outside the switch is return true; This also indicates that once the switch event distribution is entered, it must be consumed here

ACTION_DOWN

I’ll start with two flags, PFLAG_PRESSED to indicate that the event was pressed, and PREPRESSED to indicate that the click event (long press, touch, etc.) cannot be determined for a short time after ACTION_DOWN (getTapTimeout event). Both flag is binary int, PREPRESSED is 0000 0010 0000 0000 0000 0000 0000 0000, which is by the seventh with a or | = 1 and a nand $= ~ to control the seventh to determine the current state of the flag.

case MotionEvent.ACTION_DOWN:
    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
    }
    mHasPerformedLongPress = false;

    if(! clickable) { checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);break;
    }

    if (performButtonActionOnTouchDown(event)) {
        break;
    }
Copy the code

First, the View needs to be clickable. Non-clickable views can respond to Touch, but will be blocked in ActionDown, such as ImageView

Judge performButtonActionOnTouchDown said is similar to the right mouse button events, not deep.

// Walk up the hierarchy to determine if we're inside a scrolling container.
boolean isInScrollingContainer = isInScrollingContainer();

// For views inside a scrolling container, delay the pressed feedback for
// a short period in case this is a scroll.
if (isInScrollingContainer) {
    mPrivateFlags |= PFLAG_PREPRESSED;
    if (mPendingCheckForTap == null) {
        mPendingCheckForTap = new CheckForTap();
    }
    mPendingCheckForTap.x = event.getX();
    mPendingCheckForTap.y = event.getY();
    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
} else {
    // Not inside a scrolling container, so show the feedback right away
    setPressed(true, x, y); checkForLongClick( ViewConfiguration.getLongPressTimeout(), x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);  }break;
Copy the code

IsInScrollingContainer Iterates to determine whether the current view is in a slideable container, which is used to handle sliding events

If mPendingCheckForTap is a runnable object in the scroll container, it determines whether the user’s touch coordinates change during tapTimeout. If the coordinates change, it is a slide. This is a delay message. Confirm that it is pressed, and enter to determine whether to long press. This runnable will unflag PFLAG_PREPRESSED because tap behavior is already confirmed.

If you are not rolling the container, judge the long press.

Action_down verifies whether the current event is a long press or slide.

Note that the delay message is a delay, which is set to false for non-sliding components when customizing a View.

checkForLongClick

Looking at long-click events, you’ll often see a method called checkForLongClick

private void checkForLongClick(long delay, float x, float y, int classification) {
    if ((mViewFlags & LONG_CLICKABLE) == LONG_CLICKABLE || (mViewFlags & TOOLTIP) == TOOLTIP) {
        mHasPerformedLongPress = false;

        if (mPendingCheckForLongPress == null) {
            mPendingCheckForLongPress = newCheckForLongPress(); } mPendingCheckForLongPress.setAnchor(x, y); mPendingCheckForLongPress.rememberWindowAttachCount(); mPendingCheckForLongPress.rememberPressedState(); mPendingCheckForLongPress.setClassification(classification); postDelayed(mPendingCheckForLongPress, delay); }}Copy the code

There are four parameters, delay, position information, and classification, which there are only two categories here, long press and deep Press like 3D Touch, leaving deep Press alone.

First check that ViewFlag is either long press or TOOLTIP(XML configurable long press TOOLTIP).

MHasPerformedLongPress this flag indicates whether the long press has been called. Set to false to indicate that it has not been called. If it has been called, the long press will not be recognized as tap. Then there is a Runnable called CheckForLongPress

private final class CheckForLongPress implements Runnable {
    private int mOriginalWindowAttachCount;
    private float mX;
    private float mY;
    private boolean mOriginalPressedState;
    /** * The classification of the long click being checked: one of the * FrameworkStatsLog.TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__* constants. */
    private int mClassification;

    @UnsupportedAppUsage
    private CheckForLongPress(a) {}@Override
    public void run(a) {
        if((mOriginalPressedState == isPressed()) && (mParent ! =null)
                && mOriginalWindowAttachCount == mWindowAttachCount) {
            recordGestureClassification(mClassification);
            if (performLongClick(mX, mY)) {
                mHasPerformedLongPress = true; }}}public void setAnchor(float x, float y) {
        mX = x;
        mY = y;
    }

    public void rememberWindowAttachCount(a) {
        mOriginalWindowAttachCount = mWindowAttachCount;
    }

    public void rememberPressedState(a) {
        mOriginalPressedState = isPressed();
    }

    public void setClassification(int classification) { mClassification = classification; }}Copy the code

Sending a delayed message executes the “run” method, which checks the “WIndowAttachCount” (the number of times a view has attached) to determine whether the Activity’s life cycle has changed during the long press, and the “view’s effectiveness” to determine whether the long press has failed. Then execute performLongClick and set mHasPerformedLongPress = true.

public boolean performLongClick(float x, float y) {
    mLongClickX = x;
    mLongClickY = y;
    final boolean handled = performLongClick();
    mLongClickX = Float.NaN;
    mLongClickY = Float.NaN;
    return handled;
}
Copy the code
public boolean performLongClick(a) {
    return performLongClickInternal(mLongClickX, mLongClickY);
}
Copy the code

This is where the View’s onLongClickListener is called.

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 booleanisAnchored = ! 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;
}
Copy the code

The first line is for auxiliary functions, which are used for special needs and can be ignored. This HANDLED is the return value that indicates whether the event has been consumed. This is where the onLongClick method is called

HapticFeedbackConstants will be provided if consumed.

ACTION_MOVE

case MotionEvent.ACTION_MOVE:
    if (clickable) {
        drawableHotspotChanged(x, y);
    }

    final int motionClassification = event.getClassification();
    final boolean ambiguousGesture =
            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
    int touchSlop = mTouchSlop;
    if (ambiguousGesture && hasPendingLongPressCallback()) {
        if(! pointInView(x, y, touchSlop)) {// The default action here is to cancel long press. But instead, we
            // just extend the timeout here, in case the classification
            // stays ambiguous.
            removeLongPressCallback();
            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                    * mAmbiguousGestureMultiplier);
            // Subtract the time already spent
            delay -= event.getEventTime() - event.getDownTime();
            checkForLongClick(
                    delay,
                    x,
                    y,
                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        }
        touchSlop *= mAmbiguousGestureMultiplier;
    }

    // Be lenient about moving outside of buttons
    if(! pointInView(x, y, touchSlop)) {// Outside button
        // Remove any future long press/tap checks
        removeTapCallback();
        removeLongPressCallback();
        if((mPrivateFlags & PFLAG_PRESSED) ! =0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    }
Copy the code

Here we mainly perform movement, judge gesture operation and judge movement margin.

TouchSlop is used to determine whether it is the view’s time if some fingers are partially outside the view. Enlarging this value increases the boundary, which determines whether the view is outside the range.

final boolean deepPress =
        motionClassification == MotionEvent.CLASSIFICATION_DEEP_PRESS;
if (deepPress && hasPendingLongPressCallback()) {
    // process the long click action immediately
    removeLongPressCallback();
    checkForLongClick(
            0 /* send immediately */,
            x,
            y,
            TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__DEEP_PRESS);
}

break;
Copy the code

This is used to judge pressure sensation, similar to 3DTouch.

ACTION_UP:

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);
    }
Copy the code

First get the focus, then determine if the button was released before it had time to respond, and continue to complete the click event.

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(); }}}Copy the code

RemoveLongPressCallback Removes the long-press detection timer.

To keep the view in order, use threads to publish messages so that the interface can be updated first

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

PerformClickInternal will eventually go performClick, successively two notifyAutofillManagerOnClick to ensure that the view of time sequence, and then the onClick, result = true. This is followed by the state judgment removeTapCallback

ACTION_CANCEL

Touch events are cancelled by the system, similar to movement events being blocked by the parent view.

case MotionEvent.ACTION_CANCEL:
    if (clickable) {
        setPressed(false);
    }
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    break;
Copy the code

Cancel is a reset operation that removes some of the original status records.

conclusion

Classic U-shaped diagram (from network)