W3CPointerEvents: transform coordinates appropriately for cancel events (#36967)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/36967

Changelog: [Internal] - W3CPointerEvents: transform coordinates appropriately for cancel events

This change updates the behavior of pointer cancel events on Android to fire on the correct target. In particular:
1. Transform motion event from child frame into root frame before processing
2. Zero-out coordinates on pointer cancel event in frame of target before dispatching

This is specifically necessary for cancel events due to the way they're propagated between views: we fire a cancel event when a child view starts handling a native gesture (e.g. a scroll view starts being scrolled). However, the native gesture event (MotionEvent) is in the frame of the child view, whereas the rest of the existing pointer event code expects events to be in the frame of the react root view. We don't have to do this conversion for other events since the corresponding MotionEvents are received directly by the root view.

Reviewed By: lunaleaps

Differential Revision: D45064308

fbshipit-source-id: f21d260f6aeb85e5d0efd53a48da6dd1e7b5efbc
This commit is contained in:
Alex Danoff
2023-04-28 09:28:55 -07:00
committed by Facebook GitHub Bot
parent 679c5b38f7
commit d0b97a8c4c
2 changed files with 89 additions and 10 deletions
@@ -7,6 +7,7 @@
package com.facebook.react.uimanager;
import android.graphics.Rect;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
@@ -63,10 +64,24 @@ public class JSPointerDispatcher {
return;
}
dispatchCancelEvent(motionEvent, eventDispatcher);
MotionEvent motionInRoot = convertMotionToRootFrame(childView, motionEvent);
dispatchCancelEventForTarget(childView, motionInRoot, eventDispatcher);
mChildHandlingNativeGesture = childView.getId();
}
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;
}
public void onChildEndedNativeGesture() {
// There should be only one child gesture at any given time. We can safely turn off the flag.
mChildHandlingNativeGesture = -1;
@@ -228,6 +243,7 @@ public class JSPointerDispatcher {
// 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
int activeTargetTag;
View activeTargetView;
List<ViewTarget> activeHitPath;
if (isExitFromRoot) {
@@ -238,7 +254,9 @@ public class JSPointerDispatcher {
if (lastHitPath == null || lastHitPath.isEmpty()) {
return;
}
activeTargetTag = lastHitPath.get(lastHitPath.size() - 1).getViewId();
ViewTarget activeTarget = lastHitPath.get(lastHitPath.size() - 1);
activeTargetTag = activeTarget.getViewId();
activeTargetView = activeTarget.getView();
// Explicitly make the hit path for this cursor empty
activeHitPath = new ArrayList<>();
@@ -248,7 +266,9 @@ public class JSPointerDispatcher {
if (activeHitPath == null || activeHitPath.isEmpty()) {
return;
}
activeTargetTag = activeHitPath.get(0).getViewId();
ViewTarget activeTarget = activeHitPath.get(0);
activeTargetTag = activeTarget.getViewId();
activeTargetView = activeTarget.getView();
}
handleHitStateDivergence(activeTargetTag, eventState, motionEvent, eventDispatcher);
@@ -284,7 +304,7 @@ public class JSPointerDispatcher {
onUp(activeTargetTag, eventState, motionEvent, eventDispatcher);
break;
case MotionEvent.ACTION_CANCEL:
dispatchCancelEvent(eventState, motionEvent, eventDispatcher);
dispatchCancelEventForTarget(activeTargetView, eventState, motionEvent, eventDispatcher);
break;
case MotionEvent.ACTION_HOVER_ENTER:
// Ignore these events as enters will be calculated from HOVER_MOVE
@@ -504,7 +524,8 @@ public class JSPointerDispatcher {
}
}
private void dispatchCancelEvent(MotionEvent motionEvent, EventDispatcher eventDispatcher) {
private void dispatchCancelEventForTarget(
View targetView, MotionEvent motionEvent, EventDispatcher eventDispatcher) {
Assertions.assertCondition(
mChildHandlingNativeGesture == -1,
"Expected to not have already sent a cancel for this gesture");
@@ -512,11 +533,14 @@ public class JSPointerDispatcher {
int activeIndex = motionEvent.getActionIndex();
int activePointerId = motionEvent.getPointerId(activeIndex);
PointerEventState eventState = createEventState(activePointerId, motionEvent);
dispatchCancelEvent(eventState, motionEvent, eventDispatcher);
dispatchCancelEventForTarget(targetView, eventState, motionEvent, eventDispatcher);
}
private void dispatchCancelEvent(
PointerEventState eventState, MotionEvent motionEvent, EventDispatcher eventDispatcher) {
private void dispatchCancelEventForTarget(
View targetView,
PointerEventState eventState,
MotionEvent motionEvent,
EventDispatcher eventDispatcher) {
// This means the gesture has already ended, via some other CANCEL or UP event. This is not
// expected to happen very often as it would mean some child View has decided to intercept the
// touch stream and start a native gesture only upon receiving the UP/CANCEL event.
@@ -532,18 +556,31 @@ public class JSPointerDispatcher {
isAnyoneListeningForBubblingEvent(activeHitPath, EVENT.CANCEL, EVENT.CANCEL_CAPTURE);
if (listeningForCancel) {
int targetTag = activeHitPath.get(0).getViewId();
// cancel events need to report client coordinates of (0, 0) and offset coordinates relative
// to the root view
int[] childOffset = getChildOffsetRelativeToRoot(targetView);
PointerEventState normalizedEventState =
normalizeToRoot(eventState, childOffset[0], childOffset[1]);
Assertions.assertNotNull(eventDispatcher)
.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_CANCEL, targetTag, eventState, motionEvent));
PointerEventHelper.POINTER_CANCEL,
targetTag,
normalizedEventState,
motionEvent));
}
// TODO(luwe) - Need to fire pointer out here as well:
// Need to fire pointer out + pointer leave here as well:
// https://w3c.github.io/pointerevents/#dfn-suppress-a-pointer-event-stream
List<ViewTarget> outViewTargets =
filterByShouldDispatch(activeHitPath, EVENT.OUT, EVENT.OUT_CAPTURE, false);
List<ViewTarget> 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,
@@ -558,6 +595,44 @@ public class JSPointerDispatcher {
}
}
// returns child (0, 0) relative to root coordinate system
private int[] getChildOffsetRelativeToRoot(View childView) {
Rect childCoords = new Rect(0, 0, 1, 1);
mRootViewGroup.offsetDescendantRectToMyCoords(childView, childCoords);
return new int[] {childCoords.top, childCoords.left};
}
// Returns a copy of `original` with coordinates zeroed relative to the provided root coordinates.
// In particular,
// - the event (client) coordinates will all be set to 0
// - the offset coordinates will be set to the root coordinates
private PointerEventState normalizeToRoot(PointerEventState original, float rootX, float rootY) {
Map<Integer, float[]> newOffsets = new HashMap<>(original.getOffsetByPointerId());
Map<Integer, float[]> newEventCoords = new HashMap<>(original.getEventCoordinatesByPointerId());
for (Map.Entry<Integer, float[]> offsetEntry : newOffsets.entrySet()) {
float[] offsetValue = offsetEntry.getValue();
offsetValue[0] = rootX;
offsetValue[1] = rootY;
}
for (Map.Entry<Integer, float[]> eventCoordsEntry : newEventCoords.entrySet()) {
float[] eventCoordsValue = eventCoordsEntry.getValue();
eventCoordsValue[0] = 0;
eventCoordsValue[1] = 0;
}
return new PointerEventState(
original.getPrimaryPointerId(),
original.getActivePointerId(),
original.getLastButtonState(),
original.getSurfaceId(),
newOffsets,
new HashMap<>(original.getHitPathByPointerId()),
newEventCoords,
new HashSet<>(original.getHoveringPointerIds()));
}
private static void debugPrintHitPath(List<ViewTarget> hitPath) {
StringBuilder builder = new StringBuilder("hitPath: ");
for (ViewTarget viewTarget : hitPath) {
@@ -356,6 +356,10 @@ public class PointerEvent extends Event<PointerEvent> {
return mHoveringPointerIds.contains(pointerId);
}
public Set<Integer> getHoveringPointerIds() {
return mHoveringPointerIds;
}
public final Map<Integer, float[]> getOffsetByPointerId() {
return mOffsetByPointerId;
}