mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Fix onPointerLeave computation by comparing button presses (#50402)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50402 While investigating pointer events, I noticed that all Android views fire `onPointerLeave`/`onPointerEnter` in rapid succession on every button press. This is because Android fires `ACTION_HOVER_EXIT` on every frame before firing `ACTION_DOWN` (in the same frame). The logic in JSPointerDispatcher needed to be updated to account for this. Unfortunately, `ACTION_HOVER_EXIT` events have no means of distinguishing between whether they were driven by the mouse actually leaving the view's bounds or by a button press. Some OS implementations fire this event on the frame just before leaving the view's boundaries while others fire after, so pure X/Y position isn't helpful enough. The button state property on `ACTION_HOVER_EXIT` also never gets set in some OS. To work around this issue, this change posts a callback to the very next frame after receiving `ACTION_HOVER_EXIT`. If no `ACTION_DOWN` event was received in the same frame, it calls all the way through the existing logic. But, if an `ACTION_DOWN` event *is* received, we compare the event timestamps and if they match we don't post `onPointerLeave`. Changelog: [Android][Fixed] - Prevent onPointerLeave from dispatching during button presses Reviewed By: vincentriemer Differential Revision: D72078450 fbshipit-source-id: dcc7cfa4086963ed8599147d60d8406c54cd5d69
This commit is contained in:
committed by
Facebook GitHub Bot
parent
22eb457c94
commit
833ab6fe1b
+57
-6
@@ -8,6 +8,7 @@
|
||||
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;
|
||||
@@ -44,12 +45,14 @@ public class JSPointerDispatcher {
|
||||
private Map<Integer, List<ViewTarget>> mLastHitPathByPointerId;
|
||||
private Map<Integer, float[]> mLastEventCoordinatesByPointerId;
|
||||
private Map<Integer, List<ViewTarget>> mCurrentlyDownPointerIdsToHitPath;
|
||||
private Set<Integer> mHoveringPointerIds = new HashSet<>();
|
||||
private final Set<Integer> mHoveringPointerIds = new HashSet<>();
|
||||
|
||||
private int mChildHandlingNativeGesture = UNSET_CHILD_VIEW_ID;
|
||||
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};
|
||||
@@ -285,9 +288,54 @@ 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();
|
||||
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);
|
||||
@@ -296,13 +344,16 @@ public class JSPointerDispatcher {
|
||||
PointerEventState eventState = createEventState(activePointerId, motionEvent);
|
||||
|
||||
// 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:
|
||||
// `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.
|
||||
//
|
||||
// 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
|
||||
boolean isExitFromRoot =
|
||||
isCapture && motionEvent.getActionMasked() == MotionEvent.ACTION_HOVER_EXIT;
|
||||
boolean isExitFromRoot = isCapture && action == 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
|
||||
|
||||
Reference in New Issue
Block a user