Skip to content

Commit f7f2743

Browse files
Keep C++ Animated's direct updates out of React commits with the animation backend (#59586)
Summary: Stacked on #59585, which is stacked on #59584. Review the last two commits: the change and its tests. With the shared animation backend, the backend's commit hook writes C++ Animated's latest values into every React commit made while it animates. C++ Animated without the backend does not do this. It writes non-layout props straight to the views, keeps them over React's mounts with `AnimatedMountingOverrideDelegate`, and commits the final values when an animation finishes. This PR adds a way to experiment with the second approach on the backend. It adds `commitDirectProps` to `AnimationFrontend` and the `enableCxxAnimatedDirectPathInBackend` flag. With the flag, C++ Animated registers with `commitDirectProps = false` and behaves as it does without the backend: - The commit hook does not write its direct values into React commits. Layout updates are unchanged. - `AnimatedMountingOverrideDelegate` keeps the values of connected props nodes over React mounts. - The last update of an animation is recorded apart, and the end-of-animation commit writes it. - One end-of-animation commit per surface is scheduled at a time. Without the flag, C++ Animated registers with `commitDirectProps = true` and the end-of-animation commit is unchanged. Turning `commitDirectProps` off without the flag asserts. ## Changelog: [INTERNAL] [ADDED] - Add `enableCxxAnimatedDirectPathInBackend` to keep C++ Animated's direct updates out of React commits with the shared animation backend Pull Request resolved: #59586 Test Plan: **Unit tests.** `AnimatedPropsRegistryTest`, `AnimationBackendFrontendTest` and `NativeAnimatedFrontendTests` cover which values the commit hook writes, when final values are written, taken and restored, that older recorded values do not overwrite a final value, and one end-of-animation commit per surface. The tests for final values and for one commit per surface fail without the code they cover. **Fantom.** - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js` runs with the flag on and off and passes. - `yarn fantom packages/react-native/Libraries/Animated/__tests__/AnimatedBackendDirectUpdates-itest.js` checks that a React commit of an animated view keeps its animated value mounted, with the flag on and off. It fails without the mount override delegate. - `yarn fantom packages/react-native/Libraries/Animated/__tests__/Animated-itest.js` passes. I also ran it locally with the backend and the flag forced on. Fantom allows at most 12 flag combinations per file, so that run is not encoded in the file. **Devices.** RNTester on an Android emulator and an iOS simulator, with the backend and C++ Animated on, once with the flag on and once with it off. I tracked a square frame by frame in screen recordings: | Check | Android, flag on | Android, flag off | iOS, flag on | iOS, flag off | |---|---|---|---|---| | React re-renders the view every 50 ms during a looping native animation | No backward jumps | No backward jumps | No backward jumps | No backward jumps | | The animation finishes, then React keeps re-rendering | Keeps the final values | Keeps the final values | Keeps the final values | Keeps the final values | | `setValue(0)`, then React keeps re-rendering | Stays reset | Stays reset | Stays reset | Stays reset | Differential Revision: D128154682 Pulled By: bartlomiejbloniarz
1 parent 6a44efa commit f7f2743

39 files changed

Lines changed: 1028 additions & 199 deletions

File tree

