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 c675838da595..6c3f62f9c05c 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<> */ /** @@ -348,6 +348,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 ac2da12670b4..30d578704b59 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<<846a8685f662d66dbcdaa4324c772d51>> + * @generated SignedSource<<829c9d546715ab90c1e2f7f823b15725>> */ /** @@ -73,6 +73,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 @@ -595,6 +596,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 2a622658f4a4..140caa931149 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<<9ccf3958e1db2db295ba73dc371b5185>> + * @generated SignedSource<<4ad11886d0b8b743d7212cdcf8baad2a>> */ /** @@ -134,6 +134,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 184ed3c53091..6848ed9dd8aa 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<<0a04c5e0a0b418f2efd4b253646ae7c1>> + * @generated SignedSource<<09f9044aae49f03af686a30b6b6b2562>> */ /** @@ -129,6 +129,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 4239667149d5..3430f8fa783c 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<<861063ef7d217992d3cecd410e25d885>> + * @generated SignedSource<> */ /** @@ -77,6 +77,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 @@ -652,6 +653,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 1e53a20b61de..43bc2cd8d769 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<<5fa21c808df68be4c007469040fbca2a>> + * @generated SignedSource<> */ /** @@ -129,6 +129,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 db6b559788e0..ff4506f1dc3a 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<<7e430d5b575ea983d53ecde87c4dc9f0>> + * @generated SignedSource<<718437adc32ac2d802dba0f07b3ef5a0>> */ /** @@ -357,6 +357,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"); @@ -890,6 +896,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(); @@ -1300,6 +1311,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 21b28bc24d49..50c07b5f03aa 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<<6b96c286c25d3c9fcb66d39d67092787>> + * @generated SignedSource<> */ /** @@ -189,6 +189,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 0399ade6a93f..cdd3b9934682 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<<493a51a28c2f55411c5c5aea374638d1>> + * @generated SignedSource<> */ /** @@ -238,6 +238,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 aa7c277ed22a..e4641297342b 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<<2d7aa66f222fa01d4cc93c7d5ef2fdb7>> + * @generated SignedSource<<69a7e8cf8850e0ce5a6c567c3c9c6187>> */ /** @@ -306,6 +306,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 88eda4281359..858c8ae318bd 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<<781830e6179bb655a1ba3d955c5d2a4a>> + * @generated SignedSource<<94ab735875637c5d78038cdde6ee554a>> */ /** @@ -983,6 +983,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(53, "enableSynchronousTransformOverlay"); + + flagValue = currentProvider_->enableSynchronousTransformOverlay(); + enableSynchronousTransformOverlay_ = flagValue; + } + + return flagValue.value(); +} + bool ReactNativeFeatureFlagsAccessor::enableViewCulling() { auto flagValue = enableViewCulling_.load(); @@ -992,7 +1010,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(53, "enableViewCulling"); + markFlagAsAccessed(54, "enableViewCulling"); flagValue = currentProvider_->enableViewCulling(); enableViewCulling_ = flagValue; @@ -1010,7 +1028,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(54, "enableViewRecycling"); + markFlagAsAccessed(55, "enableViewRecycling"); flagValue = currentProvider_->enableViewRecycling(); enableViewRecycling_ = flagValue; @@ -1028,7 +1046,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(55, "enableViewRecyclingForImage"); + markFlagAsAccessed(56, "enableViewRecyclingForImage"); flagValue = currentProvider_->enableViewRecyclingForImage(); enableViewRecyclingForImage_ = flagValue; @@ -1046,7 +1064,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(56, "enableViewRecyclingForScrollView"); + markFlagAsAccessed(57, "enableViewRecyclingForScrollView"); flagValue = currentProvider_->enableViewRecyclingForScrollView(); enableViewRecyclingForScrollView_ = flagValue; @@ -1064,7 +1082,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(57, "enableViewRecyclingForText"); + markFlagAsAccessed(58, "enableViewRecyclingForText"); flagValue = currentProvider_->enableViewRecyclingForText(); enableViewRecyclingForText_ = flagValue; @@ -1082,7 +1100,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(58, "enableViewRecyclingForView"); + markFlagAsAccessed(59, "enableViewRecyclingForView"); flagValue = currentProvider_->enableViewRecyclingForView(); enableViewRecyclingForView_ = flagValue; @@ -1100,7 +1118,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(59, "enableVirtualViewContainerStateExperimental"); + markFlagAsAccessed(60, "enableVirtualViewContainerStateExperimental"); flagValue = currentProvider_->enableVirtualViewContainerStateExperimental(); enableVirtualViewContainerStateExperimental_ = flagValue; @@ -1118,7 +1136,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(60, "fixBorderlessRippleAndroid"); + markFlagAsAccessed(61, "fixBorderlessRippleAndroid"); flagValue = currentProvider_->fixBorderlessRippleAndroid(); fixBorderlessRippleAndroid_ = flagValue; @@ -1136,7 +1154,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(61, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); + markFlagAsAccessed(62, "fixMappingOfEventPrioritiesBetweenFabricAndReact"); flagValue = currentProvider_->fixMappingOfEventPrioritiesBetweenFabricAndReact(); fixMappingOfEventPrioritiesBetweenFabricAndReact_ = flagValue; @@ -1154,7 +1172,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(62, "fixTextLayoutOnPixelDensityChange"); + markFlagAsAccessed(63, "fixTextLayoutOnPixelDensityChange"); flagValue = currentProvider_->fixTextLayoutOnPixelDensityChange(); fixTextLayoutOnPixelDensityChange_ = flagValue; @@ -1172,7 +1190,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(63, "fixYogaFlexBasisFitContentInMainAxis"); + markFlagAsAccessed(64, "fixYogaFlexBasisFitContentInMainAxis"); flagValue = currentProvider_->fixYogaFlexBasisFitContentInMainAxis(); fixYogaFlexBasisFitContentInMainAxis_ = flagValue; @@ -1190,7 +1208,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(64, "fuseboxAssertSingleHostState"); + markFlagAsAccessed(65, "fuseboxAssertSingleHostState"); flagValue = currentProvider_->fuseboxAssertSingleHostState(); fuseboxAssertSingleHostState_ = flagValue; @@ -1208,7 +1226,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(65, "fuseboxEnabledRelease"); + markFlagAsAccessed(66, "fuseboxEnabledRelease"); flagValue = currentProvider_->fuseboxEnabledRelease(); fuseboxEnabledRelease_ = flagValue; @@ -1226,7 +1244,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(66, "fuseboxFrameRecordingEnabled"); + markFlagAsAccessed(67, "fuseboxFrameRecordingEnabled"); flagValue = currentProvider_->fuseboxFrameRecordingEnabled(); fuseboxFrameRecordingEnabled_ = flagValue; @@ -1244,7 +1262,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(67, "fuseboxScreenshotCaptureEnabled"); + markFlagAsAccessed(68, "fuseboxScreenshotCaptureEnabled"); flagValue = currentProvider_->fuseboxScreenshotCaptureEnabled(); fuseboxScreenshotCaptureEnabled_ = flagValue; @@ -1262,7 +1280,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(68, "fuseboxWebSocketEventsEnabled"); + markFlagAsAccessed(69, "fuseboxWebSocketEventsEnabled"); flagValue = currentProvider_->fuseboxWebSocketEventsEnabled(); fuseboxWebSocketEventsEnabled_ = 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 ced722113f2c..1f8bc86571fd 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<<66130994f415fa235df5b77405d068ee>> + * @generated SignedSource<<3acbe133cd28b1cdabb4724555927ada>> */ /** @@ -87,6 +87,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_; @@ -197,6 +198,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 89bdc7100fe2..2e76080b368d 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<> + * @generated SignedSource<> */ /** @@ -241,6 +241,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 18d900df42ee..ed3043998270 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<<41d72ef0a42994ab19ca76c16dc04a86>> + * @generated SignedSource<<1dbef4dc0cf127f661c879b245dfdfa3>> */ /** @@ -524,6 +524,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 20f0a085d43b..cd8e68566ba9 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<<96e9edf6ff0b347963cb155d59cb1005>> + * @generated SignedSource<<4f9391f16cd4355b0dd6b71f8d775c6a>> */ /** @@ -80,6 +80,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 83eec6acd78e..e3a41b93afb3 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<> */ /** @@ -309,6 +309,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 493206eca621..921f0c450c3c 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<<3f0c67926f9bc7a9973e17afe59b3ec9>> + * @generated SignedSource<<22aaf992a3dd50efcb2476ef41be568a>> */ /** @@ -144,6 +144,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 a2757e6007eb..68203d2c72ed 100644 --- a/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js +++ b/packages/react-native/scripts/featureflags/ReactNativeFeatureFlags.config.js @@ -619,6 +619,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 237cf99f874b..28dc7c7f5f3d 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<<73327eab3ccc39c8d823c9d2b110d23c>> + * @generated SignedSource<> * @flow strict * @noformat */ @@ -102,6 +102,7 @@ export type ReactNativeFeatureFlags = Readonly<{ enablePropsUpdateReconciliationAndroid: Getter, enableResizeObserverByDefault: Getter, enableSwiftUIBasedFilters: Getter, + enableSynchronousTransformOverlay: Getter, enableViewCulling: Getter, enableViewRecycling: Getter, enableViewRecyclingForImage: Getter, @@ -434,6 +435,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 65dd1af1c1b9..1b07a75ea9f2 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<<043ccaebbb88ea3c413809d9d628d584>> + * @generated SignedSource<<88ca5bf621cca9649ab75d70d87a5e3b>> * @flow strict * @noformat */ @@ -78,6 +78,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 6d9ac1caa787..b6cf63c850dc 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 d2b41ea569c8..08f74f037c11 100644 --- a/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAndroidFrameworksCxx.api @@ -2590,6 +2590,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; @@ -2607,11 +2608,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 { @@ -3913,6 +3920,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); } @@ -4177,6 +4194,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; @@ -4199,9 +4217,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); @@ -8772,11 +8792,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 57f7bc098311..ec28516c298f 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 57f7bc098311..ec28516c298f 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 863038207ad0..8f799189bdd2 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleDebugCxx.api @@ -4000,6 +4000,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; @@ -4017,11 +4018,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 dece37c06a37..ef0965fcca44 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleFrameworksCxx.api @@ -4809,6 +4809,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; @@ -4826,11 +4827,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 { @@ -6088,6 +6095,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); } @@ -6350,6 +6367,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; @@ -6372,9 +6390,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); @@ -10669,11 +10689,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 3e9a7b4744fc..6e1407290389 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleNewarchCxx.api @@ -3991,6 +3991,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; @@ -4008,11 +4009,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 a692b6489f17..f97d79239a75 100644 --- a/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactAppleReleaseCxx.api @@ -3999,6 +3999,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; @@ -4016,11 +4017,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 cd608ca12bc3..77ba1ca8727e 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 c06d3fc44d2a..83618cd1523e 100644 --- a/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api +++ b/scripts/cxx-api/api-snapshots/ReactCommonFrameworksCxx.api @@ -1521,6 +1521,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; @@ -1538,11 +1539,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 { @@ -2450,6 +2457,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); } @@ -2660,6 +2677,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; @@ -2682,9 +2700,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); @@ -6001,11 +6021,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 1da6bfffd246..a1b25be62c54 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 1da6bfffd246..a1b25be62c54 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 {