From 471907c0471ffebf8edde6a337844dc349d4cfc5 Mon Sep 17 00:00:00 2001 From: Luna Wei Date: Mon, 23 May 2022 16:29:52 -0700 Subject: [PATCH] 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 --- .../react/fabric/jni/viewPropConversions.h | 22 +++ .../react/uimanager/BaseViewManager.java | 33 ++++ .../react/uimanager/JSPointerDispatcher.java | 155 +++++++++++++----- .../uimanager/events/PointerEventHelper.java | 85 ++++++++++ .../views/view/ReactMapBufferPropSetter.kt | 24 +++ .../main/res/views/uimanager/values/ids.xml | 9 +- .../renderer/components/view/primitives.h | 4 + .../components/view/propsConversions.h | 24 +++ 8 files changed, 311 insertions(+), 45 deletions(-) diff --git a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h index 6fd7eb18130..a65c586fbff 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h +++ b/ReactAndroid/src/main/java/com/facebook/react/fabric/jni/viewPropConversions.h @@ -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) { diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java index 3cf7db7be1b..a877786acb8 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -557,6 +557,39 @@ public abstract class BaseViewManager 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 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 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; + } } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java index f265b0f55ac..04649e56e1b 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/events/PointerEventHelper.java @@ -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; diff --git a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt index 5ee668497e6..247dd1eb617 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt +++ b/ReactAndroid/src/main/java/com/facebook/react/views/view/ReactMapBufferPropSetter.kt @@ -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) } diff --git a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml index b9e9b732bb3..8bb47aebeb4 100644 --- a/ReactAndroid/src/main/res/views/uimanager/values/ids.xml +++ b/ReactAndroid/src/main/res/views/uimanager/values/ids.xml @@ -40,5 +40,12 @@ - + + + + + + + + diff --git a/ReactCommon/react/renderer/components/view/primitives.h b/ReactCommon/react/renderer/components/view/primitives.h index 47d17e28748..62fc433ae9b 100644 --- a/ReactCommon/react/renderer/components/view/primitives.h +++ b/ReactCommon/react/renderer/components/view/primitives.h @@ -53,6 +53,10 @@ struct ViewEvents { // W3C Pointer Events PointerEnter2 = 20, PointerLeave2 = 21, + PointerMove2 = 22, + PointerEnter2Capture = 23, + PointerLeave2Capture = 24, + PointerMove2Capture = 25, }; constexpr bool operator[](const Offset offset) const { diff --git a/ReactCommon/react/renderer/components/view/propsConversions.h b/ReactCommon/react/renderer/components/view/propsConversions.h index 19f3de9c1be..80e631af91a 100644 --- a/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/ReactCommon/react/renderer/components/view/propsConversions.h @@ -499,12 +499,36 @@ static inline ViewEvents convertRawProp( "onPointerEnter2", sourceValue[Offset::PointerEnter2], defaultValue[Offset::PointerEnter2]); + result[Offset::PointerEnter2Capture] = convertRawProp( + context, + rawProps, + "onPointerEnter2Capture", + sourceValue[Offset::PointerEnter2Capture], + defaultValue[Offset::PointerEnter2Capture]); + result[Offset::PointerMove2] = convertRawProp( + context, + rawProps, + "onPointerMove2", + sourceValue[Offset::PointerMove2], + defaultValue[Offset::PointerMove2]); + result[Offset::PointerMove2Capture] = convertRawProp( + context, + rawProps, + "onPointerMove2Capture", + sourceValue[Offset::PointerMove2Capture], + defaultValue[Offset::PointerMove2Capture]); result[Offset::PointerLeave2] = convertRawProp( context, rawProps, "onPointerLeave2", sourceValue[Offset::PointerLeave2], defaultValue[Offset::PointerLeave2]); + result[Offset::PointerLeave2Capture] = convertRawProp( + context, + rawProps, + "onPointerLeave2Capture", + sourceValue[Offset::PointerLeave2Capture], + defaultValue[Offset::PointerLeave2Capture]); // PanResponder callbacks result[Offset::MoveShouldSetResponder] = convertRawProp(