‎packages/react-native/Libraries/Animated/__tests__/AnimatedBackend-itest.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:*
7+
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:* optimizedAnimatedPropUpdates:* enableCxxAnimatedDirectPathInBackend:*
88
* @flow strict-local
99
* @format
1010
*/
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/**
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @fantom_flags useSharedAnimatedBackend:true enableCxxAnimatedDirectPathInBackend:* updateRuntimeShadowNodeReferencesOnCommitThread:*
8+
* @flow strict-local
9+
* @format
10+
*/
11+
12+
import '@react-native/fantom/src/setUpDefaultReactNativeEnvironment';
13+
14+
import type {HostInstance} from 'react-native';
15+
16+
import * as Fantom from '@react-native/fantom';
17+
import * as React from 'react';
18+
import {createRef, useState} from 'react';
19+
import {Animated, View, useAnimatedValue} from 'react-native';
20+
21+
let _opacity;
22+
let _setAnimatedWidth;
23+
24+
// `still` has the same position and structure as `animated` but never
25+
// animates, so it is cloned only for the reasons unrelated to animations.
26+
function MyApp({
27+
animatedRef,
28+
stillRef,
29+
}: {
30+
animatedRef: React.RefSetter<HostInstance>,
31+
stillRef?: React.RefSetter<HostInstance>,
32+
}) {
33+
const opacity = useAnimatedValue(1);
34+
const stillOpacity = useAnimatedValue(1);
35+
const [animatedWidth, setAnimatedWidth] = useState(100);
36+
_opacity = opacity;
37+
_setAnimatedWidth = setAnimatedWidth;
38+
return (
39+
<View collapsable={false}>
40+
<Animated.View
41+
ref={animatedRef}
42+
style={{
43+
width: animatedWidth,
44+
height: 100,
45+
opacity,
46+
}}
47+
/>
48+
<Animated.View
49+
ref={stillRef}
50+
style={{width: 100, height: 100, opacity: stillOpacity}}
51+
/>
52+
<View collapsable={false} style={{width: 100, height: 100}} />
53+
</View>
54+
);
55+
}
56+
57+
function startOpacityAnimation(duration: number) {
58+
Fantom.runTask(() => {
59+
Animated.timing(_opacity, {
60+
toValue: 0,
61+
duration,
62+
useNativeDriver: true,
63+
}).start();
64+
});
65+
}
66+
67+
test('a React commit of the animated view keeps its direct animated value mounted', () => {
68+
const animatedRef = createRef<HostInstance>();
69+
const root = Fantom.createRoot();
70+
Fantom.runTask(() => {
71+
root.render(<MyApp animatedRef={animatedRef} />);
72+
});
73+
74+
startOpacityAnimation(1000);
75+
Fantom.unstable_produceFramesForDuration(500);
76+
77+
Fantom.runTask(() => {
78+
_setAnimatedWidth(150);
79+
});
80+
81+
expect(root.getRenderedOutput({props: ['opacity', 'width']}).toJSX()).toEqual(
82+
<rn-view>
83+
<rn-view key={0} opacity="0.5" width="150" />
84+
<rn-view key={1} width="100" />
85+
<rn-view key={2} width="100" />
86+
</rn-view>,
87+
);
88+
});

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlags.kt‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<a89ede36c4631b7a7709899858a7f9c9>>
7+
* @generated SignedSource<<e6a3e20f8cbff9506e51a3eef5652fe8>>
88
*/
99

1010
/**
@@ -156,6 +156,12 @@ public object ReactNativeFeatureFlags {
156156
@JvmStatic
157157
public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = accessor.enableCustomFocusSearchOnClippedElementsAndroid()
158158

159+
/**
160+
* With the shared animation backend, C++ Animated keeps its non-layout values out of React commits and commits the final values once per surface when animations end, as it does without the backend.
161+
*/
162+
@JvmStatic
163+
public fun enableCxxAnimatedDirectPathInBackend(): Boolean = accessor.enableCxxAnimatedDirectPathInBackend()
164+
159165
/**
160166
* Enables destructor calls for ShadowTreeRevision in the background to reduce UI thread work.
161167
*/

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxAccessor.kt‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<cd3b4f6a7fb4e616d3f56ab8c9eb9658>>
7+
* @generated SignedSource<<b6771291afa59d44d3f693b803231999>>
88
*/
99

