From 980c52de41258f6cf2d2360144ea7ca16a19c9f8 Mon Sep 17 00:00:00 2001 From: Andrei Shikov Date: Thu, 10 Feb 2022 06:05:21 -0800 Subject: [PATCH] Disable view flattening when the view has event handlers on Android Summary: The views with touch event props are currently flattened by Fabric core, as we don't take event listeners into account when calculating whether the view should be flattened. This results in a confusing situation when components with touch event listeners (e.g. ` {}} /> `) or ones using `PanResponder` are either ignored (iOS) or cause a crash (Android). This change passes touch event props to C++ layer and uses them to calculate whether the view node should be flattened or not. It also refactors events to be kept as a singular bitset with 32 bit (~`uint32_t`). Changelog: [Changed][General] Avoid flattening nodes with event props Reviewed By: sammy-SC Differential Revision: D34005536 fbshipit-source-id: 96255b389a7bfff4aa208a96fd0c173d9edf1512 --- .../NativeComponent/PlatformBaseViewConfig.js | 55 ++++++- React/Views/RCTViewManager.m | 25 ++++ .../react/uimanager/BaseViewManager.java | 97 ++++++++++++- .../react/uimanager/LayoutShadowNode.java | 6 +- .../uimanager/UIManagerModuleConstants.java | 6 +- .../renderer/components/view/ViewProps.cpp | 19 +-- .../renderer/components/view/ViewProps.h | 6 +- .../components/view/ViewShadowNode.cpp | 4 +- .../renderer/components/view/primitives.h | 41 ++++++ .../components/view/propsConversions.h | 136 ++++++++++++++++++ 10 files changed, 352 insertions(+), 43 deletions(-) diff --git a/Libraries/NativeComponent/PlatformBaseViewConfig.js b/Libraries/NativeComponent/PlatformBaseViewConfig.js index 1487637b0302d9..c3454a3eecc315 100644 --- a/Libraries/NativeComponent/PlatformBaseViewConfig.js +++ b/Libraries/NativeComponent/PlatformBaseViewConfig.js @@ -38,13 +38,13 @@ const PlatformBaseViewConfig: PartialViewConfigWithoutName = registrationName: 'onAccessibilityAction', }, topPointerEnter: { - registrationName: 'pointerenter', + registrationName: 'onPointerEnter', }, topPointerLeave: { - registrationName: 'pointerleave', + registrationName: 'onPointerLeave', }, topPointerMove: { - registrationName: 'pointermove', + registrationName: 'onPointerMove', }, onGestureHandlerEvent: DynamicallyInjectedByGestureHandler({ registrationName: 'onGestureHandlerEvent', @@ -219,9 +219,31 @@ const PlatformBaseViewConfig: PartialViewConfigWithoutName = position: true, onLayout: true, - pointerenter: true, - pointerleave: true, - pointermove: true, + // Pointer events + onPointerEnter: true, + onPointerLeave: true, + onPointerMove: true, + + // PanResponder handlers + onMoveShouldSetResponder: true, + onMoveShouldSetResponderCapture: true, + onStartShouldSetResponder: true, + onStartShouldSetResponderCapture: true, + onResponderGrant: true, + onResponderReject: true, + onResponderStart: true, + onResponderEnd: true, + onResponderRelease: true, + onResponderMove: true, + onResponderTerminate: true, + onResponderTerminationRequest: true, + onShouldBlockNativeResponder: true, + + // Touch events + onTouchStart: true, + onTouchMove: true, + onTouchEnd: true, + onTouchCancel: true, style: ReactNativeStyleAttributes, }, @@ -456,6 +478,27 @@ const PlatformBaseViewConfig: PartialViewConfigWithoutName = onAccessibilityAction: true, onAccessibilityEscape: true, onAccessibilityTap: true, + + // PanResponder handlers + onMoveShouldSetResponder: true, + onMoveShouldSetResponderCapture: true, + onStartShouldSetResponder: true, + onStartShouldSetResponderCapture: true, + onResponderGrant: true, + onResponderReject: true, + onResponderStart: true, + onResponderEnd: true, + onResponderRelease: true, + onResponderMove: true, + onResponderTerminate: true, + onResponderTerminationRequest: true, + onShouldBlockNativeResponder: true, + + // Touch events + onTouchStart: true, + onTouchMove: true, + onTouchEnd: true, + onTouchCancel: true, }), }, }; diff --git a/React/Views/RCTViewManager.m b/React/Views/RCTViewManager.m index a46158dcee39ab..a18174ea4fe40d 100644 --- a/React/Views/RCTViewManager.m +++ b/React/Views/RCTViewManager.m @@ -418,4 +418,29 @@ - (RCTShadowView *)shadowView RCT_EXPORT_SHADOW_PROPERTY(direction, YGDirection) +// The events below define the properties that are not used by native directly, but required in the view config for new +// renderer to function. +// They can be deleted after Static View Configs are rolled out. + +// PanResponder handlers +RCT_CUSTOM_VIEW_PROPERTY(onMoveShouldSetResponder, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onMoveShouldSetResponderCapture, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onStartShouldSetResponder, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onStartShouldSetResponderCapture, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderGrant, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderReject, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderStart, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderEnd, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderRelease, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderMove, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderTerminate, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onResponderTerminationRequest, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onShouldBlockNativeResponder, BOOL, RCTView) {} + +// Touch events +RCT_CUSTOM_VIEW_PROPERTY(onTouchStart, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onTouchMove, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onTouchEnd, BOOL, RCTView) {} +RCT_CUSTOM_VIEW_PROPERTY(onTouchCancel, BOOL, RCTView) {} + @end 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 3c9c1eae3bb3ad..b48b272d9c2a26 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/BaseViewManager.java @@ -486,18 +486,103 @@ private void logUnsupportedPropertyWarning(String propName) { FLog.w(ReactConstants.TAG, "%s doesn't support property '%s'", getName(), propName); } - @ReactProp(name = "pointerenter") - public void setPointerEnter(@NonNull T view, @Nullable boolean value) { + @ReactProp(name = "onPointerEnter") + public void setPointerEnter(@NonNull T view, boolean value) { view.setTag(R.id.pointer_enter, value); } - @ReactProp(name = "pointerleave") - public void setPointerLeave(@NonNull T view, @Nullable boolean value) { + @ReactProp(name = "onPointerLeave") + public void setPointerLeave(@NonNull T view, boolean value) { view.setTag(R.id.pointer_leave, value); } - @ReactProp(name = "pointermove") - public void setPointerMove(@NonNull T view, @Nullable boolean value) { + @ReactProp(name = "onPointerMove") + public void setPointerMove(@NonNull T view, boolean value) { view.setTag(R.id.pointer_move, value); } + + @ReactProp(name = "onMoveShouldSetResponder") + public void setMoveShouldSetResponder(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onMoveShouldSetResponderCapture") + public void setMoveShouldSetResponderCapture(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onStartShouldSetResponder") + public void setStartShouldSetResponder(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onStartShouldSetResponderCapture") + public void setStartShouldSetResponderCapture(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderGrant") + public void setResponderGrant(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderReject") + public void setResponderReject(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderStart") + public void setResponderStart(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderEnd") + public void setResponderEnd(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderRelease") + public void setResponderRelease(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderMove") + public void setResponderMove(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderTerminate") + public void setResponderTerminate(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onResponderTerminationRequest") + public void setResponderTerminationRequest(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onShouldBlockNativeResponder") + public void setShouldBlockNativeResponder(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onTouchStart") + public void setTouchStart(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onTouchMove") + public void setTouchMove(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onTouchEnd") + public void setTouchEnd(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } + + @ReactProp(name = "onTouchCancel") + public void setTouchCancel(@NonNull T view, boolean value) { + // no-op, handled by JSResponder + } } diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java index 5cc1ad9e4ccaa4..f5a7d8a9ca603a 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/LayoutShadowNode.java @@ -808,19 +808,19 @@ public void setShouldNotifyOnLayout(boolean shouldNotifyOnLayout) { super.setShouldNotifyOnLayout(shouldNotifyOnLayout); } - @ReactProp(name = "pointerenter") + @ReactProp(name = "onPointerEnter") public void setShouldNotifyPointerEnter(boolean value) { // This method exists to inject Native View configs in RN Android VR // DO NOTHING } - @ReactProp(name = "pointerleave") + @ReactProp(name = "onPointerLeave") public void setShouldNotifyPointerLeave(boolean value) { // This method exists to inject Native View configs in RN Android VR // DO NOTHING } - @ReactProp(name = "pointermove") + @ReactProp(name = "onPointerMove") public void setShouldNotifyPointerMove(boolean value) { // This method exists to inject Native View configs in RN Android VR // DO NOTHING diff --git a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java index 71b64cd2364faf..ad9321f895eea3 100644 --- a/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java +++ b/ReactAndroid/src/main/java/com/facebook/react/uimanager/UIManagerModuleConstants.java @@ -59,9 +59,9 @@ return MapBuilder.builder() .put("topContentSizeChange", MapBuilder.of(rn, "onContentSizeChange")) .put("topLayout", MapBuilder.of(rn, "onLayout")) - .put("topPointerEnter", MapBuilder.of(rn, "pointerenter")) - .put("topPointerLeave", MapBuilder.of(rn, "pointerleave")) - .put("topPointerMove", MapBuilder.of(rn, "pointermove")) + .put("topPointerEnter", MapBuilder.of(rn, "onPointerEnter")) + .put("topPointerLeave", MapBuilder.of(rn, "onPointerLeave")) + .put("topPointerMove", MapBuilder.of(rn, "onPointerMove")) .put("topLoadingError", MapBuilder.of(rn, "onLoadingError")) .put("topLoadingFinish", MapBuilder.of(rn, "onLoadingFinish")) .put("topLoadingStart", MapBuilder.of(rn, "onLoadingStart")) diff --git a/ReactCommon/react/renderer/components/view/ViewProps.cpp b/ReactCommon/react/renderer/components/view/ViewProps.cpp index bcc53907706155..9c694fd501f754 100644 --- a/ReactCommon/react/renderer/components/view/ViewProps.cpp +++ b/ReactCommon/react/renderer/components/view/ViewProps.cpp @@ -125,24 +125,7 @@ ViewProps::ViewProps( "onLayout", sourceProps.onLayout, {})), - pointerEnter(convertRawProp( - context, - rawProps, - "pointerenter", - sourceProps.pointerEnter, - {})), - pointerLeave(convertRawProp( - context, - rawProps, - "pointerleave", - sourceProps.pointerLeave, - {})), - pointerMove(convertRawProp( - context, - rawProps, - "pointermove", - sourceProps.pointerMove, - {})), + events(convertRawProp(context, rawProps, sourceProps.events, {})), collapsable(convertRawProp( context, rawProps, diff --git a/ReactCommon/react/renderer/components/view/ViewProps.h b/ReactCommon/react/renderer/components/view/ViewProps.h index 1f43dfa60d39d4..29dfa265eb1136 100644 --- a/ReactCommon/react/renderer/components/view/ViewProps.h +++ b/ReactCommon/react/renderer/components/view/ViewProps.h @@ -61,11 +61,7 @@ class ViewProps : public YogaStylableProps, public AccessibilityProps { EdgeInsets hitSlop{}; bool onLayout{}; - bool pointerEnter{}; - - bool pointerLeave{}; - - bool pointerMove{}; + ViewEvents events{}; bool collapsable{true}; diff --git a/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp b/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp index 661858cba131d3..bbc05ea771dc5f 100644 --- a/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp +++ b/ReactCommon/react/renderer/components/view/ViewShadowNode.cpp @@ -48,8 +48,8 @@ void ViewShadowNode::initialize() noexcept { bool formsView = formsStackingContext || isColorMeaningful(viewProps.backgroundColor) || - isColorMeaningful(viewProps.foregroundColor) || viewProps.pointerEnter || - viewProps.pointerLeave || viewProps.pointerMove || + isColorMeaningful(viewProps.foregroundColor) || + viewProps.events.bits.any() || !(viewProps.yogaStyle.border() == YGStyle::Edges{}) || !viewProps.testId.empty(); diff --git a/ReactCommon/react/renderer/components/view/primitives.h b/ReactCommon/react/renderer/components/view/primitives.h index 5f86de9a49b5eb..66df8707a0fe8d 100644 --- a/ReactCommon/react/renderer/components/view/primitives.h +++ b/ReactCommon/react/renderer/components/view/primitives.h @@ -11,6 +11,7 @@ #include #include #include +#include #include namespace facebook { @@ -18,6 +19,46 @@ namespace react { enum class PointerEventsMode { Auto, None, BoxNone, BoxOnly }; +struct ViewEvents { + std::bitset<32> bits{}; + + enum class Offset : std::size_t { + // Pointer events + PointerEnter = 0, + PointerMove = 1, + PointerLeave = 2, + + // PanResponder callbacks + MoveShouldSetResponder = 3, + MoveShouldSetResponderCapture = 4, + StartShouldSetResponder = 5, + StartShouldSetResponderCapture = 6, + ResponderGrant = 7, + ResponderReject = 8, + ResponderStart = 9, + ResponderEnd = 10, + ResponderRelease = 11, + ResponderMove = 12, + ResponderTerminate = 13, + ResponderTerminationRequest = 14, + ShouldBlockNativeResponder = 15, + + // Touch events + TouchStart = 16, + TouchMove = 17, + TouchEnd = 18, + TouchCancel = 19, + }; + + constexpr bool operator[](const Offset offset) const { + return bits[static_cast(offset)]; + } + + std::bitset<32>::reference operator[](const Offset offset) { + return bits[static_cast(offset)]; + } +}; + enum class BackfaceVisibility { Auto, Visible, Hidden }; enum class BorderStyle { Solid, Dotted, Dashed }; diff --git a/ReactCommon/react/renderer/components/view/propsConversions.h b/ReactCommon/react/renderer/components/view/propsConversions.h index 6d7233f50c0ea2..502b14704be160 100644 --- a/ReactCommon/react/renderer/components/view/propsConversions.h +++ b/ReactCommon/react/renderer/components/view/propsConversions.h @@ -463,5 +463,141 @@ static inline CascadedRectangleEdges convertRawProp( return result; } +static inline ViewEvents convertRawProp( + const PropsParserContext &context, + RawProps const &rawProps, + ViewEvents const &sourceValue, + ViewEvents const &defaultValue) { + ViewEvents result{}; + using Offset = ViewEvents::Offset; + + result[Offset::PointerEnter] = convertRawProp( + context, + rawProps, + "onPointerEnter", + sourceValue[Offset::PointerEnter], + defaultValue[Offset::PointerEnter]); + result[Offset::PointerMove] = convertRawProp( + context, + rawProps, + "onPointerMove", + sourceValue[Offset::PointerMove], + defaultValue[Offset::PointerMove]); + result[Offset::PointerLeave] = convertRawProp( + context, + rawProps, + "onPointerLeave", + sourceValue[Offset::PointerLeave], + defaultValue[Offset::PointerLeave]); + + // PanResponder callbacks + result[Offset::MoveShouldSetResponder] = convertRawProp( + context, + rawProps, + "onMoveShouldSetResponder", + sourceValue[Offset::MoveShouldSetResponder], + defaultValue[Offset::MoveShouldSetResponder]); + result[Offset::MoveShouldSetResponderCapture] = convertRawProp( + context, + rawProps, + "onMoveShouldSetResponderCapture", + sourceValue[Offset::MoveShouldSetResponderCapture], + defaultValue[Offset::MoveShouldSetResponderCapture]); + result[Offset::StartShouldSetResponder] = convertRawProp( + context, + rawProps, + "onStartShouldSetResponder", + sourceValue[Offset::StartShouldSetResponder], + defaultValue[Offset::StartShouldSetResponder]); + result[Offset::StartShouldSetResponderCapture] = convertRawProp( + context, + rawProps, + "onStartShouldSetResponderCapture", + sourceValue[Offset::StartShouldSetResponderCapture], + defaultValue[Offset::StartShouldSetResponderCapture]); + result[Offset::ResponderGrant] = convertRawProp( + context, + rawProps, + "onResponderGrant", + sourceValue[Offset::ResponderGrant], + defaultValue[Offset::ResponderGrant]); + result[Offset::ResponderReject] = convertRawProp( + context, + rawProps, + "onResponderReject", + sourceValue[Offset::ResponderReject], + defaultValue[Offset::ResponderReject]); + result[Offset::ResponderStart] = convertRawProp( + context, + rawProps, + "onResponderStart", + sourceValue[Offset::ResponderStart], + defaultValue[Offset::ResponderStart]); + result[Offset::ResponderEnd] = convertRawProp( + context, + rawProps, + "onResponderEnd", + sourceValue[Offset::ResponderEnd], + defaultValue[Offset::ResponderEnd]); + result[Offset::ResponderRelease] = convertRawProp( + context, + rawProps, + "onResponderRelease", + sourceValue[Offset::ResponderRelease], + defaultValue[Offset::ResponderRelease]); + result[Offset::ResponderMove] = convertRawProp( + context, + rawProps, + "onResponderMove", + sourceValue[Offset::ResponderMove], + defaultValue[Offset::ResponderMove]); + result[Offset::ResponderTerminate] = convertRawProp( + context, + rawProps, + "onResponderTerminate", + sourceValue[Offset::ResponderTerminate], + defaultValue[Offset::ResponderTerminate]); + result[Offset::ResponderTerminationRequest] = convertRawProp( + context, + rawProps, + "onResponderTerminationRequest", + sourceValue[Offset::ResponderTerminationRequest], + defaultValue[Offset::ResponderTerminationRequest]); + result[Offset::ShouldBlockNativeResponder] = convertRawProp( + context, + rawProps, + "onShouldBlockNativeResponder", + sourceValue[Offset::ShouldBlockNativeResponder], + defaultValue[Offset::ShouldBlockNativeResponder]); + + // Touch events + result[Offset::TouchStart] = convertRawProp( + context, + rawProps, + "onTouchStart", + sourceValue[Offset::TouchStart], + defaultValue[Offset::TouchStart]); + result[Offset::TouchMove] = convertRawProp( + context, + rawProps, + "onTouchMove", + sourceValue[Offset::TouchMove], + defaultValue[Offset::TouchMove]); + result[Offset::TouchEnd] = convertRawProp( + context, + rawProps, + "onTouchEnd", + sourceValue[Offset::TouchEnd], + defaultValue[Offset::TouchEnd]); + result[Offset::TouchCancel] = convertRawProp( + context, + rawProps, + "onTouchCancel", + sourceValue[Offset::TouchCancel], + defaultValue[Offset::TouchCancel]); + + return result; +} + } // namespace react } // namespace facebook