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 eb50d4cf859..ef22bd280dd 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 @@ -294,7 +294,14 @@ public class JSPointerDispatcher { PointerEventState eventState = createEventState(activePointerId, motionEvent); - boolean isExitFromRoot = motionEvent.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT; + // We've empirically determined that when we get a ACTION_HOVER_EXIT from the root view on the + // `onInterceptHoverEvent`, this means we've exited the root view. + // This logic may be wrong but reasoning about the dispatch sequence for HOVER_ENTER/HOVER_EXIT + // doesn't follow the capture/bubbling sequence like other MotionEvents. See: + // https://developer.android.com/reference/android/view/MotionEvent#ACTION_HOVER_ENTER + // https://suragch.medium.com/how-touch-events-are-delivered-in-android-eee3b607b038 + boolean isExitFromRoot = + isCapture && motionEvent.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT; // Calculate the targetTag, with special handling for when we exit the root view. In that case, // we use the root viewId of the last event