1010
/**
@@ -41,6 +41,7 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
4141
private var enableBridgelessArchitectureCache: Boolean? = null
4242
private var enableCppPropsIteratorSetterCache: Boolean? = null
4343
private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null
44+
private var enableCxxAnimatedDirectPathInBackendCache: Boolean? = null
4445
private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null
4546
private var enableDoubleMeasurementFixAndroidCache: Boolean? = null
4647
private var enableEagerRootViewAttachmentCache: Boolean? = null
@@ -307,6 +308,15 @@ internal class ReactNativeFeatureFlagsCxxAccessor : ReactNativeFeatureFlagsAcces
307308
return cached
308309
}
309310

311+
override fun enableCxxAnimatedDirectPathInBackend(): Boolean {
312+
var cached = enableCxxAnimatedDirectPathInBackendCache
313+
if (cached == null) {
314+
cached = ReactNativeFeatureFlagsCxxInterop.enableCxxAnimatedDirectPathInBackend()
315+
enableCxxAnimatedDirectPathInBackendCache = cached
316+
}
317+
return cached
318+
}
319+
310320
override fun enableDestroyShadowTreeRevisionAsync(): Boolean {
311321
var cached = enableDestroyShadowTreeRevisionAsyncCache
312322
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsCxxInterop.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<6d86d4adbc261c9271b5bed72bd12dcf>>
7+
* @generated SignedSource<<ec5f08343ce9c2e053445c15effef54b>>
88
*/
99

1010
/**
@@ -70,6 +70,8 @@ public object ReactNativeFeatureFlagsCxxInterop {
7070

7171
@DoNotStrip @JvmStatic public external fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean
7272

73+
@DoNotStrip @JvmStatic public external fun enableCxxAnimatedDirectPathInBackend(): Boolean
74+
7375
@DoNotStrip @JvmStatic public external fun enableDestroyShadowTreeRevisionAsync(): Boolean
7476

7577
@DoNotStrip @JvmStatic public external fun enableDoubleMeasurementFixAndroid(): Boolean

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsDefaults.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<a1368426dd823245c54b4312ae7b14e6>>
7+
* @generated SignedSource<<28e566c4e784a0bfc743ad46f316c50b>>
88
*/
99

1010
/**
@@ -65,6 +65,8 @@ public open class ReactNativeFeatureFlagsDefaults : ReactNativeFeatureFlagsProvi
6565

6666
override fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean = true
6767

68+
override fun enableCxxAnimatedDirectPathInBackend(): Boolean = false
69+
6870
override fun enableDestroyShadowTreeRevisionAsync(): Boolean = false
6971

7072
override fun enableDoubleMeasurementFixAndroid(): Boolean = false

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsLocalAccessor.kt‎

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<c305274be2fc6720b6c0eb75e78bd6b0>>
7+
* @generated SignedSource<<d9391a3bef70fdab935a98b4f8affdb3>>
88
*/
99

1010
/**
@@ -45,6 +45,7 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
4545
private var enableBridgelessArchitectureCache: Boolean? = null
4646
private var enableCppPropsIteratorSetterCache: Boolean? = null
4747
private var enableCustomFocusSearchOnClippedElementsAndroidCache: Boolean? = null
48+
private var enableCxxAnimatedDirectPathInBackendCache: Boolean? = null
4849
private var enableDestroyShadowTreeRevisionAsyncCache: Boolean? = null
4950
private var enableDoubleMeasurementFixAndroidCache: Boolean? = null
5051
private var enableEagerRootViewAttachmentCache: Boolean? = null
@@ -332,6 +333,16 @@ internal class ReactNativeFeatureFlagsLocalAccessor : ReactNativeFeatureFlagsAcc
332333
return cached
333334
}
334335

336+
override fun enableCxxAnimatedDirectPathInBackend(): Boolean {
337+
var cached = enableCxxAnimatedDirectPathInBackendCache
338+
if (cached == null) {
339+
cached = currentProvider.enableCxxAnimatedDirectPathInBackend()
340+
accessedFeatureFlags.add("enableCxxAnimatedDirectPathInBackend")
341+
enableCxxAnimatedDirectPathInBackendCache = cached
342+
}
343+
return cached
344+
}
345+
335346
override fun enableDestroyShadowTreeRevisionAsync(): Boolean {
336347
var cached = enableDestroyShadowTreeRevisionAsyncCache
337348
if (cached == null) {

‎packages/react-native/ReactAndroid/src/main/java/com/facebook/react/internal/featureflags/ReactNativeFeatureFlagsProvider.kt‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<da45e41e9a58711ee3b68a38965938fb>>
7+
* @generated SignedSource<<e4b8bf98b7a7a845fd80820c668d969d>>
88
*/
99

