A brief analysis of Android View touch feedback principle

Rewrite OnTouchEvent() and write the touch algorithm inside the method

Return true, indicating that the event is consumed, all touch feedback is no longer effective, and event ownership is returned

if (event.actionMasked == MotionEvent.ACTION_UP){
    performClick()//Lift the event to execute performClick to trigger the click
}
override fun onTouchEvent(event: MotionEvent): Boolean {
  //event touch event
}

//All touch events are a sequence, such as Down-Up, Down-Up-Cancel, Down-Move

//Example 1

View TouchEvnet ->ViewGroup TouchEvent

//Example 2

View1 TouchEvnet ->View2 TouchEvnet ->ViewGroup TouchEvent

The difference between event.action and ActionMarked

ActionMarked properties:

ACTION_MASK
ACTION_DOWN
ACTION_UP
ACTION_MOVE
ACTION_CANCEL
ACTION_OUTSIDE
ACTION_POINTER_DOWN
ACTION_POINTER_UP
ACTION_HOVER_MOVE
ACTION_SCROLL
ACTION_HOVER_ENTER
ACTION_HOVER_EXIT
ACTION_BUTTON_PRESS
ACTION_BUTTON_RELEASE
ACTION_POINTER_INDEX_MASK
ACTION_POINTER_INDEX_SHIFT

….

Compared with Action, ActionMarked is suitable for multi-touch //ACTION_POINTER_UP when the first finger is not lifted

The new version will be based on events and Pointers

event.Action will get multiple events. For example, down may be action-down – action-pointer-down, which will merge multiple events.

onTouchEvent

