preface

This series is divided into three parts:

  1. Step by step explore learning Android Touch event distribution mechanism (a) by writing demo Log, ACTION_DOWN event as an example, a complete understanding of the entire Android Touch event distribution mechanism.
  1. (2) Explore the distribution and transfer rules of ACTION_MOVE and ACTION_UP events.
  1. Step by step explore learning Android Touch event distribution transfer mechanism (three) that is, through Android source code analysis, from the essence of understanding Android Touch event distribution transfer mechanism.

1. DispatchTouchEvent () method source analysis

1.1 Activity’s dispatchTouchEvent() method
  • Source:
   /**
     * Called to process touch screen events.  You can override this to
     * intercept all touch screen events before they are dispatched to the
     * window.  Be sure to call this implementation for touch screen events
     * that should be handled normally.
     *
     * @param ev The touch screen event.
     *
     * @return boolean Return true if this event was consumed.
     */
    public boolean dispatchTouchEvent(MotionEvent ev) {
        if (ev.getAction() == MotionEvent.ACTION_DOWN) {
	        // Call onUserInteraction() when judging an ACTION_DOWN event.
            onUserInteraction();
        }
        if (getWindow().superDispatchTouchEvent(ev)) {
	        // the getWindow () method gets an instance of PhoneWindow
            return true;
        }
        // If no child View is found to consume the event, it is eventually passed to the Activity's onTouchEvent() handler
        return onTouchEvent(ev);
    }
Copy the code
  • Analysis:
    • The method first determines whether an ACTION_DOWN event is passed in, and if so, triggers a callback method called onUserInteraction().

    • The onUserInteraction() method is an empty implementation in activity.java. Developers can override it as needed, for example to determine if the user is doing something to interact with the screen.

    • Then determine that getWindow().superDispatchTouchEvent(ev) was called. GetWindow () gets an instance of PhoneWindow.

    • Here is a brief description of the Android window structure:

    • Moving on to the source code, look at the superDispatchTouchEvent(EV) method in PhoneWindow.java.

       @Override
          public boolean superDispatchTouchEvent(MotionEvent event) {
              return mDecor.superDispatchTouchEvent(event);
          }
      Copy the code
    • As you can see, the superDispatchTouchEvent(EV) method of the DecorView is called internally.

    • This way, the DecorView is actually a subclass of FrameLayout. It doesn’t override this method, so it ends up calling the dispatchTouchEvent() method of the ViewGroup.