1010
/**
@@ -65,6 +65,8 @@ public interface ReactNativeFeatureFlagsProvider {
6565

6666
@DoNotStrip public fun enableCustomFocusSearchOnClippedElementsAndroid(): Boolean
6767

68+
@DoNotStrip public fun enableCxxAnimatedDirectPathInBackend(): Boolean
69+
6870
@DoNotStrip public fun enableDestroyShadowTreeRevisionAsync(): Boolean
6971

7072
@DoNotStrip public fun enableDoubleMeasurementFixAndroid(): Boolean

‎packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.cpp‎

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<944781e7a547e601f0637869a3a3ec5e>>
7+
* @generated SignedSource<<30b22c622145cb7664db5550e6d82440>>
88
*/
99

1010
/**
@@ -165,6 +165,12 @@ class ReactNativeFeatureFlagsJavaProvider
165165
return method(javaProvider_);
166166
}
167167

168+
bool enableCxxAnimatedDirectPathInBackend() override {
169+
static const auto method =
170+
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableCxxAnimatedDirectPathInBackend");
171+
return method(javaProvider_);
172+
}
173+
168174
bool enableDestroyShadowTreeRevisionAsync() override {
169175
static const auto method =
170176
getReactNativeFeatureFlagsProviderJavaClass()->getMethod<jboolean()>("enableDestroyShadowTreeRevisionAsync");
@@ -730,6 +736,11 @@ bool JReactNativeFeatureFlagsCxxInterop::enableCustomFocusSearchOnClippedElement
730736
return ReactNativeFeatureFlags::enableCustomFocusSearchOnClippedElementsAndroid();
731737
}
732738

739+
bool JReactNativeFeatureFlagsCxxInterop::enableCxxAnimatedDirectPathInBackend(
740+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
741+
return ReactNativeFeatureFlags::enableCxxAnimatedDirectPathInBackend();
742+
}
743+
733744
bool JReactNativeFeatureFlagsCxxInterop::enableDestroyShadowTreeRevisionAsync(
734745
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop> /*unused*/) {
735746
return ReactNativeFeatureFlags::enableDestroyShadowTreeRevisionAsync();
@@ -1204,6 +1215,9 @@ void JReactNativeFeatureFlagsCxxInterop::registerNatives() {
12041215
makeNativeMethod(
12051216
"enableCustomFocusSearchOnClippedElementsAndroid",
12061217
JReactNativeFeatureFlagsCxxInterop::enableCustomFocusSearchOnClippedElementsAndroid),
1218+
makeNativeMethod(
1219+
"enableCxxAnimatedDirectPathInBackend",
1220+
JReactNativeFeatureFlagsCxxInterop::enableCxxAnimatedDirectPathInBackend),
12071221
makeNativeMethod(
12081222
"enableDestroyShadowTreeRevisionAsync",
12091223
JReactNativeFeatureFlagsCxxInterop::enableDestroyShadowTreeRevisionAsync),

‎packages/react-native/ReactAndroid/src/main/jni/react/featureflags/JReactNativeFeatureFlagsCxxInterop.h‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* This source code is licensed under the MIT license found in the
55
* LICENSE file in the root directory of this source tree.
66
*
7-
* @generated SignedSource<<ad9275a6afb74520bec7d446faafd1bc>>
7+
* @generated SignedSource<<80d466e62bb754e4ed9b6f46f9b9b546>>
88
*/
99

1010
/**
@@ -93,6 +93,9 @@ class JReactNativeFeatureFlagsCxxInterop
9393
static bool enableCustomFocusSearchOnClippedElementsAndroid(
9494
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
9595

96+
static bool enableCxxAnimatedDirectPathInBackend(
97+
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
98+
9699
static bool enableDestroyShadowTreeRevisionAsync(
97100
facebook::jni::alias_ref<JReactNativeFeatureFlagsCxxInterop>);
98101

0 commit comments

Comments
 (0)