public boolean onTouchEvent(MotionEvent event) {
    final float x = event.getX(); //x coordinate
    final float y = event.getY(); //y coordinate
    final int viewFlags = mViewFlags;
    final int action = event.getAction(); //Action
    final boolean clickable = ((viewFlags & amp; CLICKABLE) == CLICKABLE
        || (viewFlags & amp; LONG_CLICKABLE) == LONG_CLICKABLE)
        || (viewFlags & amp; CONTEXT_CLICKABLE) == CONTEXT_CLICKABLE;
       //Conform to click, long press context, all belong to click events

if ((viewFlags & amp; ENABLED_MASK) == DISABLED) {
    if (action == MotionEvent.ACTION_UP & amp; & amp; (mPrivateFlags & amp; PFLAG_PRESSED) != 0) {
        setPressed(false); //Lift the mark to mark unclicked
    }
    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 clickable but disabled, return clickable to mark consumption
}
if (mTouchDelegate != null) {
    if (mTouchDelegate.onTouchEvent(event)) {
        return true;
        //mTouchDelegate adds click area
    }
}
if (clickable || (viewFlags & amp; TOOLTIP) == TOOLTIP) {
//TOOLTIP API26 and above new features, explanation tools, auxiliary tools
//Clickable or with auxiliary tools to explain the description
    switch (action) {
        case MotionEvent.ACTION_UP:
            mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
            if ((viewFlags & amp; TOOLTIP) == TOOLTIP) {
                //Auxiliary prompt, release your finger and disappear for 1500ms
                handleTooltipUp();
            }
            if (!clickable) {
                //Not clickable, cancel all events
                removeTapCallback();
                removeLongPressCallback();
                mInContextButtonPress = false;
                mHasPerformedLongPress = false;
                mIgnoreNextUpEvent = false;
                break;
            }
        
        boolean prepressed = (mPrivateFlags & amp; PFLAG_PREPRESSED) != 0;
        //Press or prepare to press
        if ((mPrivateFlags & amp; PFLAG_PRESSED) != 0 || prepressed) {
        boolean focusTaken = false;
        //If focus can be obtained and touch mode can obtain focus
        if (isFocusable() & amp; & amp; isFocusableInTouchMode() & amp; & amp; !isFocused()) {
            focusTaken = requestFocus();
        }

        //Prepare to press
        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); //The pressed state is true
            }

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

        // Only perform take click actions if we were in the pressed state
        if (!focusTaken) {
            //Not ready to press
            // 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)) {
                //Lift up to trigger click
                performClickInternal();
            }
        }
    }

    if (mUnsetPressedState == null) {
        mUnsetPressedState = new UnsetPressedState();
    }
    //Delay operation is left blank for 64ms and automatically raised
    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:
    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
         //Whether the View is touched
    }
    mHasPerformedLongPress = false;

    if (!clickable) {
          //If it is not clickable, check the long press and there will be a delay in execution.
        checkForLongClick(
                ViewConfiguration.getLongPressTimeout(), x, y,
                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        break;
    }

    if (performButtonActionOnTouchDown(event)) {
           //right mouse click to display context menu
        break;
    }

 
/*
If the sliding control keeps recursing and returns the status
public boolean isInScrollingContainer() {
    ViewParent p = getParent();
    while (p != null & amp; & amp; p instanceof ViewGroup) {
        if (((ViewGroup) p).shouldDelayChildPressedState()) {
//shouldDelayChildPressedState Whether to delay the child View delay state
            return true;
        }
        p = p.getParent();
    }
    return false;
}
*/
    boolean isInScrollingContainer = isInScrollingContainer();
    //Pre-press whether to slide or press for delay judgment
    if (isInScrollingContainer) {
        mPrivateFlags |= PFLAG_PREPRESSED;
        if (mPendingCheckForTap == null) {
            mPendingCheckForTap = new CheckForTap();


/*

private final class CheckForTap implements Runnable {
    public float x;
    public float y;

    @Override
    public void run() {
        mPrivateFlags & amp;= ~PFLAG_PREPRESSED; //Blank pre-click
        setPressed(true, x, y);
        final long delay = //Check long press
                ViewConfiguration.getLongPressTimeout() - ViewConfiguration.getTapTimeout();
        checkForLongClick(delay, x, y, TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
    }
}
*/
        }
        mPendingCheckForTap.x = event.getX();
        mPendingCheckForTap.y = event.getY();
        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout() ); //100ms

    } else {
        // Not inside a scrolling container, so show the feedback right away

        setPressed(true, x, y); //Set the sliding control to the pressed state
        checkForLongClick( //Check whether to long press and set the waiter
                ViewConfiguration.getLongPressTimeout(), //500ms
                x,
                y,
                TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
                //Pre-press needs to wait getLongPressTimeout 500 - getTapTimeout 100ms
    }
    break;




case MotionEvent.ACTION_CANCEL:
    //Remove all events
    if (clickable) {
        setPressed(false);
    }
    removeTapCallback();
    removeLongPressCallback();
    mInContextButtonPress = false;
    mHasPerformedLongPress = false;
    mIgnoreNextUpEvent = false;
    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    break;

case MotionEvent.ACTION_MOVE:
    if (clickable) {
        //Clickable ripple effect after 5.0
        drawableHotspotChanged(x, y);
    }

    final int motionClassification = event.getClassification();
    final boolean ambiguousGesture =
        //Unspecified gesture, add long press event
            motionClassification == MotionEvent.CLASSIFICATION_AMBIGUOUS_GESTURE;
    int touchSlop = mTouchSlop;
    if (ambiguousGesture & amp; & amp; hasPendingLongPressCallback()) {
        final float ambiguousMultiplier =
                ViewConfiguration.getAmbiguousGestureMultiplier();
        if (!pointInView(x, y, touchSlop)) {
        //If the finger points to the boundary, touchSlop touches the boundary and increases the extended long press

            // The default action here is to cancel long press. But instead, we
            // just extend the timeout here, in case the classification
            // stays ambiguous.
            removeLongPressCallback();
            // Double the ms of the long press event
            long delay = (long) (ViewConfiguration.getLongPressTimeout()
                    * ambiguousMultiplier);
            // Subtract the time already spent
              delay -= event.getEventTime() - event.getDownTime();
            //Check long press
            checkForLongClick(
                    delay,
                    x,
                    y,
                    TOUCH_GESTURE_CLASSIFIED__CLASSIFICATION__LONG_PRESS);
        }
        touchSlop *= ambiguousMultiplier;
    }

    // Be lenient about moving outside of buttons
    if (!pointInView(x, y, touchSlop)) {
       //cancel event
        removeTapCallback();
        removeLongPressCallback();
        if ((mPrivateFlags & amp; PFLAG_PRESSED) != 0) {
            setPressed(false);
        }
        mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
    }

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

    break;


        }
}

If it is not a sliding Layout, you can override holdDelayChildPressedState false to turn off delayed click events.

ViewGroup:

onIntercerceptTouchEvent

onTouchEvent

onIntercerceptTouchEvent determines whether to intercept

1.

ViewGroup onIntercerceptTouchEvent false ->View -> onTouchEvent false ->… ->ViewGroup ->onTouchEvent

2.

ViewGroup onIntercerceptTouchEvent false -> ViewGroup onIntercerceptTouchEvent false -> View -> onTouchEvent false ->… ->ViewGroup ->onTouchEvent ->ViewGroup ->onTouchEvent

3.

ViewGroup onIntercerceptTouchEven true -> ViewGroup -> onTouchEvent true

ViewGroup onInterceptTouchEvent It is recommended to return false first to release, and then record the event/data to prepare

disparchTouchEvent manages ViewGroup’s onIntercerceptTouchEvent / onTouchEvent

disparchTouchEvent manages onTouchEvent with View

The child View dispatchTouchEvent returns true and the consumption event is no longer delivered.

ViewGroup dispatchTouchEvent calls super.dispatchTouchEvent

