mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Use buttonState to distinguish ACTION_DOWN and ACTION_HOVER_EXIT (#53033)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/53033 This diff replaces the logic introduced in D72078450 to prevent `onPointerEnter`/`onPointerLeave` from firing when a button is pressed. The new approach trades off some complexity for reliability: instead of deferring `ACTION_HOVER_EXIT` handling to the next frame, we now suppress it immediately if any button is pressed (`buttonState != 0`). This simpler logic appears to work reliably on Quest devices, though it may behave differently on the Android emulator (something we’ll monitor). The main reason for this change is that deferring ACTION_HOVER_EXIT introduces problems in newer Spatial React use cases, particularly when a single component hierarchy spans multiple roots. For example, when hovering between ReactSurfaceRoot and another root like VolumetricWindow, deferring ACTION_HOVER_EXIT can lead to incorrect enter/exit ordering: * Cursor starts hovering over `ReactSurfaceRoot` * Cursor moves to `VolumetricWindow` * `ACTION_HOVER_EXIT` (`ReactSurfaceRoot`) — deferred * `ACTION_HOVER_ENTER` (`VolumetricWindow`) — processed * `ACTION_HOVER_EXIT` (`ReactSurfaceRoot`) — processed (too late) This results in inconsistent hover state updates across roots, which this diff resolves by handling `ACTION_HOVER_EXIT` immediately when appropriate. Changelog: [Internal] Reviewed By: Abbondanzo Differential Revision: D79504775 fbshipit-source-id: ea97bff48ddf4d3d09caf56ca29057c202b12409
This commit is contained in:
committed by
Facebook GitHub Bot
parent
d547d9e56e
commit
893730633c
+16
-50
@@ -8,7 +8,6 @@
|
||||
package com.facebook.react.uimanager;
|
||||
|
||||
import android.graphics.Rect;
|
||||
import android.view.Choreographer;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -52,8 +51,6 @@ public class JSPointerDispatcher {
|
||||
private int mPrimaryPointerId = UNSET_POINTER_ID;
|
||||
private int mCoalescingKey = 0;
|
||||
private int mLastButtonState = 0;
|
||||
private volatile long mLastActionDownEventTime = 0;
|
||||
private boolean mRunHoverExitNextFrame = true;
|
||||
private final ViewGroup mRootViewGroup;
|
||||
|
||||
private static final int[] sRootScreenCoords = {0, 0};
|
||||
@@ -289,54 +286,25 @@ public class JSPointerDispatcher {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Android does not provide a consistent mechanism for determining if a MotionEvent is outside
|
||||
* the bounds of a view. It fires ACTION_HOVER_EXIT in two cases:
|
||||
*
|
||||
* <ol>
|
||||
* <li>If the cursor leaves the bounds of the view
|
||||
* <li>If the user presses a button
|
||||
* </ol>
|
||||
*
|
||||
* <p>Some OS will fire ACTION_HOVER_EXIT on the frame before the cursor leaves the bounds of
|
||||
* the view, while others will fire it on the frame after the cursor leaves the bounds of the
|
||||
* view, so using bounds is not sufficient. Some OS will include the button state in the
|
||||
* ACTION_HOVER_EXIT event while others will not, so using button state is not sufficient.
|
||||
* Instead, we must wait for both the ACTION_HOVER_EXIT and ACTION_DOWN events to fire, and then
|
||||
* compare their event times to determine if the ACTION_HOVER_EXIT event was triggered by the
|
||||
* cursor leaving the bounds of the view or by a button press. If no ACTION_DOWN event has fired
|
||||
* by the next frame, we know that the cursor has left the bounds of the root view.
|
||||
*
|
||||
* <p>As ACTION_DOWN fires after ACTION_HOVER_EXIT, we need to wait until the next frame to make
|
||||
* this determination. We do this by posting a frame callback to the choreographer and
|
||||
* re-running this method on the next frame should timestamps between the two events not align.
|
||||
*/
|
||||
if (isCapture
|
||||
&& mRunHoverExitNextFrame
|
||||
&& motionEvent.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT) {
|
||||
mRunHoverExitNextFrame = false;
|
||||
Choreographer.getInstance()
|
||||
.postFrameCallback(
|
||||
new Choreographer.FrameCallback() {
|
||||
@Override
|
||||
public void doFrame(long frameTimeNanos) {
|
||||
if (mLastActionDownEventTime != motionEvent.getEventTime()) {
|
||||
handleMotionEventHelper(motionEvent, eventDispatcher, isCapture);
|
||||
}
|
||||
mRunHoverExitNextFrame = true;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
handleMotionEventHelper(motionEvent, eventDispatcher, isCapture);
|
||||
}
|
||||
}
|
||||
|
||||
private void handleMotionEventHelper(
|
||||
MotionEvent motionEvent, EventDispatcher eventDispatcher, boolean isCapture) {
|
||||
int action = motionEvent.getActionMasked();
|
||||
|
||||
// On stylus or mouse input, Android will systematically dispatch ACTION_HOVER_EXIT
|
||||
// before ACTION_DOWN (button press), even if the pointer has not moved and is still
|
||||
// hovering over the same view.
|
||||
//
|
||||
// This leads to onPointerLeave being triggered incorrectly.
|
||||
//
|
||||
// To mitigate this, we suppress ACTION_HOVER_EXIT events that occur
|
||||
// while a button is pressed (i.e., buttonState != 0).
|
||||
//
|
||||
// This workaround is effective on Quest devices, however, it may not behave consistently on the
|
||||
// Android emulator, something we’ll revisit if it becomes an issue in open source.
|
||||
if (action == MotionEvent.ACTION_HOVER_EXIT && motionEvent.getButtonState() != 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
int activePointerId = motionEvent.getPointerId(motionEvent.getActionIndex());
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
mLastActionDownEventTime = motionEvent.getEventTime();
|
||||
mPrimaryPointerId = motionEvent.getPointerId(0);
|
||||
} else if (action == MotionEvent.ACTION_HOVER_MOVE) {
|
||||
mHoveringPointerIds.add(activePointerId);
|
||||
@@ -349,8 +317,6 @@ public class JSPointerDispatcher {
|
||||
// reasoning about the dispatch sequence for HOVER_ENTER/HOVER_EXIT doesn't follow the
|
||||
// capture/bubbling sequence like other MotionEvents.
|
||||
//
|
||||
// The choreographer logic above is a hack to try to work around this, but it's not perfect.
|
||||
//
|
||||
// For more information, see:
|
||||
// https://developer.android.com/reference/android/view/MotionEvent#ACTION_HOVER_ENTER
|
||||
// https://suragch.medium.com/how-touch-events-are-delivered-in-android-eee3b607b038
|
||||
|
||||
Reference in New Issue
Block a user