From 047200ba2dfd91b742a8ec933efb0be66ba5420a Mon Sep 17 00:00:00 2001 From: Alex Danoff Date: Thu, 1 Jun 2023 07:08:41 -0700 Subject: [PATCH] W3CPointerEvents: properly update hit path during native gestures (#37638) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/37638 Changelog: [Android] [Fixed] - W3CPointerEvents: properly update hit path during native gestures Per [the W3C spec](https://www.w3.org/TR/pointerevents/#the-pointercancel-event), we need to fire pointerout and pointerleave after firing a pointercancel. However, in cases where the pointer doesn't physically leave the target after a cancel (e.g. scrolling by clicking and dragging), we would never re-fire a pointerenter event once the native gesture was completed. This change fixes the bug by clearing out the last hit path (and other relevant state) for the pointer when we start handling a native gesture. Then we'll re-fire a pointerenter as expected upon the next motion event (due to the logic in handleHitStateDivergence). Note: this bug only affected hovering pointers (e.g. mouse) because for non-hovering pointers the native gesture won't end unless the pointer is physically removed (i.e. finger is lifted). Reviewed By: javache Differential Revision: D46226021 fbshipit-source-id: c583771b99efd1480b51e44bb0bf0a9f3a97779b --- .../react/uimanager/JSPointerDispatcher.java | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 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 88eec78e2d875a..13701c36f65756 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 @@ -70,6 +70,9 @@ public void onChildStartedNativeGesture( dispatchCancelEventForTarget(childView, motionInRoot, eventDispatcher); mChildHandlingNativeGesture = childView.getId(); + + // clear "previous" state since interaction was canceled + resetPreviousStateForMotionEvent(motionEvent); } private MotionEvent convertMotionToRootFrame(View childView, MotionEvent childMotion) { @@ -84,6 +87,28 @@ private MotionEvent convertMotionToRootFrame(View childView, MotionEvent childMo return motionInRoot; } + private void updatePreviousStateFromEvent(MotionEvent event, PointerEventState eventState) { + // Caching the event state so we have a new "last" + mLastHitPathByPointerId = eventState.getHitPathByPointerId(); + mLastEventCoordinatesByPointerId = eventState.getEventCoordinatesByPointerId(); + mLastButtonState = event.getButtonState(); + + // 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; + } + public void onChildEndedNativeGesture() { // There should be only one child gesture at any given time. We can safely turn off the flag. mChildHandlingNativeGesture = -1; @@ -368,14 +393,7 @@ public void handleMotionEvent( return; } - // Caching the event state so we have a new "last" - mLastHitPathByPointerId = eventState.getHitPathByPointerId(); - mLastEventCoordinatesByPointerId = eventState.getEventCoordinatesByPointerId(); - mLastButtonState = motionEvent.getButtonState(); - - // Clean up any stale pointerIds - Set allPointerIds = mLastEventCoordinatesByPointerId.keySet(); - mHoveringPointerIds.retainAll(allPointerIds); + updatePreviousStateFromEvent(motionEvent, eventState); } private static boolean isAnyoneListeningForBubblingEvent(