View dispatchTouchEvnet:

 if (onFilterTouchEventForSecurity(event)) {
            if ((mViewFlags & amp; ENABLED_MASK) == ENABLED & amp; & amp; handleScrollBarDragging(event)) {
                result = true;
            }
            //noinspection SimplifiableIfStatement
            ListenerInfo li = mListenerInfo;
            //mOnTouchListener touch event monitoring does not call onTouchEvnet
            if (li != null & amp; & amp; li.mOnTouchListener != null
                     & amp; & amp; (mViewFlags & amp; ENABLED_MASK) == ENABLED
                     & amp; & amp; li.mOnTouchListener.onTouch(this, event)) {
                result = true;
            }

            if (!result & amp; & amp; onTouchEvent(event)) {
                result = true;
            }
return TouchEvent
        }

ViewGroup.dispatchTouchEvent -> Core

if(intercptTouchEvent)

{onTouchEvent}

else{child View.dispatchTouchEvent}

 @Override
    public boolean dispatchTouchEvent(MotionEvent ev){
        if (mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onTouchEvent(ev, 1);
        }

        //obstacle related
        // 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() & amp; & amp; isAccessibilityFocusedViewOrHost()) {
            ev.setTargetAccessibilityFocus(false);
        }

        boolean handled = false;
        if (onFilterTouchEventForSecurity(ev)) {
            final int action = ev.getAction();
            final int actionMasked = action & amp; 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); //Clear the state, make sure the view is not pressed, it is a brand new sequence down
                resetTouchState();//Intercept child View
            }

            // Check for interception.
            final boolean intercepted;
            if (actionMasked == MotionEvent.ACTION_DOWN // Press the event to determine whether the sub-View is intercepted
                    || mFirstTouchTarget != null) {
                //mFirstTouchTarget has a sub-View pressed
                final boolean disallowIntercept = (mGroupFlags & amp; FLAG_DISALLOW_INTERCEPT) != 0;
                if (!disallowIntercept) {
                    intercepted = onInterceptTouchEvent(ev);
                    ev.setAction(action); // restore action in case it was changed
                } else {
                    //Do not intercept
                    intercepted = false;
                }
            } else {
                // There are no touch targets and this action is not an initial down
                // so this view group continues to intercept touches.
                //interception
                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);
            }

            // Check for cancellation.
            final boolean canceled = resetCancelNextUpFlag(this)
                    || actionMasked == MotionEvent.ACTION_CANCEL;

            // Update list of touch targets for pointer down, if needed.
            final boolean split = (mGroupFlags & amp; FLAG_SPLIT_MOTION_EVENTS) != 0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            //Non-cancellation and interception status
            if (!canceled & amp; & amp; !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;

                //split multi-touch,
                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split & amp; & amp; 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 & amp; & amp; 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
                                 & amp; & amp; 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 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 (!child.canReceivePointerEvents()
                                    || !isTransformedTouchPointInView(x, y, child, null)) {
                                ev.setTargetAccessibilityFocus(false);
                                continue;
                            }

                            //Get whether there is a touching sub-View that can receive it
                            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.
                                //pointerIdBits added
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);

                               //No sub-View that is being touched can receive the event. Try to find a new sub-View to receive the event.
                            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();
                                //Add to
                                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 & amp; & amp; 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;
                    }
                }
            }

            // Dispatch to touch targets.
            //Sub View does not receive events
            if (mFirstTouchTarget == null) {
                // No touch targets so treat this as an ordinary view.
                //Call your own TouchEvent
                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; //The inner sub-View that was touched
                while (target != null) {
                    final TouchTarget next = target.next;
                    if (alreadyDispatchedToNewTouchTarget & amp; & amp; target == newTouchTarget) {
                        handled = true;
                    } else {
                        final boolean cancelChild = resetCancelNextUpFlag(target.child)
                                || intercepted;
                        //Handle subView offset
                        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 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 & amp; & amp; actionMasked == MotionEvent.ACTION_POINTER_UP) {
                final int actionIndex = ev.getActionIndex();
                final int idBitsToRemove = 1 << ev.getPointerId(actionIndex);
                removePointersFromTouchTargets(idBitsToRemove);
            }
        }

        if (!handled & amp; & amp; mInputEventConsistencyVerifier != null) {
            mInputEventConsistencyVerifier.onUnhandledEvent(ev, 1);
        }
        return handled;
    }







 @Override
    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {

//After down and before up, called in the parent View ViewGroup, the same event is not intercepted
        if (disallowIntercept == ((mGroupFlags & amp; FLAG_DISALLOW_INTERCEPT) != 0)) {
            // We're already in this state, assume our ancestors are too
            return;
        }

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

        // Pass it up to our parent
        if (mParent != null) {
            mParent.requestDisallowInterceptTouchEvent(disallowIntercept);
        }
    }


dispatchTransformedTouchEvent () ViewGroup offset calculation, processing sub-View for conversion