From 381e5975bd2c702fa0b50877f1bf600d97d8f9fd Mon Sep 17 00:00:00 2001 From: Vincent Riemer <1398555+vincentriemer@users.noreply.github.com> Date: Mon, 21 Aug 2023 09:42:23 -0700 Subject: [PATCH] Fix proper pointercancel/leave behavior when scrolling (#39081) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/39081 Changelog: [Internal] - Fix pointer cancel/leave behavior when a child view scrolls Differential Revision: D48472486 fbshipit-source-id: 140af572ced13daf953dee78571991fba81b0083 --- .../react/uimanager/JSPointerDispatcher.java | 102 ++++++------------ 1 file changed, 34 insertions(+), 68 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java index d65ecbea8bcfbc..d37ee537452b64 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/uimanager/JSPointerDispatcher.java @@ -35,6 +35,7 @@ * and also dispatch the appropriate event to JS */ public class JSPointerDispatcher { + private static final int UNSELECTED_VIEW_TAG = -1; private static final int UNSET_POINTER_ID = -1; private static final float ONMOVE_EPSILON = 0.1f; private static final String TAG = "POINTER EVENTS"; @@ -69,48 +70,24 @@ public void onChildStartedNativeGesture( } MotionEvent motionInRoot = convertMotionToRootFrame(childView, motionEvent); + motionInRoot.setAction(MotionEvent.ACTION_CANCEL); + handleMotionEvent(motionInRoot, eventDispatcher, false); - dispatchCancelEventForTarget(childView, motionInRoot, eventDispatcher); mChildHandlingNativeGesture = childView.getId(); - - // clear "previous" state since interaction was canceled - resetPreviousStateForMotionEvent(motionEvent); } private MotionEvent convertMotionToRootFrame(View childView, MotionEvent childMotion) { MotionEvent motionInRoot = MotionEvent.obtain(childMotion); - final int cX = (int) childMotion.getX(); - final int cY = (int) childMotion.getY(); - Rect childCoords = new Rect(cX, cY, cX + 1, cY + 1); - // note: should be safe since we're only looking at descendants of the root - mRootViewGroup.offsetDescendantRectToMyCoords(childView, childCoords); - motionInRoot.setLocation(childCoords.left, childCoords.top); - - return motionInRoot; - } - private void updatePreviousStateFromEvent(MotionEvent event, PointerEventState eventState) { - // Caching the event state so we have a new "last" - // note: we need to make copies here as the eventState may be accessed later and we don't want - // mutations of these instance vars to affect it - mLastHitPathByPointerId = new HashMap<>(eventState.getHitPathByPointerId()); - mLastEventCoordinatesByPointerId = new HashMap<>(eventState.getEventCoordinatesByPointerId()); - mLastButtonState = event.getButtonState(); + int[] location = new int[2]; + mRootViewGroup.getLocationOnScreen(location); + float screenX = childMotion.getRawX(); + float screenY = childMotion.getRawY(); + float clientX = screenX - location[0]; + float clientY = screenY - location[1]; + motionInRoot.setLocation(clientX, clientY); - // Clean up any stale pointerIds - Set allPointerIds = mLastEventCoordinatesByPointerId.keySet(); - mHoveringPointerIds.retainAll(allPointerIds); - } - - private void resetPreviousStateForMotionEvent(MotionEvent event) { - int activePointerId = event.getPointerId(event.getActionIndex()); - if (mLastHitPathByPointerId != null) { - mLastHitPathByPointerId.remove(activePointerId); - } - if (mLastEventCoordinatesByPointerId != null) { - mLastEventCoordinatesByPointerId.remove(activePointerId); - } - mLastButtonState = 0; + return motionInRoot; } public void onChildEndedNativeGesture() { @@ -391,6 +368,7 @@ public void handleMotionEvent( break; case MotionEvent.ACTION_CANCEL: dispatchCancelEventForTarget(activeTargetView, eventState, motionEvent, eventDispatcher); + handleHitStateDivergence(UNSELECTED_VIEW_TAG, eventState, motionEvent, eventDispatcher); break; case MotionEvent.ACTION_HOVER_ENTER: // Ignore these events as enters will be calculated from HOVER_MOVE @@ -409,8 +387,6 @@ public void handleMotionEvent( "Motion Event was ignored. Action=" + action + " Target=" + activeTargetTag); return; } - - updatePreviousStateFromEvent(motionEvent, eventState); } private static boolean isAnyoneListeningForBubblingEvent( @@ -485,7 +461,10 @@ private void handleHitStateDivergence( MotionEvent motionEvent, EventDispatcher eventDispatcher) { int activePointerId = eventState.getActivePointerId(); - List activeHitPath = eventState.getHitPathByPointerId().get(activePointerId); + List activeHitPath = + targetTag != UNSELECTED_VIEW_TAG + ? eventState.getHitPathByPointerId().get(activePointerId) + : new ArrayList(); List lastHitPath = mLastHitPathByPointerId != null && mLastHitPathByPointerId.containsKey(activePointerId) ? mLastHitPathByPointerId.get(activePointerId) @@ -580,6 +559,24 @@ private void handleHitStateDivergence( eventDispatcher); } } + + Map> nextHitPathByPointerId = + new HashMap<>(eventState.getHitPathByPointerId()); + Map nextEventCoordinatesByPointerId = + new HashMap<>(eventState.getEventCoordinatesByPointerId()); + + if (targetTag == UNSELECTED_VIEW_TAG) { + nextHitPathByPointerId.remove(activePointerId); + nextEventCoordinatesByPointerId.remove(activePointerId); + } + + mLastHitPathByPointerId = nextHitPathByPointerId; + mLastEventCoordinatesByPointerId = nextEventCoordinatesByPointerId; + mLastButtonState = motionEvent.getButtonState(); + + // Clean up any stale pointerIds + Set allPointerIds = mLastEventCoordinatesByPointerId.keySet(); + mHoveringPointerIds.retainAll(allPointerIds); } private void onMove( @@ -603,18 +600,6 @@ private void onMove( } } - private void dispatchCancelEventForTarget( - View targetView, MotionEvent motionEvent, EventDispatcher eventDispatcher) { - Assertions.assertCondition( - mChildHandlingNativeGesture == -1, - "Expected to not have already sent a cancel for this gesture"); - - int activeIndex = motionEvent.getActionIndex(); - int activePointerId = motionEvent.getPointerId(activeIndex); - PointerEventState eventState = createEventState(activePointerId, motionEvent); - dispatchCancelEventForTarget(targetView, eventState, motionEvent, eventDispatcher); - } - private void dispatchCancelEventForTarget( View targetView, PointerEventState eventState, @@ -650,26 +635,7 @@ private void dispatchCancelEventForTarget( motionEvent)); } - // Need to fire pointer out + pointer leave here as well: - // https://w3c.github.io/pointerevents/#dfn-suppress-a-pointer-event-stream - List outViewTargets = - filterByShouldDispatch(activeHitPath, EVENT.OUT, EVENT.OUT_CAPTURE, false); - List leaveViewTargets = - filterByShouldDispatch(activeHitPath, EVENT.LEAVE, EVENT.LEAVE_CAPTURE, false); - - // dispatch from target -> root - dispatchEventForViewTargets( - PointerEventHelper.POINTER_OUT, eventState, motionEvent, outViewTargets, eventDispatcher); - dispatchEventForViewTargets( - PointerEventHelper.POINTER_LEAVE, - eventState, - motionEvent, - leaveViewTargets, - eventDispatcher); - incrementCoalescingKey(); - mHoveringPointerIds.remove(mPrimaryPointerId); - mHoveringPointerIds.remove(activePointerId); mPrimaryPointerId = UNSET_POINTER_ID; } }