PointerEvents: Mark when listened to for touch interactions

Summary: Changelog: [Internal] - Bypass dispatching an event if no view along the hierarchy is listening to it. Only applied for touch-based interactions. Next change will add optimization for mouse interactions

Reviewed By: vincentriemer

Differential Revision: D35739417

fbshipit-source-id: 134ffefef3bb4f97bf3e63b6bccc0caca464dfbd
This commit is contained in:
Luna Wei
2022-05-23 16:29:52 -07:00
committed by Facebook GitHub Bot
parent 0cff2efd3a
commit 471907c047
8 changed files with 311 additions and 45 deletions
@@ -55,6 +55,12 @@ constexpr MapBuffer::Key VP_SHADOW_COLOR = 31;
constexpr MapBuffer::Key VP_TEST_ID = 32;
constexpr MapBuffer::Key VP_TRANSFORM = 33;
constexpr MapBuffer::Key VP_ZINDEX = 34;
constexpr MapBuffer::Key VP_POINTER_ENTER2 = 35;
constexpr MapBuffer::Key VP_POINTER_LEAVE2 = 36;
constexpr MapBuffer::Key VP_POINTER_MOVE2 = 37;
constexpr MapBuffer::Key VP_POINTER_ENTER2_CAPTURE = 38;
constexpr MapBuffer::Key VP_POINTER_LEAVE2_CAPTURE = 39;
constexpr MapBuffer::Key VP_POINTER_MOVE2_CAPTURE = 40;
// Yoga values
constexpr MapBuffer::Key YG_BORDER_WIDTH = 100;
@@ -462,6 +468,22 @@ static inline MapBuffer viewPropsDiff(
VP_POINTER_LEAVE, newProps.events[ViewEvents::Offset::PointerLeave]);
builder.putBool(
VP_POINTER_MOVE, newProps.events[ViewEvents::Offset::PointerMove]);
builder.putBool(
VP_POINTER_ENTER2, newProps.events[ViewEvents::Offset::PointerEnter2]);
builder.putBool(
VP_POINTER_ENTER2_CAPTURE,
newProps.events[ViewEvents::Offset::PointerEnter2Capture]);
builder.putBool(
VP_POINTER_LEAVE2, newProps.events[ViewEvents::Offset::PointerLeave2]);
builder.putBool(
VP_POINTER_LEAVE2_CAPTURE,
newProps.events[ViewEvents::Offset::PointerLeave2Capture]);
builder.putBool(
VP_POINTER_MOVE2, newProps.events[ViewEvents::Offset::PointerMove2]);
builder.putBool(
VP_POINTER_MOVE2_CAPTURE,
newProps.events[ViewEvents::Offset::PointerMove2Capture]);
}
if (oldProps.removeClippedSubviews != newProps.removeClippedSubviews) {
@@ -557,6 +557,39 @@ public abstract class BaseViewManager<T extends View, C extends LayoutShadowNode
view.setTag(R.id.pointer_move, value);
}
/* Experimental W3C Pointer events start */
@ReactProp(name = "onPointerEnter2")
public void setPointerEnter2(@NonNull T view, boolean value) {
view.setTag(R.id.pointer_enter2, value);
}
@ReactProp(name = "onPointerEnter2Capture")
public void setPointerEnter2Capture(@NonNull T view, boolean value) {
view.setTag(R.id.pointer_enter2_capture, value);
}
@ReactProp(name = "onPointerLeave2")
public void setPointerLeave2(@NonNull T view, boolean value) {
view.setTag(R.id.pointer_leave2, value);
}
@ReactProp(name = "onPointerLeave2Capture")
public void setPointerLeave2Capture(@NonNull T view, boolean value) {
view.setTag(R.id.pointer_leave2_capture, value);
}
@ReactProp(name = "onPointerMove2")
public void setPointerMove2(@NonNull T view, boolean value) {
view.setTag(R.id.pointer_move2, value);
}
@ReactProp(name = "onPointerMove2Capture")
public void setPointerMove2Capture(@NonNull T view, boolean value) {
view.setTag(R.id.pointer_move2_capture, value);
}
/* Experimental W3C Pointer events end */
@ReactProp(name = "onMoveShouldSetResponder")
public void setMoveShouldSetResponder(@NonNull T view, boolean value) {
// no-op, handled by JSResponder
@@ -17,6 +17,7 @@ import com.facebook.react.uimanager.TouchTargetHelper.ViewTarget;
import com.facebook.react.uimanager.events.EventDispatcher;
import com.facebook.react.uimanager.events.PointerEvent;
import com.facebook.react.uimanager.events.PointerEventHelper;
import com.facebook.react.uimanager.events.PointerEventHelper.EVENT;
import com.facebook.react.uimanager.events.TouchEvent;
import com.facebook.react.uimanager.events.TouchEventCoalescingKeyHelper;
import java.util.Collections;
@@ -37,7 +38,7 @@ public class JSPointerDispatcher {
private final TouchEventCoalescingKeyHelper mTouchEventCoalescingKeyHelper =
new TouchEventCoalescingKeyHelper();
private static final float ONMOVE_EPSILON = 1f;
private static final float ONMOVE_EPSILON = 0.1f;
// Set globally for hover interactions, referenced for coalescing hover events
private long mHoverInteractionKey = TouchEvent.UNSET;
@@ -82,7 +83,9 @@ public class JSPointerDispatcher {
if (hitPath.isEmpty()) {
return;
}
int targetTag = hitPath.get(0).getViewId();
TouchTargetHelper.ViewTarget activeViewTarget = hitPath.get(0);
int activeTargetTag = activeViewTarget.getViewId();
if (supportsHover) {
if (action == MotionEvent.ACTION_HOVER_MOVE) {
@@ -107,15 +110,17 @@ public class JSPointerDispatcher {
mTouchEventCoalescingKeyHelper.addCoalescingKey(mDownStartTime);
if (!supportsHover) {
// Enter root -> child
for (int i = hitPath.size(); i-- > 0; ) {
int tag = hitPath.get(i).getViewId();
eventDispatcher.dispatchEvent(
PointerEvent.obtain(PointerEventHelper.POINTER_ENTER, surfaceId, tag, motionEvent));
}
dispatchNonBubblingEventForPathWhenListened(
EVENT.ENTER, EVENT.ENTER_CAPTURE, hitPath, eventDispatcher, surfaceId, motionEvent);
}
boolean listeningForDown =
isAnyoneListeningForBubblingEvent(hitPath, EVENT.DOWN, EVENT.DOWN_CAPTURE);
if (listeningForDown) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_DOWN, surfaceId, activeTargetTag, motionEvent));
}
eventDispatcher.dispatchEvent(
PointerEvent.obtain(PointerEventHelper.POINTER_DOWN, surfaceId, targetTag, motionEvent));
return;
}
@@ -129,25 +134,47 @@ public class JSPointerDispatcher {
// New pointer goes down, this can only happen after ACTION_DOWN is sent for the first pointer
if (action == MotionEvent.ACTION_POINTER_DOWN) {
mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mDownStartTime);
eventDispatcher.dispatchEvent(
PointerEvent.obtain(PointerEventHelper.POINTER_DOWN, surfaceId, targetTag, motionEvent));
boolean listeningForDown =
isAnyoneListeningForBubblingEvent(hitPath, EVENT.DOWN, EVENT.DOWN_CAPTURE);
if (listeningForDown) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_DOWN, surfaceId, activeTargetTag, motionEvent));
}
return;
}
if (action == MotionEvent.ACTION_MOVE) {
int coalescingKey = mTouchEventCoalescingKeyHelper.getCoalescingKey(mDownStartTime);
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_MOVE, surfaceId, targetTag, motionEvent, coalescingKey));
boolean listeningForMove =
isAnyoneListeningForBubblingEvent(hitPath, EVENT.MOVE, EVENT.MOVE_CAPTURE);
if (listeningForMove) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_MOVE,
surfaceId,
activeTargetTag,
motionEvent,
coalescingKey));
}
return;
}
// Exactly one of the pointers goes up, not the last one
if (action == MotionEvent.ACTION_POINTER_UP) {
mTouchEventCoalescingKeyHelper.incrementCoalescingKey(mDownStartTime);
eventDispatcher.dispatchEvent(
PointerEvent.obtain(PointerEventHelper.POINTER_UP, surfaceId, targetTag, motionEvent));
boolean listeningForUp =
isAnyoneListeningForBubblingEvent(hitPath, EVENT.UP, EVENT.UP_CAPTURE);
if (listeningForUp) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_UP, surfaceId, activeTargetTag, motionEvent));
}
return;
}
@@ -159,16 +186,17 @@ public class JSPointerDispatcher {
mTouchEventCoalescingKeyHelper.removeCoalescingKey(mDownStartTime);
mDownStartTime = TouchEvent.UNSET;
eventDispatcher.dispatchEvent(
PointerEvent.obtain(PointerEventHelper.POINTER_UP, surfaceId, targetTag, motionEvent));
boolean listeningForUp =
isAnyoneListeningForBubblingEvent(hitPath, EVENT.UP, EVENT.UP_CAPTURE);
if (listeningForUp) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_UP, surfaceId, activeTargetTag, motionEvent));
}
if (!supportsHover) {
// Leave child -> root
for (int i = 0; i < hitPath.size(); i++) {
int tag = hitPath.get(i).getViewId();
eventDispatcher.dispatchEvent(
PointerEvent.obtain(PointerEventHelper.POINTER_LEAVE, surfaceId, tag, motionEvent));
}
dispatchNonBubblingEventForPathWhenListened(
EVENT.LEAVE, EVENT.LEAVE_CAPTURE, hitPath, eventDispatcher, surfaceId, motionEvent);
}
return;
}
@@ -183,15 +211,54 @@ public class JSPointerDispatcher {
"Warning : Motion Event was ignored. Action="
+ action
+ " Target="
+ targetTag
+ activeTargetTag
+ " Supports Hover="
+ supportsHover);
}
private int findTargetTagAndSetCoordinates(MotionEvent ev) {
// This method updates `mTargetCoordinates` with coordinates for the motion event.
return TouchTargetHelper.findTargetTagAndCoordinatesForTouch(
ev.getX(), ev.getY(), mRootViewGroup, mTargetCoordinates, null);
private static boolean isAnyoneListeningForBubblingEvent(
List<ViewTarget> hitPath, EVENT event, EVENT captureEvent) {
for (ViewTarget viewTarget : hitPath) {
if (PointerEventHelper.isListening(viewTarget.getView(), event)
|| PointerEventHelper.isListening(viewTarget.getView(), captureEvent)) {
return true;
}
}
return false;
}
/*
Dispatch event only if ancestor is listening to relevant event.
This should only be relevant for ENTER/LEAVE events.
@param hitPath - ordered from inner target to root
*/
/** Dispatch non-bubbling event along the hit path only when relevant listeners */
private static void dispatchNonBubblingEventForPathWhenListened(
EVENT event,
EVENT captureEvent,
List<ViewTarget> hitPath,
EventDispatcher dispatcher,
int surfaceId,
MotionEvent motionEvent) {
boolean ancestorListening = false;
String eventName = PointerEventHelper.getDispatchableEventName(event);
if (eventName == null) {
return;
}
// iterate through hitPath from ancestor -> target
for (int i = hitPath.size() - 1; i >= 0; i--) {
View view = hitPath.get(i).getView();
int viewId = hitPath.get(i).getViewId();
if (ancestorListening
|| (i == 0 && PointerEventHelper.isListening(view, event))
|| PointerEventHelper.isListening(view, captureEvent)) {
dispatcher.dispatchEvent(PointerEvent.obtain(eventName, surfaceId, viewId, motionEvent));
ancestorListening = true;
}
}
}
// called on hover_move motion events only
@@ -311,21 +378,21 @@ public class JSPointerDispatcher {
int surfaceId = UIManagerHelper.getSurfaceId(mRootViewGroup);
if (!hitPath.isEmpty()) {
int targetTag = hitPath.get(0).getViewId();
// Question: Does cancel fire on all in hit path?
Assertions.assertNotNull(eventDispatcher)
.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_CANCEL, surfaceId, targetTag, motionEvent));
for (ViewTarget viewTarget : hitPath) {
eventDispatcher.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_LEAVE, surfaceId, viewTarget.getViewId(), motionEvent));
boolean listeningForCancel =
isAnyoneListeningForBubblingEvent(hitPath, EVENT.CANCEL, EVENT.CANCEL_CAPTURE);
if (listeningForCancel) {
int targetTag = hitPath.get(0).getViewId();
Assertions.assertNotNull(eventDispatcher)
.dispatchEvent(
PointerEvent.obtain(
PointerEventHelper.POINTER_CANCEL, surfaceId, targetTag, motionEvent));
}
}
mTouchEventCoalescingKeyHelper.removeCoalescingKey(mDownStartTime);
mDownStartTime = TouchEvent.UNSET;
dispatchNonBubblingEventForPathWhenListened(
EVENT.LEAVE, EVENT.LEAVE_CAPTURE, hitPath, eventDispatcher, surfaceId, motionEvent);
mTouchEventCoalescingKeyHelper.removeCoalescingKey(mDownStartTime);
mDownStartTime = TouchEvent.UNSET;
}
}
}
@@ -8,6 +8,11 @@
package com.facebook.react.uimanager.events;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.Nullable;
import com.facebook.common.logging.FLog;
import com.facebook.react.R;
import com.facebook.react.common.ReactConstants;
/** Class responsible for generating catalyst touch events based on android {@link MotionEvent}. */
public class PointerEventHelper {
@@ -17,6 +22,21 @@ public class PointerEventHelper {
public static final String POINTER_TYPE_MOUSE = "mouse";
public static final String POINTER_TYPE_UNKNOWN = "";
public static enum EVENT {
CANCEL,
CANCEL_CAPTURE,
DOWN,
DOWN_CAPTURE,
ENTER,
ENTER_CAPTURE,
LEAVE,
LEAVE_CAPTURE,
MOVE,
MOVE_CAPTURE,
UP,
UP_CAPTURE,
};
public static final String POINTER_CANCEL = "topPointerCancel";
public static final String POINTER_DOWN = "topPointerDown";
public static final String POINTER_ENTER = "topPointerEnter2";
@@ -24,6 +44,27 @@ public class PointerEventHelper {
public static final String POINTER_MOVE = "topPointerMove2";
public static final String POINTER_UP = "topPointerUp";
/** We don't dispatch capture events from native; that's currently handled by JS. */
public static @Nullable String getDispatchableEventName(EVENT event) {
switch (event) {
case LEAVE:
return PointerEventHelper.POINTER_LEAVE;
case DOWN:
return PointerEventHelper.POINTER_DOWN;
case MOVE:
return PointerEventHelper.POINTER_MOVE;
case ENTER:
return PointerEventHelper.POINTER_ENTER;
case CANCEL:
return PointerEventHelper.POINTER_CANCEL;
case UP:
return PointerEventHelper.POINTER_UP;
default:
FLog.e(ReactConstants.TAG, "No dispatchable event name for type: " + event);
return null;
}
}
public static String getW3CPointerType(final int toolType) {
// https://www.w3.org/TR/pointerevents3/#dom-pointerevent-pointertype
switch (toolType) {
@@ -39,6 +80,50 @@ public class PointerEventHelper {
return POINTER_TYPE_UNKNOWN;
}
public static boolean isListening(@Nullable View view, EVENT event) {
if (view == null) {
return false;
}
Object value = null;
switch (event) {
case DOWN:
case DOWN_CAPTURE:
case UP:
case UP_CAPTURE:
case CANCEL:
case CANCEL_CAPTURE:
return true;
case ENTER:
value = view.getTag(R.id.pointer_enter2);
break;
case ENTER_CAPTURE:
value = view.getTag(R.id.pointer_enter2_capture);
break;
case LEAVE:
value = view.getTag(R.id.pointer_leave2);
break;
case LEAVE_CAPTURE:
value = view.getTag(R.id.pointer_leave2_capture);
break;
case MOVE:
value = view.getTag(R.id.pointer_move2);
break;
case MOVE_CAPTURE:
value = view.getTag(R.id.pointer_move2_capture);
break;
}
if (value == null) {
return false;
}
if (value instanceof Boolean) {
return (Boolean) value;
}
return false;
}
public static int getEventCategory(String pointerEventType) {
if (pointerEventType == null) {
return EventCategoryDef.UNSPECIFIED;
@@ -55,6 +55,12 @@ object ReactMapBufferPropSetter {
private const val VP_TEST_ID = 32
private const val VP_TRANSFORM = 33
private const val VP_ZINDEX = 34
private const val VP_POINTER_ENTER2 = 35
private const val VP_POINTER_LEAVE2 = 36
private const val VP_POINTER_MOVE2 = 37
private const val VP_POINTER_ENTER2_CAPTURE = 38
private const val VP_POINTER_LEAVE2_CAPTURE = 39
private const val VP_POINTER_MOVE2_CAPTURE = 40
// Yoga values
private const val YG_BORDER_WIDTH = 100
@@ -185,6 +191,24 @@ object ReactMapBufferPropSetter {
VP_POINTER_MOVE -> {
viewManager.setPointerMove(view, entry.booleanValue)
}
VP_POINTER_ENTER2 -> {
viewManager.setPointerEnter2(view, entry.booleanValue)
}
VP_POINTER_LEAVE2 -> {
viewManager.setPointerLeave2(view, entry.booleanValue)
}
VP_POINTER_MOVE2 -> {
viewManager.setPointerMove2(view, entry.booleanValue)
}
VP_POINTER_ENTER2_CAPTURE -> {
viewManager.setPointerEnter2Capture(view, entry.booleanValue)
}
VP_POINTER_LEAVE2_CAPTURE -> {
viewManager.setPointerLeave2Capture(view, entry.booleanValue)
}
VP_POINTER_MOVE2_CAPTURE -> {
viewManager.setPointerMove2Capture(view, entry.booleanValue)
}
VP_REMOVE_CLIPPED_SUBVIEW -> {
viewManager.setRemoveClippedSubviews(view, entry.booleanValue)
}