From d5429fac6ccb3642f83c8032f5c13d7117c34e54 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Fri, 23 Oct 2026 10:56:58 +0200 Subject: [PATCH 1/2] Make layout queries aware of synchronously updated transforms Summary: Native-driven animations (Animated with `useNativeDriver`, the shared Animation Backend, and third-party libraries like Reanimated's synchronous props path) apply non-layout props such as `transform` directly to the mounted views through `synchronouslyUpdateViewOnUIThread`, without a ShadowTree commit. The committed tree therefore lags behind the screen for the duration of the animation. Layout queries (`measure`, `measureInWindow`, `getBoundingClientRect`, `findNodeAtPoint`, `UIManager::getRelativeLayoutMetrics`) read the committed `transform` prop, while native hit-testing uses the real view hierarchy. `Pressability` mixes the two: it measures the responder region with `measure()` and compares it with the touch coordinates reported by the platform, so a press with a slight finger movement on a view that is being animated leaves the stale rectangle and `onPress` is silently dropped (facebook/react-native#51621, #36504, #44768; the sticky header hack `passthroughAnimatedPropExplicitValues` exists for the same reason). This adds a `SynchronousTransformOverlay` to `UIManager`: a thread-safe map from view tag to the last `transform` / `transformOrigin` written synchronously, recorded at the platform entry points every synchronous update funnels through (`FabricUIManager.synchronouslyUpdateViewOnUIThread` and the batched `AnimatedProps` path on Android, `RCTSurfacePresenter` on iOS, `SchedulerDelegateImpl` on ReactCxxPlatform). The overlay implements a new `LayoutableShadowNode::TransformOverrideProvider` interface, which `computeRelativeLayoutMetrics` and `findNodeAtPoint` consult (through `LayoutInspectingPolicy`) before falling back to `getTransform()`. Entries are resolved against the committed layout metrics exactly like `ConcreteViewShadowNode::getTransform` resolves props, live until the view is deleted (lazy pruning on commit) or until the writer removes them (the Animation Backend does so when it clears its registry after the `AnimationEndSync` commit). Libraries that bypass the platform entry points can call `UIManager::recordSynchronousTransform` directly. Everything is behind the new `enableSynchronousTransformOverlay` feature flag (default off). See `ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md`. Changelog: [GENERAL] [FIXED] - Layout queries and Pressable now take natively-driven `transform` animations into account (behind `enableSynchronousTransformOverlay`) Test Plan: - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedSynchronousTransformOverlay-itest.js`: 16/16 passed (both flag values x `useSharedAnimatedBackend` x `animatedDeferStartOfTimingAnimations`). - New gtest cases in `LayoutableShadowNodeTest`, `FindNodeAtPointTest` and `SynchronousTransformOverlayTest` (not run locally: no OSS C++ unit test runner). - `yarn flow-check`, ESLint on the changed JS, `yarn test` for the feature flags, `yarn format`. - RNTester, new "Pressable inside a natively animated view" example, with the flag enabled (see the follow-up commit): on a Samsung Galaxy A16 (Android 16) and the iPhone 18 Pro simulator, a press with finger drift on the moving button fires both `onPressIn` and `onPress`, and `measure()` reports the live position. On `main` the same press only fires `onPressIn`. - Not run: `scripts/cxx-api` snapshot regeneration (requires doxygen). Co-Authored-By: Claude Fable 5.1 --- __docs__/README.md | 1 + ...imatedSynchronousTransformOverlay-itest.js | 327 ++++++++++++++++++ .../React/Fabric/RCTSurfacePresenter.mm | 7 + .../react/fabric/FabricUIManager.java | 20 ++ .../react/fabric/FabricUIManagerBinding.kt | 7 + .../featureflags/ReactNativeFeatureFlags.kt | 8 +- .../ReactNativeFeatureFlagsCxxAccessor.kt | 12 +- .../ReactNativeFeatureFlagsCxxInterop.kt | 4 +- .../ReactNativeFeatureFlagsDefaults.kt | 4 +- .../ReactNativeFeatureFlagsLocalAccessor.kt | 13 +- .../ReactNativeFeatureFlagsProvider.kt | 4 +- .../react/fabric/FabricUIManagerBinding.cpp | 17 + .../jni/react/fabric/FabricUIManagerBinding.h | 2 + .../JReactNativeFeatureFlagsCxxInterop.cpp | 16 +- .../JReactNativeFeatureFlagsCxxInterop.h | 5 +- .../featureflags/ReactNativeFeatureFlags.cpp | 6 +- .../featureflags/ReactNativeFeatureFlags.h | 7 +- .../ReactNativeFeatureFlagsAccessor.cpp | 110 +++--- .../ReactNativeFeatureFlagsAccessor.h | 6 +- .../ReactNativeFeatureFlagsDefaults.h | 6 +- .../ReactNativeFeatureFlagsDynamicProvider.h | 11 +- .../ReactNativeFeatureFlagsProvider.h | 3 +- .../react/nativemodule/dom/NativeDOM.cpp | 15 +- .../NativeReactNativeFeatureFlags.cpp | 7 +- .../NativeReactNativeFeatureFlags.h | 4 +- .../AnimatedPropsRegistry.cpp | 17 + .../animationbackend/AnimatedPropsRegistry.h | 8 + .../animationbackend/AnimationBackend.cpp | 14 +- .../renderer/core/LayoutableShadowNode.cpp | 27 +- .../renderer/core/LayoutableShadowNode.h | 26 ++ .../core/tests/LayoutableShadowNodeTest.cpp | 119 +++++++ .../ReactCommon/react/renderer/dom/DOM.cpp | 24 +- .../ReactCommon/react/renderer/dom/DOM.h | 16 +- .../uimanager/SynchronousTransformOverlay.cpp | 134 +++++++ .../uimanager/SynchronousTransformOverlay.h | 105 ++++++ .../react/renderer/uimanager/UIManager.cpp | 82 ++++- .../react/renderer/uimanager/UIManager.h | 39 +++ .../renderer/uimanager/UIManagerBinding.cpp | 20 +- .../__docs__/SynchronousTransformOverlay.md | 77 +++++ .../tests/SynchronousTransformOverlayTest.cpp | 185 ++++++++++ .../scheduler/SchedulerDelegateImpl.cpp | 3 + .../ReactNativeFeatureFlags.config.js | 11 + .../featureflags/ReactNativeFeatureFlags.js | 7 +- .../specs/NativeReactNativeFeatureFlags.js | 3 +- .../api-snapshots/ReactAndroidDebugCxx.api | 7 + .../ReactAndroidFrameworksCxx.api | 26 +- .../api-snapshots/ReactAndroidNewarchCxx.api | 7 + .../api-snapshots/ReactAndroidReleaseCxx.api | 7 + .../api-snapshots/ReactAppleDebugCxx.api | 7 + .../api-snapshots/ReactAppleFrameworksCxx.api | 26 +- .../api-snapshots/ReactAppleNewarchCxx.api | 7 + .../api-snapshots/ReactAppleReleaseCxx.api | 7 + .../api-snapshots/ReactCommonDebugCxx.api | 7 + .../ReactCommonFrameworksCxx.api | 26 +- .../api-snapshots/ReactCommonNewarchCxx.api | 7 + .../api-snapshots/ReactCommonReleaseCxx.api | 7 + 56 files changed, 1580 insertions(+), 100 deletions(-) create mode 100644 packages/react-native/Libraries/Animated/__tests__/AnimatedSynchronousTransformOverlay-itest.js create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md create mode 100644 packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp diff --git a/__docs__/README.md b/__docs__/README.md index 4ff46942ed26..b21441fe6171 100644 --- a/__docs__/README.md +++ b/__docs__/README.md @@ -48,6 +48,7 @@ TODO: Explain the different components of React Native at a high level. - [Runtime Shadow Node Reference Update](../packages/react-native/ReactCommon/react/renderer/core/__docs__/RSNRU.md) - [passChildrenWhenCloningPersistedNodes](../packages/react-native/ReactCommon/react/renderer/core/__docs__/passChildrenWhenCloning.md) - Layout + - [Synchronous Transform Overlay](../packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md) - Mounting - [Animation Backend](../packages/react-native/ReactCommon/react/renderer/animationbackend/__docs__/AnimationBackend.md) - [Native Animated](../packages/react-native/ReactCommon/react/renderer/animated/__docs__/NativeAnimated.md) diff --git a/packages/react-native/Libraries/Animated/__tests__/AnimatedSynchronousTransformOverlay-itest.js b/packages/react-native/Libraries/Animated/__tests__/AnimatedSynchronousTransformOverlay-itest.js new file mode 100644 index 000000000000..ae3335c357f6 --- /dev/null +++ b/packages/react-native/Libraries/Animated/__tests__/AnimatedSynchronousTransformOverlay-itest.js @@ -0,0 +1,327 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @fantom_flags enableSynchronousTransformOverlay:* useSharedAnimatedBackend:* animatedDeferStartOfTimingAnimations:* + * @flow strict-local + * @format + */ + +import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment'; + +import type {HostInstance} from 'react-native'; + +import * as ReactNativeFeatureFlags from '../../../src/private/featureflags/ReactNativeFeatureFlags'; +import * as Fantom from '@react-native/fantom'; +import nullthrows from 'nullthrows'; +import * as React from 'react'; +import {createRef} from 'react'; +import {Animated, Easing, View, useAnimatedValue} from 'react-native'; + +// Deferred start outputs the initial value on the first animation frame and +// re-anchors timing on the second. This delays animation progress by one +// frame interval (~16ms at 60 fps). +const DEFERRED_START_MS = + ReactNativeFeatureFlags.animatedDeferStartOfTimingAnimations() ? 16 : 0; + +const OVERLAY_ENABLED = + ReactNativeFeatureFlags.enableSynchronousTransformOverlay(); + +function measure(instance: HostInstance): {pageX: number, pageY: number} { + let result = {pageX: NaN, pageY: NaN}; + instance.measure((x, y, width, height, pageX, pageY) => { + result = {pageX, pageY}; + }); + return result; +} + +function measureInWindow(instance: HostInstance): {x: number, y: number} { + let result = {x: NaN, y: NaN}; + instance.measureInWindow((x, y) => { + result = {x, y}; + }); + return result; +} + +/** + * Native-driven animations write non-layout props like `transform` directly + * to the mounted views, without committing to the shadow tree. With the + * `enableSynchronousTransformOverlay` flag, layout queries take those + * synchronous transforms into account, so they match what is on the screen + * (which is what e.g. `Pressable` relies on to validate presses). + */ +test('layout queries reflect a natively-driven transform mid-animation', () => { + let _translateX; + const viewRef = createRef(); + const childRef = createRef(); + + function MyApp() { + const translateX = useAnimatedValue(0); + _translateX = translateX; + return ( + + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const viewElement = nullthrows(viewRef.current); + const childElement = nullthrows(childRef.current); + + expect(viewElement.getBoundingClientRect().x).toBe(0); + expect(childElement.getBoundingClientRect().x).toBe(10); + + Fantom.runTask(() => { + Animated.timing(_translateX, { + toValue: 100, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500 + DEFERRED_START_MS); + + // The view was moved through the synchronous path only. + const transform = + // $FlowFixMe[incompatible-use] + Fantom.unstable_getDirectManipulationProps(viewElement).transform[0]; + expect(transform.translateX).toBeCloseTo(50, 0.001); + + // Without the overlay the shadow tree is not synchronised yet and the + // queries still report the committed position. With the overlay they + // report the position of the mounted view, for the animated view itself + // and for its descendants alike. + const expectedX = OVERLAY_ENABLED ? 50 : 0; + + expect(viewElement.getBoundingClientRect().x).toBeCloseTo(expectedX, 0.001); + expect(measure(viewElement).pageX).toBeCloseTo(expectedX, 0.001); + expect(measureInWindow(viewElement).x).toBeCloseTo(expectedX, 0.001); + + expect(childElement.getBoundingClientRect().x).toBeCloseTo( + expectedX + 10, + 0.001, + ); + expect(measure(childElement).pageX).toBeCloseTo(expectedX + 10, 0.001); + + Fantom.unstable_produceFramesForDuration(500); + + // Animation is completed now. C++ Animated commits the final position to + // the shadow tree, after which the overlay is no longer needed and the + // queries agree with the committed tree. + Fantom.runWorkLoop(); + expect(viewElement.getBoundingClientRect().x).toBe(100); + expect(childElement.getBoundingClientRect().x).toBe(110); +}); + +test('layout queries compose natively-driven transforms of nested views', () => { + let _outerTranslateX; + let _innerTranslateY; + const leafRef = createRef(); + const innerRef = createRef(); + + function MyApp() { + const outerTranslateX = useAnimatedValue(0); + const innerTranslateY = useAnimatedValue(0); + _outerTranslateX = outerTranslateX; + _innerTranslateY = innerTranslateY; + return ( + + + + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const innerElement = nullthrows(innerRef.current); + const leafElement = nullthrows(leafRef.current); + + expect(leafElement.getBoundingClientRect().x).toBe(25); + expect(leafElement.getBoundingClientRect().y).toBe(20); + + // Both levels are animated through the synchronous path, each by a + // different value. + Fantom.runTask(() => { + Animated.timing(_outerTranslateX, { + toValue: 80, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + Animated.timing(_innerTranslateY, { + toValue: 60, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500 + DEFERRED_START_MS); + + // The leaf has no transform of its own: its position is the composition of + // the synchronous transforms of both ancestors. + const expectedOuterX = OVERLAY_ENABLED ? 40 : 0; + const expectedInnerY = OVERLAY_ENABLED ? 30 : 0; + + const innerRect = innerElement.getBoundingClientRect(); + expect(innerRect.x).toBeCloseTo(20 + expectedOuterX, 0.001); + expect(innerRect.y).toBeCloseTo(20 + expectedInnerY, 0.001); + + const leafRect = leafElement.getBoundingClientRect(); + expect(leafRect.x).toBeCloseTo(25 + expectedOuterX, 0.001); + expect(leafRect.y).toBeCloseTo(20 + expectedInnerY, 0.001); + expect(measure(leafElement).pageX).toBeCloseTo(25 + expectedOuterX, 0.001); + expect(measure(leafElement).pageY).toBeCloseTo(20 + expectedInnerY, 0.001); + + Fantom.unstable_produceFramesForDuration(500); + Fantom.runWorkLoop(); + + // Both final values are committed. + expect(leafElement.getBoundingClientRect().x).toBe(105); + expect(leafElement.getBoundingClientRect().y).toBe(80); +}); + +test('a natively-driven transform composes with a committed scale of an ancestor', () => { + let _translateY; + const innerRef = createRef(); + + function MyApp() { + const translateY = useAnimatedValue(0); + _translateY = translateY; + return ( + // The committed `scale` is applied around the center of the outer view. + + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const innerElement = nullthrows(innerRef.current); + + // (20, 20, 100, 100) scaled by 0.5 around (100, 100). + let rect = innerElement.getBoundingClientRect(); + expect(rect.x).toBe(60); + expect(rect.y).toBe(60); + expect(rect.width).toBe(50); + expect(rect.height).toBe(50); + + Fantom.runTask(() => { + Animated.timing(_translateY, { + toValue: 60, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500 + DEFERRED_START_MS); + + // The synchronous translation (30) moves the inner view in the outer view's + // coordinate space before the committed scale of the outer view applies: + // (20, 50) scaled by 0.5 around (100, 100) gives (60, 75). + rect = innerElement.getBoundingClientRect(); + expect(rect.x).toBeCloseTo(60, 0.001); + expect(rect.y).toBeCloseTo(OVERLAY_ENABLED ? 75 : 60, 0.001); + expect(rect.width).toBeCloseTo(50, 0.001); + expect(rect.height).toBeCloseTo(50, 0.001); + + Fantom.unstable_produceFramesForDuration(500); + Fantom.runWorkLoop(); + + // Final value committed: (20, 80) scaled by 0.5 around (100, 100). + rect = innerElement.getBoundingClientRect(); + expect(rect.x).toBe(60); + expect(rect.y).toBe(90); +}); + +test('layout queries reflect a natively-driven transform after unmounting other views', () => { + let _translateY; + const viewRef = createRef(); + + function MyApp({showSibling}: {showSibling: boolean}) { + const translateY = useAnimatedValue(0); + _translateY = translateY; + return ( + <> + {showSibling ? : null} + + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + const viewElement = nullthrows(viewRef.current); + expect(viewElement.getBoundingClientRect().y).toBe(10); + + Fantom.runTask(() => { + Animated.timing(_translateY, { + toValue: 100, + duration: 1000, + easing: Easing.linear, + useNativeDriver: true, + }).start(); + }); + + Fantom.unstable_produceFramesForDuration(500 + DEFERRED_START_MS); + + // A commit that deletes other views keeps the overlay entry of the view + // that is still mounted. + Fantom.runTask(() => { + root.render(); + }); + + // Without the overlay, the committed position is only up to date when the + // shared Animation Backend is on: its commit hook re-applies the animated + // value to every React commit. + const expectedY = + OVERLAY_ENABLED || ReactNativeFeatureFlags.useSharedAnimatedBackend() + ? 50 + : 0; + expect(viewElement.getBoundingClientRect().y).toBeCloseTo(expectedY, 0.001); +}); diff --git a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm index e5b19bd47658..a60512454fd9 100644 --- a/packages/react-native/React/Fabric/RCTSurfacePresenter.mm +++ b/packages/react-native/React/Fabric/RCTSurfacePresenter.mm @@ -208,6 +208,13 @@ - (void)schedulerDidSynchronouslyUpdateViewOnUIThread:(ReactTag)tag props:(folly return; } + // Every synchronous update (Animated, the Animation Backend, third-party + // animation libraries) funnels through here, so this is where UIManager + // learns about transforms that bypassed the ShadowTree. + if (ReactNativeFeatureFlags::enableSynchronousTransformOverlay()) { + scheduler.uiManager->recordSynchronousTransform(tag, props); + } + [_mountingManager synchronouslyUpdateViewOnUIThread:tag changedProps:std::move(props) componentDescriptor:*componentDescriptor]; diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java index 2727e880d083..a93ab53c9585 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManager.java @@ -34,6 +34,7 @@ import com.facebook.infer.annotation.Nullsafe; import com.facebook.infer.annotation.ThreadConfined; import com.facebook.proguard.annotations.DoNotStripAny; +import com.facebook.react.bridge.Arguments; import com.facebook.react.bridge.ColorPropConverter; import com.facebook.react.bridge.GuardedRunnable; import com.facebook.react.bridge.LifecycleEventListener; @@ -832,6 +833,8 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable MountItem synchronousMountItem = new SynchronousMountItem(reactTag, props); + recordSynchronousTransform(reactTag, props); + // If the reactTag exists, we assume that it might at the end of the next // batch of MountItems. Otherwise, we try to execute immediately. if (!mMountingManager.getViewExists(reactTag)) { @@ -856,6 +859,23 @@ public void synchronouslyUpdateViewOnUIThread(final int reactTag, final Readable ReactMarkerConstants.FABRIC_UPDATE_UI_MAIN_THREAD_END, null, commitNumber); } + /** + * Lets UIManager know about the `transform` / `transformOrigin` of a synchronous update, so that + * layout queries (measure, measureInWindow, etc.) describe the mounted view rather than the + * committed ShadowTree, which the synchronous path bypasses. See `SynchronousTransformOverlay`. + */ + private void recordSynchronousTransform(final int reactTag, final ReadableMap props) { + if (mBinding == null || !ReactNativeFeatureFlags.enableSynchronousTransformOverlay()) { + return; + } + if (!props.hasKey("transform") && !props.hasKey("transformOrigin")) { + return; + } + // `props` is either a JavaOnlyMap (legacy Animated) or a ReadableNativeMap that the mount + // item still needs, so a fresh NativeMap is handed to C++ (it is consumed by the call). + mBinding.recordSynchronousTransform(reactTag, Arguments.makeNativeMap(props.toHashMap())); + } + /** Called from C++ via JNI with a batch encoded by AnimatedPropBufferEncoder.cpp. */ @SuppressLint("NotInvokedPrivateMethod") @SuppressWarnings("unused") diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt index b1b3f12f50c2..1daf354f7c6e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/FabricUIManagerBinding.kt @@ -79,6 +79,13 @@ internal class FabricUIManagerBinding : HybridClassBase() { external fun driveCxxAnimations() + /** + * Records the `transform` / `transformOrigin` of a synchronous view update in UIManager, so that + * layout queries (measure, etc.) match the mounted view. `transformProps` must only contain those + * keys and is consumed by the call. + */ + external fun recordSynchronousTransform(reactTag: Int, transformProps: NativeMap) + external fun driveAnimationBackend(frameTimeNanos: Long) external fun drainPreallocateViewsQueue() diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt index 7fcdaaeec16d..63d8fb2b6b26 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6394b2f1569b795b117d2afea8fdd4d8>> */ /** @@ -342,6 +342,12 @@ public object ReactNativeFeatureFlags { @JvmStatic public fun enableSwiftUIBasedFilters(): Boolean = accessor.enableSwiftUIBasedFilters() + /** + * When enabled, transforms applied to views synchronously on the UI thread (e.g. by native-driven animations, without a ShadowTree commit) are recorded in UIManager and taken into account by layout queries (measure, measureInWindow, getBoundingClientRect), so that they match the mounted views. + */ + @JvmStatic + public fun enableSynchronousTransformOverlay(): Boolean = accessor.enableSynchronousTransformOverlay() + /** * Enables View Culling: as soon as a view goes off screen, it can be reused anywhere in the UI and pieced together with other items to create new UI elements. */ diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt index 6e0da4d9d0e4..f2adda9cba7b 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -72,6 +72,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null private var enableResizeObserverByDefaultCache: Boolean? = null private var enableSwiftUIBasedFiltersCache: Boolean? = null + private var enableSynchronousTransformOverlayCache: Boolean? = null private var enableViewCullingCache: Boolean? = null private var enableViewRecyclingCache: Boolean? = null private var enableViewRecyclingForImageCache: Boolean? = null @@ -586,6 +587,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces return cached } + override fun enableSynchronousTransformOverlay(): Boolean { + var cached = enableSynchronousTransformOverlayCache + if (cached == null) { + cached = ReactNativeFeatureFlagsCxxInterop.enableSynchronousTransformOverlay() + enableSynchronousTransformOverlayCache = cached + } + return cached + } + override fun enableViewCulling(): Boolean { var cached = enableViewCullingCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt index 2268d03f3674..41fad6851830 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6d86d4adbc261c9271b5bed72bd12dcf>> + * @generated SignedSource<> */ /** @@ -132,6 +132,8 @@ public object ReactNativeFeatureFlagsCxxInterop { @DoNotStrip @JvmStatic public external fun enableSwiftUIBasedFilters(): Boolean + @DoNotStrip @JvmStatic public external fun enableSynchronousTransformOverlay(): Boolean + @DoNotStrip @JvmStatic public external fun enableViewCulling(): Boolean @DoNotStrip @JvmStatic public external fun enableViewRecycling(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt index defafe46f6b4..aa5b78941b29 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<8b2e167a9022cc009a7cd2ff0d4093b9>> */ /** @@ -127,6 +127,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi override fun enableSwiftUIBasedFilters(): Boolean = false + override fun enableSynchronousTransformOverlay(): Boolean = false + override fun enableViewCulling(): Boolean = false override fun enableViewRecycling(): Boolean = false diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt index 53af4b4edab0..288b7db994bb 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -76,6 +76,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc private var enablePropsUpdateReconciliationAndroidCache: Boolean? = null private var enableResizeObserverByDefaultCache: Boolean? = null private var enableSwiftUIBasedFiltersCache: Boolean? = null + private var enableSynchronousTransformOverlayCache: Boolean? = null private var enableViewCullingCache: Boolean? = null private var enableViewRecyclingCache: Boolean? = null private var enableViewRecyclingForImageCache: Boolean? = null @@ -642,6 +643,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc return cached } + override fun enableSynchronousTransformOverlay(): Boolean { + var cached = enableSynchronousTransformOverlayCache + if (cached == null) { + cached = currentProvider.enableSynchronousTransformOverlay() + accessedFeatureFlags.add("enableSynchronousTransformOverlay") + enableSynchronousTransformOverlayCache = cached + } + return cached + } + override fun enableViewCulling(): Boolean { var cached = enableViewCullingCache if (cached == null) { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt index 8fa2f4269464..f08cf8c14315 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<4a8e2033558127866a761f0df4b79b6a>> */ /** @@ -127,6 +127,8 @@ public interface ReactNativeFeatureFlagsProvider { @DoNotStrip public fun enableSwiftUIBasedFilters(): Boolean + @DoNotStrip public fun enableSynchronousTransformOverlay(): Boolean + @DoNotStrip public fun enableViewCulling(): Boolean @DoNotStrip public fun enableViewRecycling(): Boolean diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index f601a71bf6cd..7ed7f2754cdb 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -860,10 +860,24 @@ void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateViewOnUIThread( void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateAnimatedProps( const std::unordered_map& updates) { if (ReactNativeFeatureFlags::cxxNativeAnimatedEnabled() && mountingManager_) { + // TODO: record the transforms of this batched path (behind + // `optimizedAnimatedPropUpdates`) in the synchronous transform overlay; + // it bypasses `FabricUIManager.synchronouslyUpdateViewOnUIThread`. mountingManager_->synchronouslyUpdateAnimatedProps(updates); } } +void FabricUIManagerBinding::recordSynchronousTransform( + jint reactTag, + NativeMap* transformProps) { + if (scheduler_ == nullptr || transformProps == nullptr) { + return; + } + if (auto uiManager = scheduler_->getUIManager()) { + uiManager->recordSynchronousTransform(reactTag, transformProps->consume()); + } +} + void FabricUIManagerBinding::schedulerDidUpdateShadowTree( const std::unordered_map& /*tagToProps*/) { // no-op @@ -925,6 +939,9 @@ void FabricUIManagerBinding::registerNatives() { "setPixelDensity", FabricUIManagerBinding::setPixelDensity), makeNativeMethod( "driveCxxAnimations", FabricUIManagerBinding::driveCxxAnimations), + makeNativeMethod( + "recordSynchronousTransform", + FabricUIManagerBinding::recordSynchronousTransform), makeNativeMethod( "driveAnimationBackend", FabricUIManagerBinding::driveAnimationBackend), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h index 68c47706ba7b..82946e5a3b74 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.h @@ -70,6 +70,8 @@ class FabricUIManagerBinding : public jni::HybridClass, void startSurface(jint surfaceId, jni::alias_ref moduleName, NativeMap *initialProps); + void recordSynchronousTransform(jint reactTag, NativeMap *transformProps); + void startSurfaceWithConstraints( jint surfaceId, jni::alias_ref moduleName, diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp index 23fd0978a766..821f9232f88e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<944781e7a547e601f0637869a3a3ec5e>> + * @generated SignedSource<<7050a2d215fac87f2fc218fc2d9b4252>> */ /** @@ -351,6 +351,12 @@ class ReactNativeFeatureFlagsJavaProvider return method(javaProvider_); } + bool enableSynchronousTransformOverlay() override { + static const auto method = + getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableSynchronousTransformOverlay"); + return method(javaProvider_); + } + bool enableViewCulling() override { static const auto method = getReactNativeFeatureFlagsProviderJavaClass()->getMethod("enableViewCulling"); @@ -885,6 +891,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableSwiftUIBasedFilters( return ReactNativeFeatureFlags::enableSwiftUIBasedFilters(); } +bool JReactNativeFeatureFlagsCxxInterop::enableSynchronousTransformOverlay( + facebook::jni::alias_ref /*unused*/) { + return ReactNativeFeatureFlags::enableSynchronousTransformOverlay(); +} + bool JReactNativeFeatureFlagsCxxInterop::enableViewCulling( facebook::jni::alias_ref /*unused*/) { return ReactNativeFeatureFlags::enableViewCulling(); @@ -1297,6 +1308,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() { makeNativeMethod( "enableSwiftUIBasedFilters", JReactNativeFeatureFlagsCxxInterop::enableSwiftUIBasedFilters), + makeNativeMethod( + "enableSynchronousTransformOverlay", + JReactNativeFeatureFlagsCxxInterop::enableSynchronousTransformOverlay), makeNativeMethod( "enableViewCulling", JReactNativeFeatureFlagsCxxInterop::enableViewCulling), diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h index ae774f80c82b..b82bda1d187e 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h +++ b/packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<> */ /** @@ -186,6 +186,9 @@ class JReactNativeFeatureFlagsCxxInterop static bool enableSwiftUIBasedFilters( facebook::jni::alias_ref); + static bool enableSynchronousTransformOverlay( + facebook::jni::alias_ref); + static bool enableViewCulling( facebook::jni::alias_ref); diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp index ac8ddb96a112..7019ccaf4a8e 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<56e127471f63e47798ec619e6fa56738>> + * @generated SignedSource<> */ /** @@ -234,6 +234,10 @@ bool ReactNativeFeatureFlags::enableSwiftUIBasedFilters() { return getAccessor().enableSwiftUIBasedFilters(); } +bool ReactNativeFeatureFlags::enableSynchronousTransformOverlay() { + return getAccessor().enableSynchronousTransformOverlay(); +} + bool ReactNativeFeatureFlags::enableViewCulling() { return getAccessor().enableViewCulling(); } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h index 0e5b3f2013fb..16248dbfcb72 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<6887b671526bc8a3b0678e3e45b4e3b4>> + * @generated SignedSource<<1692d5c36eb41b2f8ccb842cd5986560>> */ /** @@ -301,6 +301,11 @@ class ReactNativeFeatureFlags { */ RN_EXPORT static bool enableSwiftUIBasedFilters(); + /** + * When enabled, transforms applied to views synchronously on the UI thread (e.g. by native-driven animations, without a ShadowTree commit) are recorded in UIManager and taken into account by layout queries (measure, measureInWindow, getBoundingClientRect), so that they match the mounted views. + */ + RN_EXPORT static bool enableSynchronousTransformOverlay(); + /** * Enables View Culling: as soon as a view goes off screen, it can be reused anywhere in the UI and pieced together with other items to create new UI elements. */ diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp index 7640cea6418a..4adaa04cfe38 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<5d9712bd7e62ba3a29b084ba4c60bec5>> + * @generated SignedSource<<0733a96b8928b4a9c304a65f58c0b412>> */ /** @@ -965,6 +965,24 @@ bool ReactNativeFeatureFlagsAccessor::enableSwiftUIBasedFilters() { return flagValue.value(); } +bool ReactNativeFeatureFlagsAccessor::enableSynchronousTransformOverlay() { + auto flagValue = enableSynchronousTransformOverlay_.load(); + + if (!flagValue.has_value()) { + // This block is not exclusive but it is not necessary. + // If multiple threads try to initialize the feature flag, we would only + // be accessing the provider multiple times but the end state of this + // instance and the returned flag value would be the same. + + markFlagAsAccessed(52, "enableSynchronousTransformOverlay"); + + flagValue = currentProvider_->enableSynchronousTransformOverlay(); + enableSynchronousTransformOverlay_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { auto flagValue = enableViewCulling_.load(); @@ -974,7 +992,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(52, "enableViewCulling"); + markFlagAsAccessed(53, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -992,7 +1010,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecycling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(53, "enableViewRecycling"); + markFlagAsAccessed(54, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -1010,7 +1028,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForImage() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(54, "enableViewRecyclingForImage"); + markFlagAsAccessed(55, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -1028,7 +1046,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForScrollView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(55, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(56, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -1046,7 +1064,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForText() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(56, "enableViewRecyclingForText"); + markFlagAsAccessed(57, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -1064,7 +1082,7 @@ bool ReactNativeFeatureFlagsAccessor::enableViewRecyclingForView() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(57, "enableViewRecyclingForView"); + markFlagAsAccessed(58, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -1082,7 +1100,7 @@ bool ReactNativeFeatureFlagsAccessor::enableVirtualViewContainerStateExperimenta // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(58, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(59, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -1100,7 +1118,7 @@ bool ReactNativeFeatureFlagsAccessor::fixBorderlessRippleAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(59, "fixBorderlessRippleAndroid"); + markFlagAsAccessed(60, "fixBorderlessRippleAndroid"); flagValue = currentProvider_->fixBorderlessRippleAndroid(); fixBorderlessRippleAndroid_ = flagValue; @@ -1118,7 +1136,7 @@ bool ReactNativeFeatureFlagsAccessor::fixMappingOfEventPrioritiesBetweenFabricAn // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(60, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(61, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1136,7 +1154,7 @@ bool ReactNativeFeatureFlagsAccessor::fixTextLayoutOnPixelDensityChange() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(61, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(62, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1154,7 +1172,7 @@ bool ReactNativeFeatureFlagsAccessor::fixYogaFlexBasisFitContentInMainAxis() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(62, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(63, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1172,7 +1190,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxAssertSingleHostState() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(63, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(64, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1190,7 +1208,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxEnabledRelease() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(64, "fuseboxEnabledRelease"); + markFlagAsAccessed(65, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1208,7 +1226,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxFrameRecordingEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(65, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(66, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1226,7 +1244,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxScreenshotCaptureEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(66, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(67, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1244,7 +1262,7 @@ bool ReactNativeFeatureFlagsAccessor::fuseboxWebSocketEventsEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(67, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(68, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = flagValue; @@ -1262,7 +1280,7 @@ bool ReactNativeFeatureFlagsAccessor::keepTimersRunningWhileHostPausedAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(68, "keepTimersRunningWhileHostPausedAndroid"); + markFlagAsAccessed(69, "keepTimersRunningWhileHostPausedAndroid"); flagValue = currentProvider_->keepTimersRunningWhileHostPausedAndroid(); keepTimersRunningWhileHostPausedAndroid_ = flagValue; @@ -1280,7 +1298,7 @@ bool ReactNativeFeatureFlagsAccessor::optimizedAnimatedPropUpdates() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(69, "optimizedAnimatedPropUpdates"); + markFlagAsAccessed(70, "optimizedAnimatedPropUpdates"); flagValue = currentProvider_->optimizedAnimatedPropUpdates(); optimizedAnimatedPropUpdates_ = flagValue; @@ -1298,7 +1316,7 @@ bool ReactNativeFeatureFlagsAccessor::overrideBySynchronousMountPropsAtMountingA // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(70, "overrideBySynchronousMountPropsAtMountingAndroid"); + markFlagAsAccessed(71, "overrideBySynchronousMountPropsAtMountingAndroid"); flagValue = currentProvider_->overrideBySynchronousMountPropsAtMountingAndroid(); overrideBySynchronousMountPropsAtMountingAndroid_ = flagValue; @@ -1316,7 +1334,7 @@ bool ReactNativeFeatureFlagsAccessor::perfIssuesEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(71, "perfIssuesEnabled"); + markFlagAsAccessed(72, "perfIssuesEnabled"); flagValue = currentProvider_->perfIssuesEnabled(); perfIssuesEnabled_ = flagValue; @@ -1334,7 +1352,7 @@ bool ReactNativeFeatureFlagsAccessor::perfMonitorV2Enabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(72, "perfMonitorV2Enabled"); + markFlagAsAccessed(73, "perfMonitorV2Enabled"); flagValue = currentProvider_->perfMonitorV2Enabled(); perfMonitorV2Enabled_ = flagValue; @@ -1352,7 +1370,7 @@ double ReactNativeFeatureFlagsAccessor::preparedTextCacheSize() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(73, "preparedTextCacheSize"); + markFlagAsAccessed(74, "preparedTextCacheSize"); flagValue = currentProvider_->preparedTextCacheSize(); preparedTextCacheSize_ = flagValue; @@ -1370,7 +1388,7 @@ bool ReactNativeFeatureFlagsAccessor::preventShadowTreeCommitExhaustion() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(74, "preventShadowTreeCommitExhaustion"); + markFlagAsAccessed(75, "preventShadowTreeCommitExhaustion"); flagValue = currentProvider_->preventShadowTreeCommitExhaustion(); preventShadowTreeCommitExhaustion_ = flagValue; @@ -1388,7 +1406,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2Android() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(75, "redBoxV2Android"); + markFlagAsAccessed(76, "redBoxV2Android"); flagValue = currentProvider_->redBoxV2Android(); redBoxV2Android_ = flagValue; @@ -1406,7 +1424,7 @@ bool ReactNativeFeatureFlagsAccessor::redBoxV2IOS() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(76, "redBoxV2IOS"); + markFlagAsAccessed(77, "redBoxV2IOS"); flagValue = currentProvider_->redBoxV2IOS(); redBoxV2IOS_ = flagValue; @@ -1424,7 +1442,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldPressibilityUseW3CPointerEventsForHo // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(77, "shouldPressibilityUseW3CPointerEventsForHover"); + markFlagAsAccessed(78, "shouldPressibilityUseW3CPointerEventsForHover"); flagValue = currentProvider_->shouldPressibilityUseW3CPointerEventsForHover(); shouldPressibilityUseW3CPointerEventsForHover_ = flagValue; @@ -1442,7 +1460,7 @@ bool ReactNativeFeatureFlagsAccessor::shouldTriggerResponderTransferOnScrollAndr // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(78, "shouldTriggerResponderTransferOnScrollAndroid"); + markFlagAsAccessed(79, "shouldTriggerResponderTransferOnScrollAndroid"); flagValue = currentProvider_->shouldTriggerResponderTransferOnScrollAndroid(); shouldTriggerResponderTransferOnScrollAndroid_ = flagValue; @@ -1460,7 +1478,7 @@ bool ReactNativeFeatureFlagsAccessor::skipActivityIdentityAssertionOnHostPause() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(79, "skipActivityIdentityAssertionOnHostPause"); + markFlagAsAccessed(80, "skipActivityIdentityAssertionOnHostPause"); flagValue = currentProvider_->skipActivityIdentityAssertionOnHostPause(); skipActivityIdentityAssertionOnHostPause_ = flagValue; @@ -1478,7 +1496,7 @@ bool ReactNativeFeatureFlagsAccessor::skipUnchangedChildrenInDifferentiator() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(80, "skipUnchangedChildrenInDifferentiator"); + markFlagAsAccessed(81, "skipUnchangedChildrenInDifferentiator"); flagValue = currentProvider_->skipUnchangedChildrenInDifferentiator(); skipUnchangedChildrenInDifferentiator_ = flagValue; @@ -1496,7 +1514,7 @@ bool ReactNativeFeatureFlagsAccessor::syncAndroidClipBoundsWithOverflow() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(81, "syncAndroidClipBoundsWithOverflow"); + markFlagAsAccessed(82, "syncAndroidClipBoundsWithOverflow"); flagValue = currentProvider_->syncAndroidClipBoundsWithOverflow(); syncAndroidClipBoundsWithOverflow_ = flagValue; @@ -1514,7 +1532,7 @@ bool ReactNativeFeatureFlagsAccessor::traceTurboModulePromiseRejectionsOnAndroid // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(82, "traceTurboModulePromiseRejectionsOnAndroid"); + markFlagAsAccessed(83, "traceTurboModulePromiseRejectionsOnAndroid"); flagValue = currentProvider_->traceTurboModulePromiseRejectionsOnAndroid(); traceTurboModulePromiseRejectionsOnAndroid_ = flagValue; @@ -1532,7 +1550,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommit( // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(83, "updateRuntimeShadowNodeReferencesOnCommit"); + markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommit"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommit(); updateRuntimeShadowNodeReferencesOnCommit_ = flagValue; @@ -1550,7 +1568,7 @@ bool ReactNativeFeatureFlagsAccessor::updateRuntimeShadowNodeReferencesOnCommitT // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(84, "updateRuntimeShadowNodeReferencesOnCommitThread"); + markFlagAsAccessed(85, "updateRuntimeShadowNodeReferencesOnCommitThread"); flagValue = currentProvider_->updateRuntimeShadowNodeReferencesOnCommitThread(); updateRuntimeShadowNodeReferencesOnCommitThread_ = flagValue; @@ -1568,7 +1586,7 @@ bool ReactNativeFeatureFlagsAccessor::useAlwaysAvailableJSErrorHandling() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(85, "useAlwaysAvailableJSErrorHandling"); + markFlagAsAccessed(86, "useAlwaysAvailableJSErrorHandling"); flagValue = currentProvider_->useAlwaysAvailableJSErrorHandling(); useAlwaysAvailableJSErrorHandling_ = flagValue; @@ -1586,7 +1604,7 @@ bool ReactNativeFeatureFlagsAccessor::useFabricInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(86, "useFabricInterop"); + markFlagAsAccessed(87, "useFabricInterop"); flagValue = currentProvider_->useFabricInterop(); useFabricInterop_ = flagValue; @@ -1604,7 +1622,7 @@ bool ReactNativeFeatureFlagsAccessor::useNativeViewConfigsInBridgelessMode() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(87, "useNativeViewConfigsInBridgelessMode"); + markFlagAsAccessed(88, "useNativeViewConfigsInBridgelessMode"); flagValue = currentProvider_->useNativeViewConfigsInBridgelessMode(); useNativeViewConfigsInBridgelessMode_ = flagValue; @@ -1622,7 +1640,7 @@ bool ReactNativeFeatureFlagsAccessor::useNestedScrollViewAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(88, "useNestedScrollViewAndroid"); + markFlagAsAccessed(89, "useNestedScrollViewAndroid"); flagValue = currentProvider_->useNestedScrollViewAndroid(); useNestedScrollViewAndroid_ = flagValue; @@ -1640,7 +1658,7 @@ bool ReactNativeFeatureFlagsAccessor::useSharedAnimatedBackend() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(89, "useSharedAnimatedBackend"); + markFlagAsAccessed(90, "useSharedAnimatedBackend"); flagValue = currentProvider_->useSharedAnimatedBackend(); useSharedAnimatedBackend_ = flagValue; @@ -1658,7 +1676,7 @@ bool ReactNativeFeatureFlagsAccessor::useTraitHiddenOnAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(90, "useTraitHiddenOnAndroid"); + markFlagAsAccessed(91, "useTraitHiddenOnAndroid"); flagValue = currentProvider_->useTraitHiddenOnAndroid(); useTraitHiddenOnAndroid_ = flagValue; @@ -1676,7 +1694,7 @@ bool ReactNativeFeatureFlagsAccessor::useTurboModuleInterop() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(91, "useTurboModuleInterop"); + markFlagAsAccessed(92, "useTurboModuleInterop"); flagValue = currentProvider_->useTurboModuleInterop(); useTurboModuleInterop_ = flagValue; @@ -1694,7 +1712,7 @@ bool ReactNativeFeatureFlagsAccessor::useWindowDensityForDisplayMetricsAndroid() // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(92, "useWindowDensityForDisplayMetricsAndroid"); + markFlagAsAccessed(93, "useWindowDensityForDisplayMetricsAndroid"); flagValue = currentProvider_->useWindowDensityForDisplayMetricsAndroid(); useWindowDensityForDisplayMetricsAndroid_ = flagValue; @@ -1712,7 +1730,7 @@ double ReactNativeFeatureFlagsAccessor::viewCullingOutsetRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(93, "viewCullingOutsetRatio"); + markFlagAsAccessed(94, "viewCullingOutsetRatio"); flagValue = currentProvider_->viewCullingOutsetRatio(); viewCullingOutsetRatio_ = flagValue; @@ -1730,7 +1748,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionEnabled() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(94, "viewTransitionEnabled"); + markFlagAsAccessed(95, "viewTransitionEnabled"); flagValue = currentProvider_->viewTransitionEnabled(); viewTransitionEnabled_ = flagValue; @@ -1748,7 +1766,7 @@ bool ReactNativeFeatureFlagsAccessor::viewTransitionUseHardwareBitmapAndroid() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(95, "viewTransitionUseHardwareBitmapAndroid"); + markFlagAsAccessed(96, "viewTransitionUseHardwareBitmapAndroid"); flagValue = currentProvider_->viewTransitionUseHardwareBitmapAndroid(); viewTransitionUseHardwareBitmapAndroid_ = flagValue; @@ -1766,7 +1784,7 @@ double ReactNativeFeatureFlagsAccessor::virtualViewPrerenderRatio() { // be accessing the provider multiple times but the end state of this // instance and the returned flag value would be the same. - markFlagAsAccessed(96, "virtualViewPrerenderRatio"); + markFlagAsAccessed(97, "virtualViewPrerenderRatio"); flagValue = currentProvider_->virtualViewPrerenderRatio(); virtualViewPrerenderRatio_ = flagValue; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h index 7a9bc59bc57e..d3df1b167367 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsAccessor.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<17f41335dce7e5b45e7af2878c47d69b>> + * @generated SignedSource<<685e46e638a7f907d6f8ba8406aa4711>> */ /** @@ -86,6 +86,7 @@ class ReactNativeFeatureFlagsAccessor { bool enablePropsUpdateReconciliationAndroid(); bool enableResizeObserverByDefault(); bool enableSwiftUIBasedFilters(); + bool enableSynchronousTransformOverlay(); bool enableViewCulling(); bool enableViewRecycling(); bool enableViewRecyclingForImage(); @@ -142,7 +143,7 @@ class ReactNativeFeatureFlagsAccessor { std::unique_ptr currentProvider_; bool wasOverridden_; - std::array, 97> accessedFeatureFlags_; + std::array, 98> accessedFeatureFlags_; std::atomic> commonTestFlag_; std::atomic> cdpInteractionMetricsEnabled_; @@ -196,6 +197,7 @@ class ReactNativeFeatureFlagsAccessor { std::atomic> enablePropsUpdateReconciliationAndroid_; std::atomic> enableResizeObserverByDefault_; std::atomic> enableSwiftUIBasedFilters_; + std::atomic> enableSynchronousTransformOverlay_; std::atomic> enableViewCulling_; std::atomic> enableViewRecycling_; std::atomic> enableViewRecyclingForImage_; diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h index 600aab1882cc..240b1e769ce3 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDefaults.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<35e8175277baa31d135d802ef5a5e6f1>> + * @generated SignedSource<> */ /** @@ -237,6 +237,10 @@ class ReactNativeFeatureFlagsDefaults : public ReactNativeFeatureFlagsProvider { return false; } + bool enableSynchronousTransformOverlay() override { + return false; + } + bool enableViewCulling() override { return false; } diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h index 88878807cfec..ef4f826b8fdd 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsDynamicProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<64de692f46ae9186eec63a73cd2e45e7>> */ /** @@ -515,6 +515,15 @@ class ReactNativeFeatureFlagsDynamicProvider : public ReactNativeFeatureFlagsDef return ReactNativeFeatureFlagsDefaults::enableSwiftUIBasedFilters(); } + bool enableSynchronousTransformOverlay() override { + auto value = values_["enableSynchronousTransformOverlay"]; + if (!value.isNull()) { + return value.getBool(); + } + + return ReactNativeFeatureFlagsDefaults::enableSynchronousTransformOverlay(); + } + bool enableViewCulling() override { auto value = values_["enableViewCulling"]; if (!value.isNull()) { diff --git a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h index 664922287009..0773ba38b146 100644 --- a/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h +++ b/packages/react-native/ReactCommon/react/featureflags/ReactNativeFeatureFlagsProvider.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<7413e958441d0502dc1266ff87e9da85>> + * @generated SignedSource<> */ /** @@ -79,6 +79,7 @@ class ReactNativeFeatureFlagsProvider { virtual bool enablePropsUpdateReconciliationAndroid() = 0; virtual bool enableResizeObserverByDefault() = 0; virtual bool enableSwiftUIBasedFilters() = 0; + virtual bool enableSynchronousTransformOverlay() = 0; virtual bool enableViewCulling() = 0; virtual bool enableViewRecycling() = 0; virtual bool enableViewRecyclingForImage() = 0; diff --git a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp index d080c1cd892e..9d8924a9ef4b 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/dom/NativeDOM.cpp @@ -276,7 +276,10 @@ NativeDOM::getBoundingClientRect( } auto domRect = dom::getBoundingClientRect( - currentRevision, *shadowNode, includeTransform); + currentRevision, + *shadowNode, + includeTransform, + getUIManagerFromRuntime(rt).getTransformOverrideProvider()); return std::tuple{domRect.x, domRect.y, domRect.width, domRect.height}; } @@ -421,7 +424,10 @@ void NativeDOM::measure( return; } - auto measureRect = dom::measure(currentRevision, *shadowNode); + auto measureRect = dom::measure( + currentRevision, + *shadowNode, + getUIManagerFromRuntime(rt).getTransformOverrideProvider()); callback( measureRect.x, @@ -443,7 +449,10 @@ void NativeDOM::measureInWindow( return; } - auto rect = dom::measureInWindow(currentRevision, *shadowNode); + auto rect = dom::measureInWindow( + currentRevision, + *shadowNode, + getUIManagerFromRuntime(rt).getTransformOverrideProvider()); callback(rect.x, rect.y, rect.width, rect.height); } diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp index ca8a80f1adbf..72e4d4b9eec4 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.cpp @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<> + * @generated SignedSource<<6c74bf6b31e8a05e781cc1574ee9c7e6>> */ /** @@ -304,6 +304,11 @@ bool NativeReactNativeFeatureFlags::enableSwiftUIBasedFilters( return ReactNativeFeatureFlags::enableSwiftUIBasedFilters(); } +bool NativeReactNativeFeatureFlags::enableSynchronousTransformOverlay( + jsi::Runtime& /*runtime*/) { + return ReactNativeFeatureFlags::enableSynchronousTransformOverlay(); +} + bool NativeReactNativeFeatureFlags::enableViewCulling( jsi::Runtime& /*runtime*/) { return ReactNativeFeatureFlags::enableViewCulling(); diff --git a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h index 7413766e285a..238fd030e500 100644 --- a/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h +++ b/packages/react-native/ReactCommon/react/nativemodule/featureflags/NativeReactNativeFeatureFlags.h @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<90f271ea47f91e6a84979209b7b3d816>> + * @generated SignedSource<> */ /** @@ -142,6 +142,8 @@ class NativeReactNativeFeatureFlags bool enableSwiftUIBasedFilters(jsi::Runtime& runtime); + bool enableSynchronousTransformOverlay(jsi::Runtime& runtime); + bool enableViewCulling(jsi::Runtime& runtime); bool enableViewRecycling(jsi::Runtime& runtime); diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp index bca9b7a82d01..1acfdb15d123 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp @@ -134,4 +134,21 @@ void AnimatedPropsRegistry::clearOnSurfaceStop(SurfaceId surfaceId) { surfaceContexts_.erase(surfaceId); } +std::vector AnimatedPropsRegistry::getTags(SurfaceId surfaceId) { + auto lock = std::lock_guard(mutex_); + auto tags = std::vector{}; + if (auto it = surfaceContexts_.find(surfaceId); + it != surfaceContexts_.end()) { + auto& surfaceContext = it->second; + tags.reserve(surfaceContext.map.size() + surfaceContext.pendingMap.size()); + for (const auto& [tag, snapshot] : surfaceContext.map) { + tags.push_back(tag); + } + for (const auto& [tag, snapshot] : surfaceContext.pendingMap) { + tags.push_back(tag); + } + } + return tags; +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h index 87444a9a5aa0..8eb3df6b5e52 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h @@ -18,6 +18,8 @@ #include "AnimatedProps.h" #include "AnimationMutation.h" +#include + namespace facebook::react { struct PropsSnapshot { @@ -45,6 +47,12 @@ class AnimatedPropsRegistry { void initializeSurface(SurfaceId surfaceId); void clear(SurfaceId surfaceId); void clearOnSurfaceStop(SurfaceId surfaceId); + + /* + * Returns the tags of the views that have (pending or applied) entries in + * the registry for the given surface. + */ + std::vector getTags(SurfaceId surfaceId); std::pair> &, SnapshotMap &> getMap(SurfaceId surfaceId); private: diff --git a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp index 0e3db03a1024..9ed3fa6464f2 100644 --- a/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp +++ b/packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp @@ -301,7 +301,8 @@ void AnimationBackend::requestAsyncFlushForSurfaces( } uiManager->getShadowTreeRegistry().visit( surfaceId, - [weakAnimatedPropsRegistry](const ShadowTree& shadowTree) { + [weakAnimatedPropsRegistry, + &uiManager](const ShadowTree& shadowTree) { auto result = shadowTree.commit( [weakAnimatedPropsRegistry]( const RootShadowNode& oldRootShadowNode) { @@ -322,6 +323,12 @@ void AnimationBackend::requestAsyncFlushForSurfaces( updateRuntimeShadowNodeReferencesOnCommitThread()) { if (auto animatedPropsRegistry = weakAnimatedPropsRegistry.lock()) { + // The committed tree now carries the final values, so + // the synchronous transforms are no longer needed. + for (const auto tag : animatedPropsRegistry->getTags( + shadowTree.getSurfaceId())) { + uiManager->removeSynchronousTransform(tag); + } animatedPropsRegistry->clear(shadowTree.getSurfaceId()); } } @@ -331,6 +338,11 @@ void AnimationBackend::requestAsyncFlushForSurfaces( } void AnimationBackend::clearRegistry(SurfaceId surfaceId) { + if (auto uiManager = uiManager_.lock()) { + for (const auto tag : animatedPropsRegistry_->getTags(surfaceId)) { + uiManager->removeSynchronousTransform(tag); + } + } animatedPropsRegistry_->clear(surfaceId); } diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp index 73e685becf39..242746e58a7e 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.cpp @@ -46,7 +46,8 @@ LayoutMetrics LayoutableShadowNode::computeLayoutMetricsFromRoot( return EmptyLayoutMetrics; } if (policy.includeTransform) { - layoutMetrics.frame = layoutMetrics.frame * rootNode.getTransform(); + layoutMetrics.frame = layoutMetrics.frame * + resolveTransform(rootNode, policy.transformOverrideProvider); } return layoutMetrics; } @@ -69,7 +70,8 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( return EmptyLayoutMetrics; } if (policy.includeTransform) { - layoutMetrics.frame = layoutMetrics.frame * ancestorNode.getTransform(); + layoutMetrics.frame = layoutMetrics.frame * + resolveTransform(ancestorNode, policy.transformOverrideProvider); } layoutMetrics.frame.origin = {.x = 0, .y = 0}; return layoutMetrics; @@ -165,11 +167,14 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( // Move frame to the coordinate space of the current node. resultFrame.origin += currentFrame.origin; + auto currentTransform = + resolveTransform(*currentShadowNode, policy.transformOverrideProvider); + if (shouldApplyTransformation) { // If a node has a transform, we need to use the center of that node as // the origin of the transform when transforming its children (which // affects the result of transforms like `scale` and `rotate`). - resultFrame = currentShadowNode->getTransform().applyWithCenter( + resultFrame = currentTransform.applyWithCenter( resultFrame, currentFrame.getCenter()); } @@ -184,8 +189,8 @@ LayoutMetrics LayoutableShadowNode::computeRelativeLayoutMetrics( if (policy.enableOverflowClipping && (i != size - 1 || policy.clipSpecifiedAncestor)) { auto overflowInset = currentShadowNode->getLayoutMetrics().overflowInset; - auto overflowRect = insetBy( - currentFrame * currentShadowNode->getTransform(), overflowInset); + auto overflowRect = + insetBy(currentFrame * currentTransform, overflowInset); resultFrame = Rect::intersect(resultFrame, overflowRect); if (resultFrame.size.width == 0 && resultFrame.size.height == 0) { return EmptyLayoutMetrics; @@ -216,6 +221,18 @@ Transform LayoutableShadowNode::getTransform() const { return Transform::Identity(); } +Transform LayoutableShadowNode::resolveTransform( + const LayoutableShadowNode& shadowNode, + const TransformOverrideProvider* transformOverrideProvider) { + if (transformOverrideProvider != nullptr) { + if (auto transformOverride = + transformOverrideProvider->getTransformOverride(shadowNode)) { + return *transformOverride; + } + } + return shadowNode.getTransform(); +} + Point LayoutableShadowNode::getContentOriginOffset( bool /*includeTransform*/) const { return {.x = 0, .y = 0}; diff --git a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h index 2fdf36483d89..a318ed86e8f2 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h +++ b/packages/react-native/ReactCommon/react/renderer/core/LayoutableShadowNode.h @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -39,6 +40,20 @@ class LayoutableShadowNode : public ShadowNode { LayoutableShadowNode(const ShadowNode &sourceShadowNode, const ShadowNodeFragment &fragment); + /* + * Supplies transforms that take precedence over `getTransform()` when + * computing layout metrics or hit-testing. The committed ShadowTree does not + * know about transforms that were applied to the mounted views directly + * (e.g. by native-driven animations, which skip the commit), so the layout + * queries would otherwise disagree with what is on the screen. A provider + * returns `std::nullopt` for nodes it has no override for. + */ + class TransformOverrideProvider { + public: + virtual ~TransformOverrideProvider() = default; + virtual std::optional getTransformOverride(const LayoutableShadowNode &shadowNode) const = 0; + }; + struct LayoutInspectingPolicy { bool includeTransform{true}; bool includeViewportOffset{false}; @@ -48,6 +63,9 @@ class LayoutableShadowNode : public ShadowNode { // the spec clips ancestors *until* the root, then intersects with the // rootMargin-expanded root intersection rectangle. bool clipSpecifiedAncestor{true}; + // Optional source of transforms that override `getTransform()` of the + // inspected nodes (see `TransformOverrideProvider`). Not owned. + const TransformOverrideProvider *transformOverrideProvider{nullptr}; }; using UnsharedList = std::vector; @@ -151,6 +169,14 @@ class LayoutableShadowNode : public ShadowNode { */ static std::shared_ptr findNodeAtPoint(const std::shared_ptr &node, Point point); + /* + * Returns the transform of `shadowNode`, taking `transformOverrideProvider` + * (optional) into account. + */ + static Transform resolveTransform( + const LayoutableShadowNode &shadowNode, + const TransformOverrideProvider *transformOverrideProvider); + /* * Clean or Dirty layout state: * Indicates whether all nodes (and possibly their subtrees) along the path diff --git a/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp b/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp index c974595b4fd2..77b5c6fbee1b 100644 --- a/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/core/tests/LayoutableShadowNodeTest.cpp @@ -1431,4 +1431,123 @@ TEST(LayoutableShadowNodeTest, inversedContentOriginOffset) { EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 130); } +namespace { + +/* + * Overrides the transform of a single node, like `SynchronousTransformOverlay` + * does for views that were transformed without a commit. + */ +class SingleNodeTransformOverrideProvider + : public LayoutableShadowNode::TransformOverrideProvider { + public: + SingleNodeTransformOverrideProvider(Tag tag, Transform transform) + : tag_(tag), transform_(std::move(transform)) {} + + std::optional getTransformOverride( + const LayoutableShadowNode& shadowNode) const override { + if (shadowNode.getTag() == tag_) { + return transform_; + } + return std::nullopt; + } + + private: + Tag tag_; + Transform transform_; +}; + +} // namespace + +/* + * The provided transform takes precedence over the committed `transform` prop + * of the node and of its ancestors, while the nodes without an override keep + * using their props. + */ +TEST(LayoutableShadowNodeTest, relativeLayoutMetricsWithTransformOverride) { + auto builder = simpleComponentBuilder(); + auto childShadowNode = std::shared_ptr{}; + auto grandChildShadowNode = std::shared_ptr{}; + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {.width=1000, .height=1000}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {.x=10, .y=20}; + layoutMetrics.frame.size = {.width=100, .height=200}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .props([] { + // The committed (stale) transform. + auto sharedProps = std::make_shared(); + sharedProps->transform = Transform::Translate(5, 5, 0); + return sharedProps; + }) + .reference(childShadowNode) + .children({ + Element() + .tag(3) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {.x=1, .y=2}; + layoutMetrics.frame.size = {.width=10, .height=10}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->transform = Transform::Translate(0, 100, 0); + return sharedProps; + }) + .reference(grandChildShadowNode) + }) + }); + // clang-format on + + auto parentShadowNode = builder.build(element); + + // Without a provider the committed transform is used. + auto relativeLayoutMetrics = + LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), *parentShadowNode, {}); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 15); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 25); + + auto provider = + SingleNodeTransformOverrideProvider{2, Transform::Translate(100, 0, 0)}; + + relativeLayoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), + *parentShadowNode, + {.transformOverrideProvider = &provider}); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 110); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 20); + EXPECT_EQ(relativeLayoutMetrics.frame.size.width, 100); + EXPECT_EQ(relativeLayoutMetrics.frame.size.height, 200); + + // The override of an ancestor applies to descendants, which keep their + // own committed transform. + relativeLayoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics( + grandChildShadowNode->getFamily(), + *parentShadowNode, + {.transformOverrideProvider = &provider}); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 111); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 122); + + // `includeTransform = false` ignores the override as well. + relativeLayoutMetrics = LayoutableShadowNode::computeRelativeLayoutMetrics( + childShadowNode->getFamily(), + *parentShadowNode, + {.includeTransform = false, .transformOverrideProvider = &provider}); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.x, 10); + EXPECT_EQ(relativeLayoutMetrics.frame.origin.y, 20); +} + } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp index 0cec80aaec44..685478382a12 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.cpp @@ -273,7 +273,9 @@ std::string getTextContent( DOMRect getBoundingClientRect( const RootShadowNode::Shared& currentRevision, const ShadowNode& shadowNode, - bool includeTransform) { + bool includeTransform, + const LayoutableShadowNode::TransformOverrideProvider* + transformOverrideProvider) { auto shadowNodeInCurrentRevision = getShadowNodeInRevision(currentRevision, shadowNode); if (shadowNodeInCurrentRevision == nullptr) { @@ -283,7 +285,9 @@ DOMRect getBoundingClientRect( auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, shadowNode, - {.includeTransform = includeTransform, .includeViewportOffset = true}); + {.includeTransform = includeTransform, + .includeViewportOffset = true, + .transformOverrideProvider = transformOverrideProvider}); if (layoutMetrics == EmptyLayoutMetrics) { return DOMRect{}; @@ -491,7 +495,9 @@ std::string getTagName(const ShadowNode& shadowNode) { RNMeasureRect measure( const RootShadowNode::Shared& currentRevision, - const ShadowNode& shadowNode) { + const ShadowNode& shadowNode, + const LayoutableShadowNode::TransformOverrideProvider* + transformOverrideProvider) { auto shadowNodeInCurrentRevision = getShadowNodeInRevision(currentRevision, shadowNode); if (shadowNodeInCurrentRevision == nullptr) { @@ -501,7 +507,9 @@ RNMeasureRect measure( auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, - {.includeTransform = true, .includeViewportOffset = false}); + {.includeTransform = true, + .includeViewportOffset = false, + .transformOverrideProvider = transformOverrideProvider}); if (layoutMetrics == EmptyLayoutMetrics) { return RNMeasureRect{}; @@ -526,7 +534,9 @@ RNMeasureRect measure( DOMRect measureInWindow( const RootShadowNode::Shared& currentRevision, - const ShadowNode& shadowNode) { + const ShadowNode& shadowNode, + const LayoutableShadowNode::TransformOverrideProvider* + transformOverrideProvider) { auto shadowNodeInCurrentRevision = getShadowNodeInRevision(currentRevision, shadowNode); if (shadowNodeInCurrentRevision == nullptr) { @@ -536,7 +546,9 @@ DOMRect measureInWindow( auto layoutMetrics = getLayoutMetricsFromRoot( *currentRevision, *shadowNodeInCurrentRevision, - {.includeTransform = true, .includeViewportOffset = true}); + {.includeTransform = true, + .includeViewportOffset = true, + .transformOverrideProvider = transformOverrideProvider}); if (layoutMetrics == EmptyLayoutMetrics) { return DOMRect{}; diff --git a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h index 1f87f39aa0f1..378c01d1fcff 100644 --- a/packages/react-native/ReactCommon/react/renderer/dom/DOM.h +++ b/packages/react-native/ReactCommon/react/renderer/dom/DOM.h @@ -83,10 +83,14 @@ uint_fast16_t compareDocumentPosition( std::string getTextContent(const RootShadowNode::Shared ¤tRevision, const ShadowNode &shadowNode); +// `transformOverrideProvider` (optional, not owned) supplies transforms that +// were applied to the mounted views without a commit, see +// `LayoutableShadowNode::TransformOverrideProvider`. DOMRect getBoundingClientRect( const RootShadowNode::Shared ¤tRevision, const ShadowNode &shadowNode, - bool includeTransform); + bool includeTransform, + const LayoutableShadowNode::TransformOverrideProvider *transformOverrideProvider = nullptr); DOMOffset getOffset(const RootShadowNode::Shared ¤tRevision, const ShadowNode &shadowNode); @@ -102,9 +106,15 @@ std::string getTagName(const ShadowNode &shadowNode); // Non-standard methods from React Native -RNMeasureRect measure(const RootShadowNode::Shared ¤tRevision, const ShadowNode &shadowNode); +RNMeasureRect measure( + const RootShadowNode::Shared ¤tRevision, + const ShadowNode &shadowNode, + const LayoutableShadowNode::TransformOverrideProvider *transformOverrideProvider = nullptr); -DOMRect measureInWindow(const RootShadowNode::Shared ¤tRevision, const ShadowNode &shadowNode); +DOMRect measureInWindow( + const RootShadowNode::Shared ¤tRevision, + const ShadowNode &shadowNode, + const LayoutableShadowNode::TransformOverrideProvider *transformOverrideProvider = nullptr); // This method returns an optional to signal to go through the error callback // instead of going through the success callback with an empty DOMRect. diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp new file mode 100644 index 000000000000..27ed7d17150e --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp @@ -0,0 +1,134 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include "SynchronousTransformOverlay.h" + +#include +#include +#include + +#include + +namespace facebook::react { + +void SynchronousTransformOverlay::update( + Tag tag, + const folly::dynamic& props, + const PropsParserContext& context) { + if (!props.isObject()) { + return; + } + + auto entry = Entry{}; + + if (auto* transform = props.get_ptr("transform"); transform != nullptr) { + auto value = Transform{}; + fromRawValue(context, RawValue(*transform), value); + entry.transform = std::move(value); + } + + if (auto* transformOrigin = props.get_ptr("transformOrigin"); + transformOrigin != nullptr) { + auto value = TransformOrigin{}; + fromRawValue(context, RawValue(*transformOrigin), value); + entry.transformOrigin = value; + } + + updateEntry(tag, std::move(entry)); +} + +void SynchronousTransformOverlay::updateEntry(Tag tag, Entry&& entry) { + if (!entry.transform.has_value() && !entry.transformOrigin.has_value()) { + return; + } + + std::unique_lock lock(mutex_); + auto& existing = entries_[tag]; + // A partial update (e.g. only `transformOrigin`) keeps the other half of + // the previous entry, mirroring how the mounted view merges props. + if (entry.transform.has_value()) { + existing.transform = std::move(entry.transform); + } + if (entry.transformOrigin.has_value()) { + existing.transformOrigin = entry.transformOrigin; + } + size_.store(entries_.size(), std::memory_order_relaxed); +} + +void SynchronousTransformOverlay::remove(Tag tag) { + std::unique_lock lock(mutex_); + entries_.erase(tag); + size_.store(entries_.size(), std::memory_order_relaxed); +} + +void SynchronousTransformOverlay::clear() { + std::unique_lock lock(mutex_); + entries_.clear(); + pruneThreshold_ = kInitialPruneThreshold; + size_.store(0, std::memory_order_relaxed); +} + +bool SynchronousTransformOverlay::isEmpty() const { + return size_.load(std::memory_order_relaxed) == 0; +} + +bool SynchronousTransformOverlay::needsPruning() const { + std::shared_lock lock(mutex_); + return entries_.size() >= pruneThreshold_; +} + +void SynchronousTransformOverlay::prune( + const std::function& isAlive) { + std::unique_lock lock(mutex_); + for (auto it = entries_.begin(); it != entries_.end();) { + if (isAlive(it->first)) { + ++it; + } else { + it = entries_.erase(it); + } + } + // Prune again only once the overlay doubled, so that a steady set of + // animated views does not trigger a tree walk on every commit. + pruneThreshold_ = std::max(kInitialPruneThreshold, entries_.size() * 2); + size_.store(entries_.size(), std::memory_order_relaxed); +} + +std::optional SynchronousTransformOverlay::getTransformOverride( + const LayoutableShadowNode& shadowNode) const { + if (isEmpty()) { + return std::nullopt; + } + + auto entry = Entry{}; + { + std::shared_lock lock(mutex_); + auto it = entries_.find(shadowNode.getTag()); + if (it == entries_.end()) { + return std::nullopt; + } + entry = it->second; + } + + // The overlay only holds what was written synchronously; the committed + // props fill in whatever half of the (transform, transformOrigin) pair was + // not overridden, which is also what the mounted view does. + const auto* viewProps = + dynamic_cast(shadowNode.getProps().get()); + + const auto& transform = entry.transform.has_value() ? *entry.transform + : viewProps != nullptr ? viewProps->transform + : Transform::Identity(); + const auto& transformOrigin = entry.transformOrigin.has_value() + ? *entry.transformOrigin + : viewProps != nullptr ? viewProps->transformOrigin + : TransformOrigin{}; + + return BaseViewProps::resolveTransform( + shadowNode.getLayoutMetrics().frame.size, transform, transformOrigin); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h new file mode 100644 index 000000000000..35e1bc469afb --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#pragma once + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +namespace facebook::react { + +/* + * Keeps track of `transform` (and `transformOrigin`) values that were applied + * to mounted views synchronously on the UI thread, bypassing the ShadowTree + * (see `UIManager::synchronouslyUpdateViewOnUIThread`). Native-driven + * animations use that path for non-layout props, so the committed ShadowTree + * lags behind what is on the screen until the animation system commits the + * final values. + * + * Layout queries (`measure`, `measureInWindow`, `getBoundingClientRect`, + * `findNodeAtPoint`, `UIManager::getRelativeLayoutMetrics`) consult the + * overlay through the `TransformOverrideProvider` interface, so they describe + * the mounted views rather than the stale committed props. Only `transform` + * is tracked because it is the only synchronously updated prop that affects + * geometry; layout props always go through a commit. + * + * An entry lives until the view is deleted (entries are pruned lazily, see + * `prune`) or until the system that wrote it stops managing the view and + * calls `remove`. Writers run on the UI thread, readers on any thread. + */ +class SynchronousTransformOverlay final : public LayoutableShadowNode::TransformOverrideProvider { + public: + /* + * Records the `transform` / `transformOrigin` entries of `props`, if any. + * Other props are ignored. Values are parsed exactly like the ShadowNode + * props would be, so the overlay matches what a commit would produce. + */ + void update(Tag tag, const folly::dynamic &props, const PropsParserContext &context); + + /* + * Drops the entry for `tag`, if any. Called by the systems that write + * synchronous props when they stop managing a view, and by `prune`. + */ + void remove(Tag tag); + + void clear(); + + /* + * Returns `true` when there are no entries. Cheap, lock-free; used to skip + * the lookups in the layout queries when nothing was written synchronously. + */ + bool isEmpty() const; + + /* + * Returns `true` when enough entries accumulated since the last `prune` to + * make it worth walking the shadow trees for deleted views. + */ + bool needsPruning() const; + + /* + * Removes the entries whose tags are not reported as alive by `isAlive`. + */ + void prune(const std::function &isAlive); + +#pragma mark - TransformOverrideProvider + + /* + * Returns the transform that the mounted view for `shadowNode` has, resolved + * against the node's layout metrics the same way + * `ConcreteViewShadowNode::getTransform` resolves committed props. Returns + * `std::nullopt` when there is no entry for the node. + */ + std::optional getTransformOverride(const LayoutableShadowNode &shadowNode) const override; + + private: + struct Entry { + std::optional transform; + std::optional transformOrigin; + }; + + void updateEntry(Tag tag, Entry &&entry); + + mutable std::shared_mutex mutex_; + std::unordered_map entries_; + std::atomic size_{0}; + size_t pruneThreshold_{kInitialPruneThreshold}; + + static constexpr size_t kInitialPruneThreshold = 64; +}; + +} // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 7eefe4606177..1b35d811bd70 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -25,6 +25,7 @@ #include +#include #include namespace { @@ -306,6 +307,9 @@ ShadowTree::Unique UIManager::stopSurface(SurfaceId surfaceId) const { if (leakChecker_) { leakChecker_->stopSurface(surfaceId); } + + // The views of the surface are gone; drop their synchronous transforms. + pruneSynchronousTransformOverlay(surfaceId, nullptr, /* force */ true); } if (ReactNativeFeatureFlags::useSharedAnimatedBackend()) { @@ -403,6 +407,10 @@ LayoutMetrics UIManager::getRelativeLayoutMetrics( return EmptyLayoutMetrics; } + if (policy.transformOverrideProvider == nullptr) { + policy.transformOverrideProvider = getTransformOverrideProvider(); + } + return LayoutableShadowNode::computeRelativeLayoutMetrics( shadowNode.getFamily(), *layoutableAncestorShadowNode, policy); } @@ -680,12 +688,17 @@ void UIManager::shadowTreeDidCommit( const noexcept { TraceSection s("UIManager::shadowTreeDidCommit"); - std::shared_lock lock(commitHookMutex_); + { + std::shared_lock lock(commitHookMutex_); - for (auto* commitHook : commitHooks_) { - commitHook->shadowTreeDidCommit( - shadowTree, rootShadowNode, affectedLayoutableNodes); + for (auto* commitHook : commitHooks_) { + commitHook->shadowTreeDidCommit( + shadowTree, rootShadowNode, affectedLayoutableNodes); + } } + + pruneSynchronousTransformOverlay( + shadowTree.getSurfaceId(), rootShadowNode, /* force */ false); } void UIManager::reportMount(SurfaceId surfaceId) const { @@ -776,6 +789,67 @@ void UIManager::synchronouslyUpdateAnimatedProps( } } +#pragma mark - Synchronous transform overlay + +void UIManager::recordSynchronousTransform(Tag tag, const folly::dynamic& props) + const { + if (!ReactNativeFeatureFlags::enableSynchronousTransformOverlay()) { + return; + } + // Transforms do not depend on the surface, so any surface id will do. + PropsParserContext propsParserContext{-1, *contextContainer_}; + synchronousTransformOverlay_.update(tag, props, propsParserContext); +} + +void UIManager::removeSynchronousTransform(Tag tag) const { + synchronousTransformOverlay_.remove(tag); +} + +const LayoutableShadowNode::TransformOverrideProvider* +UIManager::getTransformOverrideProvider() const { + if (!ReactNativeFeatureFlags::enableSynchronousTransformOverlay() || + synchronousTransformOverlay_.isEmpty()) { + return nullptr; + } + return &synchronousTransformOverlay_; +} + +void UIManager::pruneSynchronousTransformOverlay( + SurfaceId committedSurfaceId, + const RootShadowNode::Shared& committedRootShadowNode, + bool force) const { + if (synchronousTransformOverlay_.isEmpty() || + (!force && !synchronousTransformOverlay_.needsPruning())) { + return; + } + + TraceSection s("UIManager::pruneSynchronousTransformOverlay"); + + std::unordered_set aliveTags; + auto collectTags = [&aliveTags](auto&& self, const ShadowNode& node) -> void { + aliveTags.insert(node.getTag()); + for (const auto& child : node.getChildren()) { + self(self, *child); + } + }; + + if (committedRootShadowNode != nullptr) { + collectTags(collectTags, *committedRootShadowNode); + } + shadowTreeRegistry_.enumerate([&](const ShadowTree& shadowTree, + bool& /*stop*/) { + if (shadowTree.getSurfaceId() == committedSurfaceId) { + return; + } + if (auto rootShadowNode = shadowTree.getCurrentRevision().rootShadowNode) { + collectTags(collectTags, *rootShadowNode); + } + }); + + synchronousTransformOverlay_.prune( + [&aliveTags](Tag tag) { return aliveTags.contains(tag); }); +} + #pragma mark ContextContainer std::shared_ptr UIManager::getContextContainer() const { diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 541d57a0b258..40280f4b9fa6 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -91,6 +92,31 @@ class UIManager final : public ShadowTreeDelegate { void synchronouslyUpdateAnimatedProps(const std::unordered_map &updates); + /* + * Records `transform` / `transformOrigin` values that were applied to the + * mounted view with the given `tag` synchronously (without a commit), so + * that layout queries reflect them. Called by the platform mounting layer + * for every synchronous update, and available to libraries that write + * props synchronously through their own channels. No-op unless the + * `enableSynchronousTransformOverlay` feature flag is enabled. + */ + void recordSynchronousTransform(Tag tag, const folly::dynamic &props) const; + + /* + * Drops the recorded synchronous transform for `tag`. To be called by the + * system that wrote it when it stops managing the view (the committed + * props are authoritative again). + */ + void removeSynchronousTransform(Tag tag) const; + + /* + * Returns the provider that layout queries should use to take synchronous + * transforms into account, or `nullptr` when there is nothing to override + * (feature flag disabled or no synchronous transforms recorded). + * `getRelativeLayoutMetrics` uses it automatically. + */ + const LayoutableShadowNode::TransformOverrideProvider *getTransformOverrideProvider() const; + /* * Provides access to a UIManagerBinding. * The `callback` methods will not be called if the internal pointer to @@ -251,6 +277,17 @@ class UIManager final : public ShadowTreeDelegate { const ShadowNode &shadowNode, const std::shared_ptr &ancestorShadowNode) const; + /* + * Drops the synchronous transforms recorded for views that are no longer + * part of any shadow tree. `committedRootShadowNode` stands in for the + * current revision of the surface with `committedSurfaceId`, which is + * being committed when this is called from `shadowTreeDidCommit`. + */ + void pruneSynchronousTransformOverlay( + SurfaceId committedSurfaceId, + const RootShadowNode::Shared &committedRootShadowNode, + bool force) const; + SharedComponentDescriptorRegistry componentDescriptorRegistry_; UIManagerDelegate *delegate_{}; UIManagerAnimationDelegate *animationDelegate_{nullptr}; @@ -272,6 +309,8 @@ class UIManager final : public ShadowTreeDelegate { std::unique_ptr lazyShadowTreeRevisionConsistencyManager_; std::shared_ptr animationBackend_; + + mutable SynchronousTransformOverlay synchronousTransformOverlay_; }; } // namespace facebook::react diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp index e84624567aad..c7f10e525426 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManagerBinding.cpp @@ -673,7 +673,10 @@ jsi::Value UIManagerBinding::get( return jsi::Value::undefined(); } - auto measureRect = dom::measure(currentRevision, *shadowNode); + auto measureRect = dom::measure( + currentRevision, + *shadowNode, + uiManager->getTransformOverrideProvider()); callbackFunction.call( runtime, @@ -714,7 +717,10 @@ jsi::Value UIManagerBinding::get( return jsi::Value::undefined(); } - auto rect = dom::measureInWindow(currentRevision, *shadowNode); + auto rect = dom::measureInWindow( + currentRevision, + *shadowNode, + uiManager->getTransformOverrideProvider()); callbackFunction.call( runtime, {jsi::Value{runtime, rect.x}, @@ -853,7 +859,10 @@ jsi::Value UIManagerBinding::get( } auto domRect = dom::getBoundingClientRect( - currentRevision, *shadowNode, includeTransform); + currentRevision, + *shadowNode, + includeTransform, + uiManager->getTransformOverrideProvider()); return jsi::Array::createWithElements( runtime, @@ -940,7 +949,10 @@ jsi::Value UIManagerBinding::get( } auto domRect = dom::getBoundingClientRect( - currentRevision, *shadowNode, true /* includeTransform */); + currentRevision, + *shadowNode, + true /* includeTransform */, + uiManager->getTransformOverrideProvider()); auto result = jsi::Object(runtime); result.setProperty(runtime, "x", domRect.x); diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md b/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md new file mode 100644 index 000000000000..19a428f28b27 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md @@ -0,0 +1,77 @@ +# Synchronous Transform Overlay + +[🏠 Home](../../../../../../../__docs__/README.md) + +`SynchronousTransformOverlay` keeps the layout queries of the renderer +(`measure`, `measureInWindow`, `getBoundingClientRect`, +`UIManager::getRelativeLayoutMetrics`) in sync with views whose `transform` was +changed synchronously on the UI thread, without a ShadowTree commit. It is +enabled by the `enableSynchronousTransformOverlay` feature flag. + +## 🚀 Usage + +Nothing changes for application code: the layout APIs return the position of the +mounted view while it is moved by a native-driven animation (`Animated` with +`useNativeDriver`, the Animation Backend, sticky headers, third-party animation +libraries). The visible effect is that `Pressable` / `Touchable*`, which +validate a press against the rectangle they `measure()`, keep firing `onPress` +on animated views. + +Animation systems that write props through the platform entry points +(`FabricUIManager.synchronouslyUpdateViewOnUIThread` on Android, +`RCTSurfacePresenter`'s `schedulerDidSynchronouslyUpdateViewOnUIThread:props:` +on iOS, `UIManager::synchronouslyUpdateViewOnUIThread` in C++) are covered +automatically. Systems that bypass them call +`UIManager::recordSynchronousTransform(tag, props)` with the props they applied, +and `UIManager::removeSynchronousTransform(tag)` when they stop managing a view. + +## 📐 Design + +The synchronous path exists because committing a ShadowTree revision on every +animation frame is too expensive, so non-layout props go straight to the mounted +views and the tree only catches up when the animation settles. For `transform` +that leaves the geometry of the tree, which +`LayoutableShadowNode::computeRelativeLayoutMetrics` reads through +`getTransform()`, behind what native hit-testing sees on the screen. + +The overlay is a thread-safe map, owned by `UIManager`, from view tag to the +last `transform` / `transformOrigin` written synchronously, parsed with the same +conversions as ShadowNode props. It implements +`LayoutableShadowNode::TransformOverrideProvider`: when a provider is set in +`LayoutInspectingPolicy`, `computeRelativeLayoutMetrics` asks it for the +transform of every node it visits and falls back to `getTransform()`, so the +override composes with nested and committed ancestor transforms, +`transformOrigin` and percentages exactly like committed props would. +`UIManager::getTransformOverrideProvider()` returns `nullptr` unless the flag is +on and something was recorded, keeping the hot path unchanged otherwise. + +Only `transform` is tracked: layout props always go through a commit, and the +other synchronously written props do not affect geometry or hit-testing. + +An entry mirrors the mounted view, which keeps the latest synchronous value +across React commits (`tagToSynchronousMountProps` on Android, +`propKeysManagedByAnimated` on iOS). It lives until the writer removes it (the +Animation Backend does so after its `AnimationEndSync` commit) or the view is +deleted: once the overlay grows past a threshold, +`UIManager::shadowTreeDidCommit` walks the current revisions and drops the tags +that no longer exist; the threshold doubles after each prune so a steady set of +animated views does not trigger a walk on every commit. + +## 🔗 Relationship with other systems + +### Part of + +- UIManager (`react/renderer/uimanager`). + +### Used by this + +- `LayoutableShadowNode` (`TransformOverrideProvider`, + `LayoutInspectingPolicy`), `BaseViewProps::resolveTransform` and the view prop + conversions, the feature flags system. + +### Uses this + +- The DOM layout APIs (`react/renderer/dom`, `NativeDOM`, `UIManagerBinding`) + and `UIManager::getRelativeLayoutMetrics`; the platform mounting layers and + the [Animation Backend](../../animationbackend/__docs__/AnimationBackend.md), + which feed and clear it. diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp new file mode 100644 index 000000000000..4cc1664dc120 --- /dev/null +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp @@ -0,0 +1,185 @@ +/* + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +#include + +#include +#include +#include +#include +#include +#include +#include + +namespace facebook::react { + +class SynchronousTransformOverlayTest : public ::testing::Test { + protected: + SynchronousTransformOverlayTest() + : contextContainer_(std::make_shared()), + propsParserContext_(-1, *contextContainer_) { + auto builder = simpleComponentBuilder(); + // clang-format off + auto element = + Element() + .tag(1) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.size = {.width=1000, .height=1000}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .children({ + Element() + .tag(2) + .finalize([](ViewShadowNode &shadowNode){ + auto layoutMetrics = EmptyLayoutMetrics; + layoutMetrics.frame.origin = {.x=10, .y=20}; + layoutMetrics.frame.size = {.width=100, .height=200}; + shadowNode.setLayoutMetrics(layoutMetrics); + }) + .props([] { + auto sharedProps = std::make_shared(); + sharedProps->transform = Transform::Translate(5, 5, 0); + return sharedProps; + }) + .reference(childShadowNode_) + }); + // clang-format on + rootShadowNode_ = builder.build(element); + } + + std::shared_ptr contextContainer_; + PropsParserContext propsParserContext_; + std::shared_ptr rootShadowNode_; + std::shared_ptr childShadowNode_; + SynchronousTransformOverlay overlay_; +}; + +TEST_F(SynchronousTransformOverlayTest, isEmptyWithoutTransforms) { + EXPECT_TRUE(overlay_.isEmpty()); + EXPECT_FALSE(overlay_.getTransformOverride(*childShadowNode_).has_value()); + + // Props without a transform are ignored. + overlay_.update( + 2, folly::dynamic::object("opacity", 0.5), propsParserContext_); + EXPECT_TRUE(overlay_.isEmpty()); + EXPECT_FALSE(overlay_.getTransformOverride(*childShadowNode_).has_value()); +} + +TEST_F(SynchronousTransformOverlayTest, parsesTransformLikeProps) { + overlay_.update( + 2, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 100))), + propsParserContext_); + + EXPECT_FALSE(overlay_.isEmpty()); + + auto transform = overlay_.getTransformOverride(*childShadowNode_); + ASSERT_TRUE(transform.has_value()); + EXPECT_EQ(*transform, Transform::Translate(100, 0, 0)); + + // Nodes without an entry are not overridden. + EXPECT_FALSE(overlay_.getTransformOverride(*rootShadowNode_).has_value()); + + // The resolved transform is relative to the node's own frame: the + // percentage is resolved against the committed layout metrics, like + // `ConcreteViewShadowNode::getTransform` does. + overlay_.update( + 2, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateY", "50%"))), + propsParserContext_); + transform = overlay_.getTransformOverride(*childShadowNode_); + ASSERT_TRUE(transform.has_value()); + EXPECT_EQ(*transform, Transform::Translate(0, 100, 0)); +} + +TEST_F(SynchronousTransformOverlayTest, mergesTransformOriginUpdates) { + // The committed props have no transform origin; a synchronous update of + // the origin alone applies to the committed transform (5, 5). + overlay_.update( + 2, + folly::dynamic::object("transformOrigin", folly::dynamic::array(0, 0, 0)), + propsParserContext_); + auto transform = overlay_.getTransformOverride(*childShadowNode_); + ASSERT_TRUE(transform.has_value()); + EXPECT_EQ(*transform, Transform::Translate(5, 5, 0)); + + // A later transform update keeps the previously written origin. + overlay_.update( + 2, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("scale", 2))), + propsParserContext_); + transform = overlay_.getTransformOverride(*childShadowNode_); + ASSERT_TRUE(transform.has_value()); + // Scaling around the top-left corner (origin 0, 0) rather than the center. + auto expected = BaseViewProps::resolveTransform( + Size{.width = 100, .height = 200}, + Transform::Scale(2, 2, 1), + TransformOrigin{ + .xy = {ValueUnit(0, UnitType::Point), ValueUnit(0, UnitType::Point)}, + .z = 0}); + EXPECT_EQ(*transform, expected); +} + +TEST_F(SynchronousTransformOverlayTest, removesAndPrunesEntries) { + overlay_.update( + 2, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 100))), + propsParserContext_); + overlay_.update( + 3, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 100))), + propsParserContext_); + + overlay_.remove(3); + EXPECT_FALSE(overlay_.isEmpty()); + EXPECT_TRUE(overlay_.getTransformOverride(*childShadowNode_).has_value()); + + // Pruning only keeps the tags reported as alive. + overlay_.prune([](Tag tag) { return tag != 2; }); + EXPECT_TRUE(overlay_.isEmpty()); + EXPECT_FALSE(overlay_.getTransformOverride(*childShadowNode_).has_value()); + + overlay_.update( + 2, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 100))), + propsParserContext_); + overlay_.clear(); + EXPECT_TRUE(overlay_.isEmpty()); +} + +TEST_F(SynchronousTransformOverlayTest, needsPruningOnceEnoughEntriesExist) { + EXPECT_FALSE(overlay_.needsPruning()); + for (Tag tag = 1; tag <= 64; tag++) { + overlay_.update( + tag, + folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateX", 1))), + propsParserContext_); + } + EXPECT_TRUE(overlay_.needsPruning()); + + // Everything is still alive: the threshold doubles so that a steady set of + // animated views does not trigger a tree walk on every commit. + overlay_.prune([](Tag /*tag*/) { return true; }); + EXPECT_FALSE(overlay_.needsPruning()); +} + +} // namespace facebook::react diff --git a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp index 9ad1bb1a4f54..98bd5b61d88b 100644 --- a/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp +++ b/packages/react-native/ReactCxxPlatform/react/renderer/scheduler/SchedulerDelegateImpl.cpp @@ -68,6 +68,9 @@ void SchedulerDelegateImpl::schedulerDidSendAccessibilityEvent( void SchedulerDelegateImpl::schedulerShouldSynchronouslyUpdateViewOnUIThread( Tag tag, const folly::dynamic& props) { + if (uiManager_ != nullptr) { + uiManager_->recordSynchronousTransform(tag, props); + } mountingManager_->synchronouslyUpdateViewOnUIThread(tag, props); } diff --git a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js index 612c196ff22d..8a5b67405cd1 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -608,6 +608,17 @@ const definitions: FeatureFlagDefinitions = { }, ossReleaseStage: 'canary', }, + enableSynchronousTransformOverlay: { + defaultValue: false, + metadata: { + dateAdded: '2026-10-23', + description: + 'When enabled, transforms applied to views synchronously on the UI thread (e.g. by native-driven animations, without a ShadowTree commit) are recorded in UIManager and taken into account by layout queries (measure, measureInWindow, getBoundingClientRect), so that they match the mounted views.', + expectedReleaseValue: true, + purpose: 'experimentation', + }, + ossReleaseStage: 'none', + }, enableViewCulling: { defaultValue: false, metadata: { diff --git a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js index e5a8120cff62..5bedc63d5386 100644 --- a/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/ReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<674f975884ae746e067e614523f2bfc3>> + * @generated SignedSource<<6e51075f5084b75494efb1ff3cf6d239>> * @flow strict * @noformat */ @@ -101,6 +101,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enablePropsUpdateReconciliationAndroid: Getter, enableResizeObserverByDefault: Getter, enableSwiftUIBasedFilters: Getter, + enableSynchronousTransformOverlay: Getter, enableViewCulling: Getter, enableViewRecycling: Getter, enableViewRecyclingForImage: Getter, @@ -430,6 +431,10 @@ export const enableResizeObserverByDefault: Getter = createNativeFlagGe * When enabled, it will use SwiftUI for filter effects like blur on iOS. */ export const enableSwiftUIBasedFilters: Getter = createNativeFlagGetter('enableSwiftUIBasedFilters', false); +/** + * When enabled, transforms applied to views synchronously on the UI thread (e.g. by native-driven animations, without a ShadowTree commit) are recorded in UIManager and taken into account by layout queries (measure, measureInWindow, getBoundingClientRect), so that they match the mounted views. + */ +export const enableSynchronousTransformOverlay: Getter = createNativeFlagGetter('enableSynchronousTransformOverlay', false); /** * Enables View Culling: as soon as a view goes off screen, it can be reused anywhere in the UI and pieced together with other items to create new UI elements. */ diff --git a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js index 6fc752bba2a9..715574225ccd 100644 --- a/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js +++ b/packages/react-native/src/private/featureflags/specs/NativeReactNativeFeatureFlags.js @@ -4,7 +4,7 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @generated SignedSource<<2eb3f4a3dca013c51ad2913944855010>> + * @generated SignedSource<<150faa6f2a5f9f4aae0b0113cf3afc00>> * @flow strict * @noformat */ @@ -77,6 +77,7 @@ export interface Spec extends TurboModule { readonly enablePropsUpdateReconciliationAndroid?: () => boolean; readonly enableResizeObserverByDefault?: () => boolean; readonly enableSwiftUIBasedFilters?: () => boolean; + readonly enableSynchronousTransformOverlay?: () => boolean; readonly enableViewCulling?: () => boolean; readonly enableViewRecycling?: () => boolean; readonly enableViewRecyclingForImage?: () => boolean; diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api index 97a6a1f652cc..20113e00dddf 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidDebugCxx.api @@ -1714,6 +1714,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -1731,11 +1732,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 4de5fdc97f51..82819d6aa8ca 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -2592,6 +2592,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -2609,11 +2610,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LineMeasureCacheKey { @@ -3915,6 +3922,16 @@ class facebook::react::SwitchProps : public facebook::react::HostPlatformViewPro public virtual folly::dynamic getDiffProps(const facebook::react::Props* prevProps) const override; } +class facebook::react::SynchronousTransformOverlay : public facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public bool isEmpty() const; + public bool needsPruning() const; + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const override; + public void clear(); + public void prune(const std::function& isAlive); + public void remove(facebook::react::Tag tag); + public void update(facebook::react::Tag tag, const folly::dynamic& props, const facebook::react::PropsParserContext& context); +} + class facebook::react::TaskPriorityComparer { public bool operator()(const std::shared_ptr& lhs, const std::shared_ptr& rhs); } @@ -4184,6 +4201,7 @@ class facebook::react::UIConstantsProviderBinding : public facebook::jni::JavaCl class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public UIManager(const facebook::react::RuntimeExecutor& runtimeExecutor, std::shared_ptr contextContainer); + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* getTransformOverrideProvider() const; public const facebook::react::ShadowTreeRegistry& getShadowTreeRegistry() const; public facebook::react::LayoutMetrics getRelativeLayoutMetrics(const facebook::react::ShadowNode& shadowNode, const facebook::react::ShadowNode* ancestorShadowNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy) const; public facebook::react::ShadowTree::Unique stopSurface(facebook::react::SurfaceId surfaceId) const; @@ -4206,9 +4224,11 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void appendChild(const std::shared_ptr& parentShadowNode, const std::shared_ptr& childShadowNode) const; public void completeSurface(facebook::react::SurfaceId surfaceId, const std::shared_ptr>>& rootChildren, facebook::react::ShadowTree::CommitOptions commitOptions); public void dispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) const; + public void recordSynchronousTransform(facebook::react::Tag tag, const folly::dynamic& props) const; public void registerCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void registerMountHook(facebook::react::UIManagerMountHook& mountHook); public void removeEventListener(const std::shared_ptr& listener); + public void removeSynchronousTransform(facebook::react::Tag tag) const; public void reportMount(facebook::react::SurfaceId surfaceId) const; public void sendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType); public void setAnimationDelegate(facebook::react::UIManagerAnimationDelegate* delegate); @@ -8779,11 +8799,11 @@ bool facebook::react::dom::isConnected(const facebook::react::RootShadowNode::Sh facebook::react::dom::DOMBorderWidthRounded facebook::react::dom::getBorderWidth(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMOffset facebook::react::dom::getOffset(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMPoint facebook::react::dom::getScrollPosition(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); -facebook::react::dom::DOMRect facebook::react::dom::getBoundingClientRect(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, bool includeTransform); -facebook::react::dom::DOMRect facebook::react::dom::measureInWindow(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); +facebook::react::dom::DOMRect facebook::react::dom::getBoundingClientRect(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, bool includeTransform, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); +facebook::react::dom::DOMRect facebook::react::dom::measureInWindow(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); facebook::react::dom::DOMSizeRounded facebook::react::dom::getInnerSize(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMSizeRounded facebook::react::dom::getScrollSize(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); -facebook::react::dom::RNMeasureRect facebook::react::dom::measure(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); +facebook::react::dom::RNMeasureRect facebook::react::dom::measure(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); std::optional facebook::react::dom::measureLayout(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::ShadowNode& relativeToShadowNode); std::shared_ptr facebook::react::dom::getElementById(const std::shared_ptr& currentRevision, const std::string& id); std::shared_ptr facebook::react::dom::getParentNode(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api index 05dc03891252..e7c74d6b7b34 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidNewarchCxx.api @@ -1713,6 +1713,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -1730,11 +1731,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api index 05dc03891252..e7c74d6b7b34 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidReleaseCxx.api @@ -1713,6 +1713,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -1730,11 +1731,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api index e04f3b738889..e91ac0f2917b 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -3975,6 +3975,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -3992,11 +3993,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index cb8ca685d4f2..cae3b0bbe763 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -4785,6 +4785,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -4802,11 +4803,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LineMeasureCacheKey { @@ -6064,6 +6071,16 @@ class facebook::react::SwitchProps : public facebook::react::HostPlatformViewPro public facebook::react::SharedColor trackColorForTrue; } +class facebook::react::SynchronousTransformOverlay : public facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public bool isEmpty() const; + public bool needsPruning() const; + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const override; + public void clear(); + public void prune(const std::function& isAlive); + public void remove(facebook::react::Tag tag); + public void update(facebook::react::Tag tag, const folly::dynamic& props, const facebook::react::PropsParserContext& context); +} + class facebook::react::TaskPriorityComparer { public bool operator()(const std::shared_ptr& lhs, const std::shared_ptr& rhs); } @@ -6326,6 +6343,7 @@ class facebook::react::TurboModuleWithJSIBindings { class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public UIManager(const facebook::react::RuntimeExecutor& runtimeExecutor, std::shared_ptr contextContainer); + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* getTransformOverrideProvider() const; public const facebook::react::ShadowTreeRegistry& getShadowTreeRegistry() const; public facebook::react::LayoutMetrics getRelativeLayoutMetrics(const facebook::react::ShadowNode& shadowNode, const facebook::react::ShadowNode* ancestorShadowNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy) const; public facebook::react::ShadowTree::Unique stopSurface(facebook::react::SurfaceId surfaceId) const; @@ -6348,9 +6366,11 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void appendChild(const std::shared_ptr& parentShadowNode, const std::shared_ptr& childShadowNode) const; public void completeSurface(facebook::react::SurfaceId surfaceId, const std::shared_ptr>>& rootChildren, facebook::react::ShadowTree::CommitOptions commitOptions); public void dispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) const; + public void recordSynchronousTransform(facebook::react::Tag tag, const folly::dynamic& props) const; public void registerCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void registerMountHook(facebook::react::UIManagerMountHook& mountHook); public void removeEventListener(const std::shared_ptr& listener); + public void removeSynchronousTransform(facebook::react::Tag tag) const; public void reportMount(facebook::react::SurfaceId surfaceId) const; public void sendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType); public void setAnimationDelegate(facebook::react::UIManagerAnimationDelegate* delegate); @@ -10645,11 +10665,11 @@ bool facebook::react::dom::isConnected(const facebook::react::RootShadowNode::Sh facebook::react::dom::DOMBorderWidthRounded facebook::react::dom::getBorderWidth(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMOffset facebook::react::dom::getOffset(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMPoint facebook::react::dom::getScrollPosition(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); -facebook::react::dom::DOMRect facebook::react::dom::getBoundingClientRect(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, bool includeTransform); -facebook::react::dom::DOMRect facebook::react::dom::measureInWindow(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); +facebook::react::dom::DOMRect facebook::react::dom::getBoundingClientRect(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, bool includeTransform, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); +facebook::react::dom::DOMRect facebook::react::dom::measureInWindow(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); facebook::react::dom::DOMSizeRounded facebook::react::dom::getInnerSize(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMSizeRounded facebook::react::dom::getScrollSize(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); -facebook::react::dom::RNMeasureRect facebook::react::dom::measure(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); +facebook::react::dom::RNMeasureRect facebook::react::dom::measure(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); std::optional facebook::react::dom::measureLayout(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::ShadowNode& relativeToShadowNode); std::shared_ptr facebook::react::dom::getElementById(const std::shared_ptr& currentRevision, const std::string& id); std::shared_ptr facebook::react::dom::getParentNode(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api index 9010fcf23951..341e899be5e5 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3967,6 +3967,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -3984,11 +3985,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api index 6bb9397bd2e4..2cb4ae631b92 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3974,6 +3974,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual bool canBeTouchTarget() const; @@ -3991,11 +3992,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api index 507e71d72698..57d80be0a383 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonDebugCxx.api @@ -837,6 +837,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual Float baseline(const facebook::react::LayoutContext& layoutContext, facebook::react::Size size) const; @@ -854,11 +855,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index cb97eccc7f11..2507af48f665 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -1522,6 +1522,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual Float baseline(const facebook::react::LayoutContext& layoutContext, facebook::react::Size size) const; @@ -1539,11 +1540,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LineMeasureCacheKey { @@ -2451,6 +2458,16 @@ class facebook::react::SurfaceTelemetry { public void incorporate(const facebook::react::TransactionTelemetry& telemetry, int numberOfMutations); } +class facebook::react::SynchronousTransformOverlay : public facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public bool isEmpty() const; + public bool needsPruning() const; + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const override; + public void clear(); + public void prune(const std::function& isAlive); + public void remove(facebook::react::Tag tag); + public void update(facebook::react::Tag tag, const folly::dynamic& props, const facebook::react::PropsParserContext& context); +} + class facebook::react::TaskPriorityComparer { public bool operator()(const std::shared_ptr& lhs, const std::shared_ptr& rhs); } @@ -2661,6 +2678,7 @@ class facebook::react::TurboModuleWithJSIBindings { class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public UIManager(const facebook::react::RuntimeExecutor& runtimeExecutor, std::shared_ptr contextContainer); + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* getTransformOverrideProvider() const; public const facebook::react::ShadowTreeRegistry& getShadowTreeRegistry() const; public facebook::react::LayoutMetrics getRelativeLayoutMetrics(const facebook::react::ShadowNode& shadowNode, const facebook::react::ShadowNode* ancestorShadowNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy) const; public facebook::react::ShadowTree::Unique stopSurface(facebook::react::SurfaceId surfaceId) const; @@ -2683,9 +2701,11 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void appendChild(const std::shared_ptr& parentShadowNode, const std::shared_ptr& childShadowNode) const; public void completeSurface(facebook::react::SurfaceId surfaceId, const std::shared_ptr>>& rootChildren, facebook::react::ShadowTree::CommitOptions commitOptions); public void dispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) const; + public void recordSynchronousTransform(facebook::react::Tag tag, const folly::dynamic& props) const; public void registerCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void registerMountHook(facebook::react::UIManagerMountHook& mountHook); public void removeEventListener(const std::shared_ptr& listener); + public void removeSynchronousTransform(facebook::react::Tag tag) const; public void reportMount(facebook::react::SurfaceId surfaceId) const; public void sendAccessibilityEvent(const std::shared_ptr& shadowNode, const std::string& eventType); public void setAnimationDelegate(facebook::react::UIManagerAnimationDelegate* delegate); @@ -6002,11 +6022,11 @@ bool facebook::react::dom::isConnected(const facebook::react::RootShadowNode::Sh facebook::react::dom::DOMBorderWidthRounded facebook::react::dom::getBorderWidth(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMOffset facebook::react::dom::getOffset(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMPoint facebook::react::dom::getScrollPosition(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); -facebook::react::dom::DOMRect facebook::react::dom::getBoundingClientRect(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, bool includeTransform); -facebook::react::dom::DOMRect facebook::react::dom::measureInWindow(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); +facebook::react::dom::DOMRect facebook::react::dom::getBoundingClientRect(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, bool includeTransform, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); +facebook::react::dom::DOMRect facebook::react::dom::measureInWindow(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); facebook::react::dom::DOMSizeRounded facebook::react::dom::getInnerSize(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); facebook::react::dom::DOMSizeRounded facebook::react::dom::getScrollSize(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); -facebook::react::dom::RNMeasureRect facebook::react::dom::measure(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); +facebook::react::dom::RNMeasureRect facebook::react::dom::measure(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider = nullptr); std::optional facebook::react::dom::measureLayout(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode, const facebook::react::ShadowNode& relativeToShadowNode); std::shared_ptr facebook::react::dom::getElementById(const std::shared_ptr& currentRevision, const std::string& id); std::shared_ptr facebook::react::dom::getParentNode(const facebook::react::RootShadowNode::Shared& currentRevision, const facebook::react::ShadowNode& shadowNode); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api index 004abd9081df..0a9521922c94 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonNewarchCxx.api @@ -836,6 +836,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual Float baseline(const facebook::react::LayoutContext& layoutContext, facebook::react::Size size) const; @@ -853,11 +854,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { diff --git a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api index 004abd9081df..0a9521922c94 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonReleaseCxx.api @@ -836,6 +836,7 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public static facebook::react::LayoutMetrics computeLayoutMetricsFromRoot(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& rootNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNode::AncestorList& ancestors, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); public static facebook::react::LayoutMetrics computeRelativeLayoutMetrics(const facebook::react::ShadowNodeFamily& descendantNodeFamily, const facebook::react::LayoutableShadowNode& ancestorNode, facebook::react::LayoutableShadowNode::LayoutInspectingPolicy policy); + public static facebook::react::Transform resolveTransform(const facebook::react::LayoutableShadowNode& shadowNode, const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider); public static std::shared_ptr findNodeAtPoint(const std::shared_ptr& node, facebook::react::Point point); public using UnsharedList = std::vector; public virtual Float baseline(const facebook::react::LayoutContext& layoutContext, facebook::react::Size size) const; @@ -853,11 +854,17 @@ class facebook::react::LayoutableShadowNode : public facebook::react::ShadowNode public void setLayoutMetrics(facebook::react::LayoutMetrics layoutMetrics); } +class facebook::react::LayoutableShadowNode::TransformOverrideProvider { + public virtual std::optional getTransformOverride(const facebook::react::LayoutableShadowNode& shadowNode) const = 0; + public virtual ~TransformOverrideProvider() = default; +} + struct facebook::react::LayoutableShadowNode::LayoutInspectingPolicy { public bool clipSpecifiedAncestor; public bool enableOverflowClipping; public bool includeTransform; public bool includeViewportOffset; + public const facebook::react::LayoutableShadowNode::TransformOverrideProvider* transformOverrideProvider; } class facebook::react::LongLivedObject { From fca50f01a775cbabb4938eff70b122400a8276a4 Mon Sep 17 00:00:00 2001 From: Tomek Zawadzki Date: Wed, 28 Oct 2026 22:37:54 +0100 Subject: [PATCH 2/2] Record transforms of the batched AnimatedProps path in the synchronous transform overlay Co-Authored-By: Claude Opus 5.5 --- .../react/fabric/FabricUIManagerBinding.cpp | 14 +++++++--- .../uimanager/SynchronousTransformOverlay.cpp | 26 ++++++++++++++++++ .../uimanager/SynchronousTransformOverlay.h | 11 ++++++-- .../react/renderer/uimanager/UIManager.cpp | 9 +++++++ .../react/renderer/uimanager/UIManager.h | 1 + .../__docs__/SynchronousTransformOverlay.md | 5 ++-- .../tests/SynchronousTransformOverlayTest.cpp | 27 +++++++++++++++++++ .../ReactAndroidFrameworksCxx.api | 2 ++ .../api-snapshots/ReactAppleFrameworksCxx.api | 2 ++ .../ReactCommonFrameworksCxx.api | 2 ++ 10 files changed, 92 insertions(+), 7 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp index 7ed7f2754cdb..e914483be91c 100644 --- a/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp +++ b/packages/react-native/ReactAndroid/src/main/jni/react/fabric/FabricUIManagerBinding.cpp @@ -860,9 +860,17 @@ void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateViewOnUIThread( void FabricUIManagerBinding::schedulerShouldSynchronouslyUpdateAnimatedProps( const std::unordered_map& updates) { if (ReactNativeFeatureFlags::cxxNativeAnimatedEnabled() && mountingManager_) { - // TODO: record the transforms of this batched path (behind - // `optimizedAnimatedPropUpdates`) in the synchronous transform overlay; - // it bypasses `FabricUIManager.synchronouslyUpdateViewOnUIThread`. + // This batched path bypasses + // `FabricUIManager.synchronouslyUpdateViewOnUIThread`, which records the + // synchronous transforms of the per-view path. + if (ReactNativeFeatureFlags::enableSynchronousTransformOverlay() && + scheduler_ != nullptr) { + if (auto uiManager = scheduler_->getUIManager()) { + for (const auto& [tag, props] : updates) { + uiManager->recordSynchronousTransform(tag, props); + } + } + } mountingManager_->synchronouslyUpdateAnimatedProps(updates); } } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp index 27ed7d17150e..40563315fe57 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.cpp @@ -41,6 +41,32 @@ void SynchronousTransformOverlay::update( updateEntry(tag, std::move(entry)); } +void SynchronousTransformOverlay::update( + Tag tag, + const AnimatedProps& props, + const PropsParserContext& context) { + if (props.rawProps != nullptr) { + // `rawProps` carries the props the animation system could not type, + // which may include `transform` (e.g. matrices or percentages). + update(tag, static_cast(*props.rawProps), context); + } + + auto entry = Entry{}; + for (const auto& prop : props.props) { + switch (prop->propName) { + case TRANSFORM: + entry.transform = get(*prop); + break; + case TRANSFORM_ORIGIN: + entry.transformOrigin = get(*prop); + break; + default: + break; + } + } + updateEntry(tag, std::move(entry)); +} + void SynchronousTransformOverlay::updateEntry(Tag tag, Entry&& entry) { if (!entry.transform.has_value() && !entry.transformOrigin.has_value()) { return; diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h index 35e1bc469afb..7af03b91553c 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/SynchronousTransformOverlay.h @@ -10,6 +10,7 @@ #include #include +#include #include #include #include @@ -32,8 +33,8 @@ namespace facebook::react { * final values. * * Layout queries (`measure`, `measureInWindow`, `getBoundingClientRect`, - * `findNodeAtPoint`, `UIManager::getRelativeLayoutMetrics`) consult the - * overlay through the `TransformOverrideProvider` interface, so they describe + * `UIManager::getRelativeLayoutMetrics`) consult the overlay through the + * `TransformOverrideProvider` interface, so they describe * the mounted views rather than the stale committed props. Only `transform` * is tracked because it is the only synchronously updated prop that affects * geometry; layout props always go through a commit. @@ -51,6 +52,12 @@ class SynchronousTransformOverlay final : public LayoutableShadowNode::Transform */ void update(Tag tag, const folly::dynamic &props, const PropsParserContext &context); + /* + * Same as above for the typed `AnimatedProps` representation used by the + * batched synchronous update path (`optimizedAnimatedPropUpdates`). + */ + void update(Tag tag, const AnimatedProps &props, const PropsParserContext &context); + /* * Drops the entry for `tag`, if any. Called by the systems that write * synchronous props when they stop managing a view, and by `prune`. diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp index 1b35d811bd70..1b01c9bec457 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.cpp @@ -801,6 +801,15 @@ void UIManager::recordSynchronousTransform(Tag tag, const folly::dynamic& props) synchronousTransformOverlay_.update(tag, props, propsParserContext); } +void UIManager::recordSynchronousTransform(Tag tag, const AnimatedProps& props) + const { + if (!ReactNativeFeatureFlags::enableSynchronousTransformOverlay()) { + return; + } + PropsParserContext propsParserContext{-1, *contextContainer_}; + synchronousTransformOverlay_.update(tag, props, propsParserContext); +} + void UIManager::removeSynchronousTransform(Tag tag) const { synchronousTransformOverlay_.remove(tag); } diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h index 40280f4b9fa6..fd7422037706 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/UIManager.h @@ -101,6 +101,7 @@ class UIManager final : public ShadowTreeDelegate { * `enableSynchronousTransformOverlay` feature flag is enabled. */ void recordSynchronousTransform(Tag tag, const folly::dynamic &props) const; + void recordSynchronousTransform(Tag tag, const AnimatedProps &props) const; /* * Drops the recorded synchronous transform for `tag`. To be called by the diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md b/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md index 19a428f28b27..3209b6f9d916 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/__docs__/SynchronousTransformOverlay.md @@ -20,8 +20,9 @@ on animated views. Animation systems that write props through the platform entry points (`FabricUIManager.synchronouslyUpdateViewOnUIThread` on Android, `RCTSurfacePresenter`'s `schedulerDidSynchronouslyUpdateViewOnUIThread:props:` -on iOS, `UIManager::synchronouslyUpdateViewOnUIThread` in C++) are covered -automatically. Systems that bypass them call +on iOS, `UIManager::synchronouslyUpdateViewOnUIThread` in C++, and the batched +`AnimatedProps` path of `UIManager::synchronouslyUpdateAnimatedProps`) are +covered automatically. Systems that bypass them call `UIManager::recordSynchronousTransform(tag, props)` with the props they applied, and `UIManager::removeSynchronousTransform(tag)` when they stop managing a view. diff --git a/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp index 4cc1664dc120..582c45ab96fe 100644 --- a/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp +++ b/packages/react-native/ReactCommon/react/renderer/uimanager/tests/SynchronousTransformOverlayTest.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -101,6 +102,32 @@ TEST_F(SynchronousTransformOverlayTest, parsesTransformLikeProps) { EXPECT_EQ(*transform, Transform::Translate(0, 100, 0)); } +TEST_F(SynchronousTransformOverlayTest, parsesAnimatedProps) { + auto animatedProps = AnimatedProps{}; + animatedProps.props.push_back( + std::make_unique>( + TRANSFORM, Transform::Translate(0, 300, 0))); + + overlay_.update(2, animatedProps, propsParserContext_); + + auto transform = overlay_.getTransformOverride(*childShadowNode_); + ASSERT_TRUE(transform.has_value()); + EXPECT_EQ(*transform, Transform::Translate(0, 300, 0)); +} + +TEST_F(SynchronousTransformOverlayTest, parsesUntypedAnimatedProps) { + auto animatedProps = AnimatedProps{}; + animatedProps.rawProps = std::make_unique(folly::dynamic::object( + "transform", + folly::dynamic::array(folly::dynamic::object("translateY", 300)))); + + overlay_.update(2, animatedProps, propsParserContext_); + + auto transform = overlay_.getTransformOverride(*childShadowNode_); + ASSERT_TRUE(transform.has_value()); + EXPECT_EQ(*transform, Transform::Translate(0, 300, 0)); +} + TEST_F(SynchronousTransformOverlayTest, mergesTransformOriginUpdates) { // The committed props have no transform origin; a synchronous update of // the origin alone applies to the committed transform (5, 5). diff --git a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api index 82819d6aa8ca..da95b10d8a59 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -3929,6 +3929,7 @@ class facebook::react::SynchronousTransformOverlay : public facebook::react::Lay public void clear(); public void prune(const std::function& isAlive); public void remove(facebook::react::Tag tag); + public void update(facebook::react::Tag tag, const facebook::react::AnimatedProps& props, const facebook::react::PropsParserContext& context); public void update(facebook::react::Tag tag, const folly::dynamic& props, const facebook::react::PropsParserContext& context); } @@ -4224,6 +4225,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void appendChild(const std::shared_ptr& parentShadowNode, const std::shared_ptr& childShadowNode) const; public void completeSurface(facebook::react::SurfaceId surfaceId, const std::shared_ptr>>& rootChildren, facebook::react::ShadowTree::CommitOptions commitOptions); public void dispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) const; + public void recordSynchronousTransform(facebook::react::Tag tag, const facebook::react::AnimatedProps& props) const; public void recordSynchronousTransform(facebook::react::Tag tag, const folly::dynamic& props) const; public void registerCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void registerMountHook(facebook::react::UIManagerMountHook& mountHook); diff --git a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api index cae3b0bbe763..16fafd97f963 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -6078,6 +6078,7 @@ class facebook::react::SynchronousTransformOverlay : public facebook::react::Lay public void clear(); public void prune(const std::function& isAlive); public void remove(facebook::react::Tag tag); + public void update(facebook::react::Tag tag, const facebook::react::AnimatedProps& props, const facebook::react::PropsParserContext& context); public void update(facebook::react::Tag tag, const folly::dynamic& props, const facebook::react::PropsParserContext& context); } @@ -6366,6 +6367,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void appendChild(const std::shared_ptr& parentShadowNode, const std::shared_ptr& childShadowNode) const; public void completeSurface(facebook::react::SurfaceId surfaceId, const std::shared_ptr>>& rootChildren, facebook::react::ShadowTree::CommitOptions commitOptions); public void dispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) const; + public void recordSynchronousTransform(facebook::react::Tag tag, const facebook::react::AnimatedProps& props) const; public void recordSynchronousTransform(facebook::react::Tag tag, const folly::dynamic& props) const; public void registerCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void registerMountHook(facebook::react::UIManagerMountHook& mountHook); diff --git a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api index 2507af48f665..4c72cf1a37b6 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -2465,6 +2465,7 @@ class facebook::react::SynchronousTransformOverlay : public facebook::react::Lay public void clear(); public void prune(const std::function& isAlive); public void remove(facebook::react::Tag tag); + public void update(facebook::react::Tag tag, const facebook::react::AnimatedProps& props, const facebook::react::PropsParserContext& context); public void update(facebook::react::Tag tag, const folly::dynamic& props, const facebook::react::PropsParserContext& context); } @@ -2701,6 +2702,7 @@ class facebook::react::UIManager : public facebook::react::ShadowTreeDelegate { public void appendChild(const std::shared_ptr& parentShadowNode, const std::shared_ptr& childShadowNode) const; public void completeSurface(facebook::react::SurfaceId surfaceId, const std::shared_ptr>>& rootChildren, facebook::react::ShadowTree::CommitOptions commitOptions); public void dispatchCommand(const std::shared_ptr& shadowNode, const std::string& commandName, const folly::dynamic& args) const; + public void recordSynchronousTransform(facebook::react::Tag tag, const facebook::react::AnimatedProps& props) const; public void recordSynchronousTransform(facebook::react::Tag tag, const folly::dynamic& props) const; public void registerCommitHook(facebook::react::UIManagerCommitHook& commitHook); public void registerMountHook(facebook::react::UIManagerMountHook& mountHook);