Skip to content

Commit

Permalink
W3CPointerEvents: transform coordinates appropriately for cancel even…
Browse files Browse the repository at this point in the history
…ts (#36967)

Summary:
Pull Request resolved: #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
  • Loading branch information
Alex Danoff authored and facebook-github-bot committed Apr 28, 2023
1 parent 679c5b3 commit d0b97a8
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 10 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -63,10 +64,24 @@ public void onChildStartedNativeGesture(
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;
Expand Down Expand Up @@ -228,6 +243,7 @@ public void handleMotionEvent(
// 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) {
Expand All @@ -238,7 +254,9 @@ public void handleMotionEvent(
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<>();
Expand All @@ -248,7 +266,9 @@ public void handleMotionEvent(
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);
Expand Down Expand Up @@ -284,7 +304,7 @@ public void handleMotionEvent(
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
Expand Down Expand Up @@ -504,19 +524,23 @@ private void onMove(
}
}

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");

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.
Expand All @@ -532,18 +556,31 @@ private void dispatchCancelEvent(
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,
Expand All @@ -558,6 +595,44 @@ private void dispatchCancelEvent(
}
}

// 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -356,6 +356,10 @@ public boolean supportsHover(int pointerId) {
return mHoveringPointerIds.contains(pointerId);
}

public Set<Integer> getHoveringPointerIds() {
return mHoveringPointerIds;
}

public final Map<Integer, float[]> getOffsetByPointerId() {
return mOffsetByPointerId;
}
Expand Down

0 comments on commit d0b97a8

Please sign in to comment.