1.2 ViewGroup dispatchTouchEvent() method
  • Source:
    @Override
    public boolean dispatchTouchEvent(MotionEvent ev) {
        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);
        }

        boolean handled = false;
        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();
            }

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

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

            // Update list of touch targets for pointer down, if needed.
            final booleansplit = (mGroupFlags & FLAG_SPLIT_MOTION_EVENTS) ! =0;
            TouchTarget newTouchTarget = null;
            boolean alreadyDispatchedToNewTouchTarget = false;
            if(! canceled && ! intercepted) {// If the event is targeting accessiiblity focus we give it to the
                // view that has accessibility focus and if it does not handle it
                // we clear the flag and dispatch the event to all children as usual.
                // We are looking up the accessibility focused host to avoid keeping
                // state since these events are very rare.
                View childWithAccessibilityFocus = ev.isTargetAccessibilityFocus()
                        ? findChildWithAccessibilityFocus() : null;

                if (actionMasked == MotionEvent.ACTION_DOWN
                        || (split && actionMasked == MotionEvent.ACTION_POINTER_DOWN)
                        || actionMasked == MotionEvent.ACTION_HOVER_MOVE) {
                    final int actionIndex = ev.getActionIndex(); // always 0 for down
                    final int idBitsToAssign = split ? 1 << ev.getPointerId(actionIndex)
                            : TouchTarget.ALL_POINTER_IDS;

                    // Clean up earlier touch targets for this pointer id in case they
                    // have become out of sync.
                    removePointersFromTouchTargets(idBitsToAssign);

                    final int childrenCount = mChildrenCount;
                    if (newTouchTarget == null&& childrenCount ! =0) {
                        final float x = ev.getX(actionIndex);
                        final float y = ev.getY(actionIndex);
                        // Find a child that can receive the event.
                        // Scan children from front to back.
                        final ArrayList<View> preorderedList = buildTouchDispatchChildList();
                        final boolean customOrder = preorderedList == null
                                && isChildrenDrawingOrderEnabled();
                        final View[] children = mChildren;
                        for (int i = childrenCount - 1; i >= 0; i--) {
                            final int childIndex = getAndVerifyPreorderedIndex(
                                    childrenCount, i, customOrder);
                            final View child = getAndVerifyPreorderedView(
                                    preorderedList, children, childIndex);

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

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

                            newTouchTarget = getTouchTarget(child);
                            if(newTouchTarget ! =null) {
                                // Child is already receiving touch within its bounds.
                                // Give it the new pointer in addition to the ones it is handling.
                                newTouchTarget.pointerIdBits |= idBitsToAssign;
                                break;
                            }

                            resetCancelNextUpFlag(child);
                            if (dispatchTransformedTouchEvent(ev, false, child, idBitsToAssign)) {
                                // 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; }}}// 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; }}// 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
  • Analysis:

    • This method has a lot of code logic, more than 200 lines. After all, we only care about the flow of Touch events and processing logic.

    • First of all, the ViewGroup dispatchTouchEvent() method is trying to determine whether the event should be intercepted or handled by itself.

           // 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;
                  }
      
      Copy the code
    • A key Boolean variable is ***disallowIntercept***. This variable controls whether the parent control is not allowed to intercept the event.

    It involves a method: * * * requestDisallowInterceptTouchEvent * * * () method. This method determines the value of mGroupFlags and controls the request parent layout not to intercept the event but to handle it itself. This method is often used when dealing with scenarios such as sliding collisions. But here for the sake of the whole source code analysis logic concise and clear, no longer specific analysis of the method of code.

    • And you can notice a judgment conditions: * * * the if (actionMasked = = MotionEvent. ACTION_DOWN | | mFirstTouchTarget! = null)***, that is, the ViewGroup determines whether the event should be intercepted or not. First, the event must be an ACTION_DOWN event or the event’s mFirstTouchTarget is not empty.

    • If mFirstTouchTarget is null, ACTION_MOVE and ACTION_UP events are not judged by interception. Instead, intercepted = true means that the event is directly blocked. Which is exactly what I was doingAndroid Touch Event Delivery mechanismThe distribution patterns of ACTION_MOVE and ACTION_UP events mentioned in.

    • So what is the mFirstTouchTarget variable? When will it be empty?

    • You can see the subsequent code to the ViewGroup dispatchTouchEvent() method, which is a for loop:

      .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.}...Copy the code
      • The for loop a traverse a child View, looking for watching events coordinates in which the range of View, if found, is set mFirstTouchTarget as a child, and put alreadyDispatchedToNewTouchTarget set to true.

      • So does the event end up being handled by you or the target subview (mFirstTouchTarget)?

      • If mFirstTouchTarget is not null, pass it to mFirstTouchTarget (target subView). If null, it consumes it itself.

      • So whether you’re doing it to the target child View or you’re doing it yourself, you’re going to go to the View’s dispatchTouchEvent() method. When mFirstTouchTarget is null, ViewGoup calls super.dispatchTouchEvent(Event). After all, ViewGroup is essentially a subclass of View. So the ViewGroup is still calling the View’s dispatchTouchEvent() method. So let’s look at the View’s dispatchTouchEvent() method.

    1.3 View’s dispatchTouchEvent() method
    • Source:
        /**
         * Pass the touch screen motion event down to the target view, or this
         * view if it is the target.
         *
         * @param event The motion event to be dispatched.
         * @return True if the event was handled by the view, false otherwise.
         */
        public boolean dispatchTouchEvent(MotionEvent event) {
            // If the event should be handled by accessibility focus first.
            if (event.isTargetAccessibilityFocus()) {
                // We don't have focus or no virtual descendant has it, do not handle the event.
                if(! isAccessibilityFocusedViewOrHost()) {return false;
                }
                // We have focus and got the event, then use normal event dispatch.
                event.setTargetAccessibilityFocus(false);
            }
    
            boolean result = false;
    
            if(mInputEventConsistencyVerifier ! =null) {
                mInputEventConsistencyVerifier.onTouchEvent(event, 0);
            }
    
            final int actionMasked = event.getActionMasked();
            if (actionMasked == MotionEvent.ACTION_DOWN) {
                // Defensive cleanup for new gesture
                stopNestedScroll();
            }
    
            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; }}if(! result && mInputEventConsistencyVerifier ! =null) {
                mInputEventConsistencyVerifier.onUnhandledEvent(event, 0);
            }
    
            // Clean up after nested scrolls if this is the end of a gesture;
            // also cancel it if we tried an ACTION_DOWN but we didn't want the rest
            // of the gesture.
            if(actionMasked == MotionEvent.ACTION_UP || actionMasked == MotionEvent.ACTION_CANCEL || (actionMasked == MotionEvent.ACTION_DOWN && ! result)) { stopNestedScroll(); }return result;
        }
    Copy the code
  • Analysis:

    • The amount of code is much smaller than ViewGroup’s dispatchTouchEvnet() method.

    • Check whether the View itself is set to OnTouchListener. If it is set to onTouch(), call the onTouch() method. If this method returns true, the event is consumed. Return false, the event is still passed back to the onTouchEvent() method.

    Note: It is worth noting that the ViewGroup itself does not override the View’s onTouchEvnet() method, so if passed back, it is also the onTouchEvent() method of the call’s parent class, View.java.

2. OnTouchEvent () method source code analysis

2.1 Activity’s onTouchEvent() method
  • Source:
 /**
     * Called when a touch screen event was not handled by any of the views
     * under it.  This is most useful to process touch events that happen
     * outside of your window bounds, where there is no view to receive it.
     *
     * @param event The touch screen event being processed.
     *
     * @return Return true if you have consumed the event, false if you haven't.
     * The default implementation always returns false.
     */
    public boolean onTouchEvent(MotionEvent event) {
        if (mWindow.shouldCloseOnTouch(this, event)) {
            finish();
            return true;
        }

        return false;
    }

Copy the code
  • Analysis:

    • This method has very little code, and the default Activity’s onTouchEvent() method returns false, which means that touch events are not handled by default.

    • Only if the PhoneWindow shouldCloseOnTouch () method returns true will the touch event be handled, finishing the Activity directly.

2.2 View onTouchEvent() method

As mentioned earlier, the ViewGroup does not override the View’s onTouchEvent() method, so when inheriting from a ViewGroup, the View’s onTouchEvent() method is called.

  • Source:
/**
     * Implement this method to handle touch screen motion events.
     * <p>
     * If this method is used to detect click actions, it is recommended that
     * the actions be performed by implementing and calling
     * {@link #performClick()}. This will ensure consistent system behavior,
     * including:
     * <ul>
     * <li>obeying click sound preferences
     * <li>dispatching OnClickListener calls
     * <li>handling {@link AccessibilityNodeInfo#ACTION_CLICK ACTION_CLICK} when
     * accessibility features are enabled
     * </ul>
     *
     * @param event The motion event.
     * @return True if the event was handled, false otherwise.
     */
    public boolean onTouchEvent(MotionEvent event) {
        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; }}if (clickable || (viewFlags & TOOLTIP) == TOOLTIP) {
            switch (action) {
                case MotionEvent.ACTION_UP:
                    mPrivateFlags3 &= ~PFLAG3_FINGER_DOWN;
                    if ((viewFlags & TOOLTIP) == TOOLTIP) {
                        handleTooltipUp();
                    }
                    if(! clickable) { removeTapCallback(); removeLongPressCallback(); mInContextButtonPress =false;
                        mHasPerformedLongPress = false;
                        mIgnoreNextUpEvent = false;
                        break;
                    }
                    booleanprepressed = (mPrivateFlags & PFLAG_PREPRESSED) ! =0;
                    if((mPrivateFlags & PFLAG_PRESSED) ! =0 || prepressed) {
                        // take focus if we don't have it already and we should in
                        // touch mode.
                        boolean focusTaken = false;
                        if(isFocusable() && isFocusableInTouchMode() && ! isFocused()) { focusTaken = requestFocus(); }if (prepressed) {
                            // The button is being released before we actually
                            // showed it as pressed. Make it show the pressed
                            // state now (before scheduling the click) to ensure
                            // the user sees it.
                            setPressed(true, x, y);
                        }

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

                            // Only perform take click actions if we were in the pressed state
                            if(! focusTaken) {// Use a Runnable and post this rather than calling
                                // performClick directly. This lets other visual state
                                // of the view update before click actions start.
                                if (mPerformClick == null) {
                                    mPerformClick = new PerformClick();
                                }
                                if(! post(mPerformClick)) { 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:
                    if (event.getSource() == InputDevice.SOURCE_TOUCHSCREEN) {
                        mPrivateFlags3 |= PFLAG3_FINGER_DOWN;
                    }
                    mHasPerformedLongPress = false;

                    if(! clickable) { checkForLongClick(0, x, y);
                        break;
                    }

                    if (performButtonActionOnTouchDown(event)) {
                        break;
                    }

                    // 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(0, x, y);
                    }
                    break;

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

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

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

            return true;
        }

        return false;
    }
Copy the code
  • Analysis:

    • The code is also long, so let’s just sort out the main logic.

    • When the event is passed to the method, it first checks whether the View is enabled or clickable.

    • It then responds differently depending on the type of Touch event. For example, the View receives Down events and Up events.

    Note: Notice an important piece of code if (! post(mPerformClick)) { performClick(); } This code calls the performClick() method after detecting an up event, which in turn calls back the onClick() method in the onClickListener interface. This is combined with the ACTION_DOWN event of the dispatchTouchEvent() method that calls onTouch, which takes precedence over onClick.

3. OnInterceptTouchEvent () method source analysis

Only ViewGroup has the onInterceptTouchEvent() method
  • Source:
  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
  • Analysis:

    • This method returns false by default, indicating that touch events are not intercepted

    • Only in ev.isFromSource(inputDevice.source_mouse) && ev.getAction() == motionEvent.action_down && Ev.isbuttonpressed (MotionEvent.button_primary) &&isonScrollBarThumb (ev.getx (), ev.gety ())).

  • You can override this method, retrun True, to intercept touch events if you need to, for example, handle sliding collisions.

Note: [reproduced please specify, questions can be asked, like can collect to share, the blog continues to update, welcome to pay attention to]