From 8fc40a7a7bcde956dcad5233f4340405945ec275 Mon Sep 17 00:00:00 2001 From: jingjing2222 Date: Thu, 21 May 2026 23:32:03 +0900 Subject: [PATCH 1/4] Fix Android Fabric Native Animated prop reset --- .../react/animated/PropsAnimatedNode.kt | 17 ++- .../react/animated/StyleAnimatedNode.kt | 6 ++ .../fabric/mounting/SurfaceMountingManager.kt | 34 ++++-- .../NativeAnimatedNodeTraversalTest.kt | 6 +- ...ountingManagerSynchronousMountPropsTest.kt | 101 +++++++++++++++++- 5 files changed, 150 insertions(+), 14 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt index 753697ac61a9..a2ecbde7c33e 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt @@ -65,8 +65,21 @@ internal class PropsAnimatedNode( } fun restoreDefaultValues() { - // In Fabric, we don't restore default values since the FabricUIManager doesn't have access - // to the ShadowNode layer. This method was only relevant for the legacy Paper renderer. + if (connectedViewTag == -1) { + return + } + + val defaultPropsMap = JavaOnlyMap() + for ((key, value) in propNodeMapping) { + val node = nativeAnimatedNodesManager.getNodeById(value) + requireNotNull(node) { "Mapped property node does not exist" } + if (node is StyleAnimatedNode) { + node.collectViewDefaultValues(defaultPropsMap) + } else { + defaultPropsMap.putNull(key) + } + } + connectedViewUIManager?.synchronouslyUpdateViewOnUIThread(connectedViewTag, defaultPropsMap) } fun updateView() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt index 05bb06114105..539180a8ddac 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt @@ -58,5 +58,11 @@ internal class StyleAnimatedNode( } } + fun collectViewDefaultValues(propsMap: JavaOnlyMap) { + for (key in propMapping.keys) { + propsMap.putNull(key) + } + } + override fun prettyPrint(): String = "StyleAnimatedNode[$tag] mPropMapping: $propMapping" } diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index ad8f5ca188ee..68a4b377ab1f 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -626,13 +626,14 @@ internal constructor( public fun storeSynchronousMountPropsOverride(reactTag: Int, props: ReadableMap): Unit { if (ReactNativeFeatureFlags.overrideBySynchronousMountPropsAtMountingAndroid()) { val propsMap = getMapFromPropsReadableMap(props) - var synchronousMountProps = tagToSynchronousMountProps[reactTag] - if (synchronousMountProps != null) { - synchronousMountProps.putAll(propsMap) + val synchronousMountProps = tagToSynchronousMountProps[reactTag] ?: mutableMapOf() + removeNullPropsFromPropsReadableMap(props, synchronousMountProps) + synchronousMountProps.putAll(propsMap) + if (synchronousMountProps.isEmpty()) { + tagToSynchronousMountProps.remove(reactTag) } else { - synchronousMountProps = propsMap + tagToSynchronousMountProps[reactTag] = synchronousMountProps } - tagToSynchronousMountProps[reactTag] = synchronousMountProps } } @@ -1332,7 +1333,10 @@ internal constructor( for ((propKey, propValue) in patchMap) { if (outputReadableMap.hasKey(propKey)) { if (propKey == PROP_TRANSFORM) { - assert(outputReadableMap.getType(propKey) == ReadableType.Array && propValue is List<*>) + val outputType = outputReadableMap.getType(propKey) + assert( + (outputType == ReadableType.Array || outputType == ReadableType.Null) && + propValue is List<*>) val array = WritableNativeArray() for (item in propValue as List<*>) { if (item is Map<*, *>) { @@ -1350,7 +1354,10 @@ internal constructor( } outputReadableMap.putArray(propKey, array) } else if (propKey == PROP_OPACITY) { - assert(outputReadableMap.getType(propKey) == ReadableType.Number && propValue is Number) + val outputType = outputReadableMap.getType(propKey) + assert( + (outputType == ReadableType.Number || outputType == ReadableType.Null) && + propValue is Number) outputReadableMap.putDouble(propKey, (propValue as Number).toDouble()) } } @@ -1387,6 +1394,19 @@ internal constructor( return outputMap } + private fun removeNullPropsFromPropsReadableMap( + readableMap: ReadableMap, + outputMap: MutableMap, + ) { + val iterator = readableMap.keySetIterator() + while (iterator.hasNextKey()) { + val propKey = iterator.nextKey() + if (readableMap.getType(propKey) == ReadableType.Null) { + outputMap.remove(propKey) + } + } + } + // prevents unchecked conversion warn of the type private fun getViewGroupManager(viewState: ViewState): IViewGroupManager { val viewManager = diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt index c780e6809e62..6ec1dbd5c292 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt @@ -1076,7 +1076,7 @@ class NativeAnimatedNodeTraversalTest { } @Test - fun testRestoreDefaultPropsIsNoOp() { + fun testRestoreDefaultPropsSendsNullUpdate() { val viewTag: Int = 1001 val propsNodeTag = 3 nativeAnimatedNodesManager.createAnimatedNode( @@ -1097,7 +1097,9 @@ class NativeAnimatedNodeTraversalTest { reset(uiManagerMock) nativeAnimatedNodesManager.restoreDefaultValues(propsNodeTag) - verify(uiManagerMock, never()).synchronouslyUpdateViewOnUIThread(anyInt(), any()) + val stylesCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ReadableMap::class.java) + verify(uiManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()) + assertThat(stylesCaptor.value.isNull("opacity")).isTrue() } /** diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt index 2c0787d17c84..a3671ae346b3 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt @@ -10,15 +10,17 @@ package com.facebook.react.fabric import com.facebook.react.ReactRootView +import com.facebook.react.bridge.JavaOnlyArray import com.facebook.react.bridge.JavaOnlyMap +import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReactTestHelper import com.facebook.react.fabric.mounting.MountingManager -import com.facebook.react.fabric.mounting.MountingManager.MountItemExecutor import com.facebook.react.fabric.mounting.SurfaceMountingManager import com.facebook.react.internal.featureflags.ReactNativeFeatureFlagsForTests import com.facebook.react.uimanager.ThemedReactContext import com.facebook.react.uimanager.ViewManager import com.facebook.react.uimanager.ViewManagerRegistry +import com.facebook.react.views.view.ReactViewGroup import com.facebook.react.views.view.ReactViewManager import com.facebook.testutils.shadows.ShadowNativeLoader import com.facebook.testutils.shadows.ShadowNativeMap @@ -67,8 +69,8 @@ class SurfaceMountingManagerSynchronousMountPropsTest { themedReactContext = ThemedReactContext(reactContext, reactContext, null, -1) mountingManager = MountingManager( - ViewManagerRegistry(listOf>(ReactViewManager())), - MountItemExecutor {}, + ViewManagerRegistry(listOf>(TestReactViewManager())), + {}, ) } @@ -83,6 +85,25 @@ class SurfaceMountingManagerSynchronousMountPropsTest { smm.addViewAt(surfaceId, tag, 0) } + private class TestReactViewManager : ReactViewManager() { + override fun setTransformProperty( + view: ReactViewGroup, + transforms: ReadableArray?, + transformOrigin: ReadableArray?, + ) { + view.translationY = 0.0f + if (transforms == null) { + return + } + for (i in 0.. Date: Fri, 22 May 2026 00:50:11 +0900 Subject: [PATCH 2/4] Scope Native Animated null override clearing --- .../fabric/mounting/SurfaceMountingManager.kt | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt index 68a4b377ab1f..1947b8d71f41 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/fabric/mounting/SurfaceMountingManager.kt @@ -627,7 +627,7 @@ internal constructor( if (ReactNativeFeatureFlags.overrideBySynchronousMountPropsAtMountingAndroid()) { val propsMap = getMapFromPropsReadableMap(props) val synchronousMountProps = tagToSynchronousMountProps[reactTag] ?: mutableMapOf() - removeNullPropsFromPropsReadableMap(props, synchronousMountProps) + removeNullTransformAndOpacityPropsFromPropsReadableMap(props, synchronousMountProps) synchronousMountProps.putAll(propsMap) if (synchronousMountProps.isEmpty()) { tagToSynchronousMountProps.remove(reactTag) @@ -1394,16 +1394,19 @@ internal constructor( return outputMap } - private fun removeNullPropsFromPropsReadableMap( + private fun removeNullTransformAndOpacityPropsFromPropsReadableMap( readableMap: ReadableMap, outputMap: MutableMap, ) { - val iterator = readableMap.keySetIterator() - while (iterator.hasNextKey()) { - val propKey = iterator.nextKey() - if (readableMap.getType(propKey) == ReadableType.Null) { - outputMap.remove(propKey) - } + // Native Animated uses synchronous null updates to restore animated-managed props. + // Keep this scoped to props stored by this override path today. + if (readableMap.hasKey(PROP_TRANSFORM) && + readableMap.getType(PROP_TRANSFORM) == ReadableType.Null) { + outputMap.remove(PROP_TRANSFORM) + } + if (readableMap.hasKey(PROP_OPACITY) && + readableMap.getType(PROP_OPACITY) == ReadableType.Null) { + outputMap.remove(PROP_OPACITY) } } From b9e22757ae78124d9acecedbc8342c183b8b4671 Mon Sep 17 00:00:00 2001 From: jingjing2222 Date: Fri, 22 May 2026 01:07:48 +0900 Subject: [PATCH 3/4] Move Native Animated restore change out of PR --- .../react/animated/PropsAnimatedNode.kt | 17 ++--------------- .../react/animated/StyleAnimatedNode.kt | 6 ------ .../animated/NativeAnimatedNodeTraversalTest.kt | 6 ++---- 3 files changed, 4 insertions(+), 25 deletions(-) diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt index a2ecbde7c33e..753697ac61a9 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/PropsAnimatedNode.kt @@ -65,21 +65,8 @@ internal class PropsAnimatedNode( } fun restoreDefaultValues() { - if (connectedViewTag == -1) { - return - } - - val defaultPropsMap = JavaOnlyMap() - for ((key, value) in propNodeMapping) { - val node = nativeAnimatedNodesManager.getNodeById(value) - requireNotNull(node) { "Mapped property node does not exist" } - if (node is StyleAnimatedNode) { - node.collectViewDefaultValues(defaultPropsMap) - } else { - defaultPropsMap.putNull(key) - } - } - connectedViewUIManager?.synchronouslyUpdateViewOnUIThread(connectedViewTag, defaultPropsMap) + // In Fabric, we don't restore default values since the FabricUIManager doesn't have access + // to the ShadowNode layer. This method was only relevant for the legacy Paper renderer. } fun updateView() { diff --git a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt index 539180a8ddac..05bb06114105 100644 --- a/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt +++ b/packages/react-native/ReactAndroid/src/main/java/com/facebook/react/animated/StyleAnimatedNode.kt @@ -58,11 +58,5 @@ internal class StyleAnimatedNode( } } - fun collectViewDefaultValues(propsMap: JavaOnlyMap) { - for (key in propMapping.keys) { - propsMap.putNull(key) - } - } - override fun prettyPrint(): String = "StyleAnimatedNode[$tag] mPropMapping: $propMapping" } diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt index 6ec1dbd5c292..c780e6809e62 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/animated/NativeAnimatedNodeTraversalTest.kt @@ -1076,7 +1076,7 @@ class NativeAnimatedNodeTraversalTest { } @Test - fun testRestoreDefaultPropsSendsNullUpdate() { + fun testRestoreDefaultPropsIsNoOp() { val viewTag: Int = 1001 val propsNodeTag = 3 nativeAnimatedNodesManager.createAnimatedNode( @@ -1097,9 +1097,7 @@ class NativeAnimatedNodeTraversalTest { reset(uiManagerMock) nativeAnimatedNodesManager.restoreDefaultValues(propsNodeTag) - val stylesCaptor: ArgumentCaptor = ArgumentCaptor.forClass(ReadableMap::class.java) - verify(uiManagerMock).synchronouslyUpdateViewOnUIThread(eq(viewTag), stylesCaptor.capture()) - assertThat(stylesCaptor.value.isNull("opacity")).isTrue() + verify(uiManagerMock, never()).synchronouslyUpdateViewOnUIThread(anyInt(), any()) } /** From d21804fc3491320600b06c609b8b7f2af70bf245 Mon Sep 17 00:00:00 2001 From: jingjing2222 Date: Fri, 22 May 2026 08:45:04 +0900 Subject: [PATCH 4/4] Use ASCII punctuation in sync mount props test --- .../fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt index a3671ae346b3..ed1a1b4b6c7e 100644 --- a/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt +++ b/packages/react-native/ReactAndroid/src/test/java/com/facebook/react/fabric/SurfaceMountingManagerSynchronousMountPropsTest.kt @@ -121,7 +121,7 @@ class SurfaceMountingManagerSynchronousMountPropsTest { assertThat(smm.getView(tag).alpha).isEqualTo(0.3f) } - /** Multiple storeSynchronousMountPropsOverride calls should merge — later values win. */ + /** Multiple storeSynchronousMountPropsOverride calls should merge; later values win. */ @Test fun storeSynchronousProps_mergesMultipleCalls() { val smm = startSurface()