Skip to content

Commit ef543ec

Browse files
tomekzawMatiPl01
andauthored
Sync animated styles back to React (force render for settled animations) (#8529)
## Summary This PR introduces `FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS` Reanimated dynamic feature flag. It can be enabled in client apps with the following code: ```tsx import { setDynamicFeatureFlag } from 'react-native-reanimated'; setDynamicFeatureFlag('FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS', true); ``` Fixes #7480. Inspired by #7601 by @hannojg. ## Test plan --------- Co-authored-by: Mateusz Łopaciński <[email protected]>
1 parent 2254329 commit ef543ec

File tree

26 files changed

+493
-12
lines changed

26 files changed

+493
-12
lines changed

apps/common-app/src/apps/reanimated/examples/AboutExample.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,16 @@ export default function AboutExample() {
136136
? 'Enabled'
137137
: 'Disabled'}
138138
</Text>
139+
<Text style={styles.text}>
140+
<Text style={styles.bold}>
141+
FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS:
142+
</Text>{' '}
143+
{getStaticFeatureFlagReanimated(
144+
'FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS'
145+
)
146+
? 'Enabled'
147+
: 'Disabled'}
148+
</Text>
139149
<Text style={styles.text}>
140150
<Text style={styles.bold}>IOS_DYNAMIC_FRAMERATE_ENABLED:</Text>{' '}
141151
{getStaticFeatureFlagWorklets('IOS_DYNAMIC_FRAMERATE_ENABLED')

apps/common-app/src/apps/reanimated/examples/ScreenStackHeaderConfigBackgroundColorExample.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import React from 'react';
2-
import { StyleSheet, View } from 'react-native';
1+
import React, { useCallback } from 'react';
2+
import { Button, StyleSheet, Text, View } from 'react-native';
33
import type {
44
GestureUpdateEvent,
55
PanGestureChangeEventPayload,
@@ -66,12 +66,20 @@ export default function ScreenStackHeaderConfigBackgroundColorExample() {
6666
};
6767
}, [offset]);
6868

69+
const [counter, setCounter] = React.useState(0);
70+
71+
const handleIncrement = useCallback(() => {
72+
setCounter((prev) => prev + 1);
73+
}, []);
74+
6975
return (
7076
<GestureHandlerRootView style={styles.root}>
7177
<ScreenStack style={styles.container}>
7278
<Screen>
7379
<AnimatedScreenStackHeaderConfig animatedProps={animatedProps} />
7480
<View style={styles.container}>
81+
<Text>Counter: {counter}</Text>
82+
<Button title="Increase counter" onPress={handleIncrement} />
7583
<GestureDetector gesture={gesture}>
7684
<Animated.View style={[styles.ball, animatedStyles]} />
7785
</GestureDetector>
Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
import { balloonsImage } from '@/apps/css/assets';
2+
import React, { useCallback } from 'react';
3+
import { Button, Platform, StyleSheet, Text, View } from 'react-native';
4+
import Animated, {
5+
DynamicColorIOS,
6+
PlatformColor,
7+
useAnimatedStyle,
8+
useSharedValue,
9+
withSpring,
10+
} from 'react-native-reanimated';
11+
12+
const instructions = [
13+
'1. Press "Toggle shared value" button',
14+
'2. Wait until the animated styles are synced back to React (about 3 seconds)',
15+
'3. Press "Increase counter" button',
16+
'4. The width and colors should not change, similar to when the feature flag is disabled',
17+
].join('\n');
18+
19+
interface CustomButtonProps {
20+
title: string;
21+
onPress: () => void;
22+
}
23+
24+
// We use a custom button component on native platforms
25+
// because the one from React Native triggers additional renders when pressed.
26+
function CustomButton({ title, onPress }: CustomButtonProps) {
27+
if (Platform.OS === 'web') {
28+
return <Button title={title} onPress={onPress} />;
29+
}
30+
31+
return (
32+
<View onTouchEnd={onPress} style={styles.buttonView}>
33+
<Text style={styles.buttonText}>{title}</Text>
34+
</View>
35+
);
36+
}
37+
38+
export default function SyncBackToReactExample() {
39+
const [count, setCount] = React.useState(0);
40+
41+
const ref = React.useRef(false);
42+
43+
const sv = useSharedValue(false);
44+
45+
const animatedStyle1 = useAnimatedStyle(() => {
46+
return {
47+
width: withSpring(sv.value ? 150 : 50),
48+
};
49+
});
50+
51+
const animatedStyle2 = useAnimatedStyle(() => {
52+
return {
53+
backgroundColor: sv.value ? 'blue' : 'green',
54+
};
55+
});
56+
57+
const animatedStyle3 = useAnimatedStyle(() => {
58+
return {
59+
backgroundColor: sv.value ? 'transparent' : 'green',
60+
};
61+
});
62+
63+
const animatedStyle4 = useAnimatedStyle(() => {
64+
return {
65+
backgroundColor:
66+
Platform.OS === 'android'
67+
? PlatformColor(
68+
sv.value
69+
? '@android:color/holo_blue_bright'
70+
: '@android:color/holo_green_light'
71+
)
72+
: Platform.OS === 'ios'
73+
? PlatformColor(sv.value ? 'systemBlue' : 'systemGreen')
74+
: 'gray',
75+
};
76+
});
77+
78+
const animatedStyle5 = useAnimatedStyle(() => {
79+
return {
80+
backgroundColor:
81+
Platform.OS === 'ios'
82+
? sv.value
83+
? DynamicColorIOS({ light: 'blue', dark: 'orange' })
84+
: DynamicColorIOS({ light: 'green', dark: 'red' })
85+
: 'gray',
86+
};
87+
});
88+
89+
const animatedStyle6 = useAnimatedStyle(() => {
90+
return {
91+
boxShadow: [
92+
{
93+
blurRadius: 10,
94+
offsetX: 10,
95+
offsetY: 10,
96+
color: sv.value ? 'blue' : 'green',
97+
},
98+
],
99+
};
100+
});
101+
102+
const animatedStyle7 = useAnimatedStyle(() => {
103+
return {
104+
transform: `rotate(${sv.value ? 30 : 0}deg)`,
105+
};
106+
});
107+
108+
const animatedStyle8 = useAnimatedStyle(() => {
109+
return {
110+
filter: `brightness(${sv.value ? 0.5 : 1})`,
111+
};
112+
});
113+
114+
const handleToggle = useCallback(() => {
115+
sv.value = ref.current = !ref.current;
116+
}, [sv]);
117+
118+
const handleIncreaseCounter = useCallback(() => {
119+
setCount((c) => c + 1);
120+
}, []);
121+
122+
return (
123+
<View style={styles.container}>
124+
<Animated.View style={[styles.box, animatedStyle1]} />
125+
<Animated.View style={[styles.box, animatedStyle2]} />
126+
<Animated.View style={[styles.box, animatedStyle3]} />
127+
<Animated.View style={[styles.box, animatedStyle4]} />
128+
<Animated.View style={[styles.box, animatedStyle5]} />
129+
<Animated.View style={[styles.box, animatedStyle6]} />
130+
<Animated.View style={[styles.box, animatedStyle7]} />
131+
<Animated.Image
132+
source={balloonsImage}
133+
// @ts-ignore
134+
style={[styles.box, animatedStyle8]}
135+
/>
136+
<CustomButton title="Toggle shared value" onPress={handleToggle} />
137+
<Text>Counter: {count}</Text>
138+
<CustomButton title="Increase counter" onPress={handleIncreaseCounter} />
139+
<Text style={styles.instructions}>{instructions}</Text>
140+
</View>
141+
);
142+
}
143+
144+
const styles = StyleSheet.create({
145+
container: {
146+
flex: 1,
147+
alignItems: 'center',
148+
justifyContent: 'center',
149+
},
150+
box: {
151+
width: 50,
152+
height: 50,
153+
backgroundColor: 'black',
154+
},
155+
buttonView: {
156+
margin: 20,
157+
},
158+
buttonText: {
159+
fontSize: 20,
160+
color: 'dodgerblue',
161+
},
162+
instructions: {
163+
marginHorizontal: 20,
164+
},
165+
});

apps/common-app/src/apps/reanimated/examples/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ import ReducedMotionLayoutExample from './LayoutAnimations/ReducedMotionLayoutEx
8080
import ReparentingExample from './LayoutAnimations/ReparentingExample';
8181
import SpringLayoutAnimation from './LayoutAnimations/SpringLayoutAnimation';
8282
import SwipeableList from './LayoutAnimations/SwipeableList';
83+
import SyncBackToReactExample from './SyncBackToReactExample';
8384
import ViewFlatteningExample from './LayoutAnimations/ViewFlattening';
8485
import ViewRecyclingExample from './LayoutAnimations/ViewRecyclingExample';
8586
import LettersExample from './LettersExample';
@@ -173,6 +174,11 @@ export const EXAMPLES: Record<string, Example> = {
173174
title: 'FPS',
174175
screen: FpsExample,
175176
},
177+
SyncBackToReactExample: {
178+
icon: '🔄',
179+
title: 'Sync back to React',
180+
screen: SyncBackToReactExample,
181+
},
176182
DetachAnimatedStylesExample: {
177183
icon: '⛓️‍💥',
178184
title: 'Detach animated styles',

apps/fabric-example/ios/Podfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3258,7 +3258,7 @@ SPEC CHECKSUMS:
32583258
RNCClipboard: 4b58c780f63676367640f23c8e114e9bd0cf86ac
32593259
RNCMaskedView: 5ef8c95cbab95334a32763b72896a7b7d07e6299
32603260
RNGestureHandler: f1dd7f92a0faa2868a919ab53bb9d66eb4ebfcf5
3261-
RNReanimated: 97ebf4d3c76929b6b0f866cfbd41c49b3a0d2dbf
3261+
RNReanimated: 9d63f516478a3b090588b51ef4f47f511a19beda
32623262
RNScreens: 0bbf16c074ae6bb1058a7bf2d1ae017f4306797c
32633263
RNSVG: 8c0bbfa480a24b24468f1c76bd852a4aac3178e6
32643264
RNWorklets: f54a415f73a3fc653bfe65e599872fdc6bca0477

apps/fabric-example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS": true,
3737
"EXPERIMENTAL_CSS_ANIMATIONS_FOR_SVG_COMPONENTS": true,
3838
"USE_SYNCHRONIZABLE_FOR_MUTABLES": true,
39-
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true
39+
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true,
40+
"FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": true
4041
}
4142
},
4243
"worklets": {

apps/macos-example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@
4747
"IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS": true,
4848
"EXPERIMENTAL_CSS_ANIMATIONS_FOR_SVG_COMPONENTS": true,
4949
"USE_SYNCHRONIZABLE_FOR_MUTABLES": true,
50-
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true
50+
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true,
51+
"FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": true
5152
}
5253
},
5354
"worklets": {

apps/tvos-example/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@
5050
"IOS_SYNCHRONOUSLY_UPDATE_UI_PROPS": true,
5151
"EXPERIMENTAL_CSS_ANIMATIONS_FOR_SVG_COMPONENTS": true,
5252
"USE_SYNCHRONIZABLE_FOR_MUTABLES": true,
53-
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true
53+
"USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS": true,
54+
"FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS": true
5455
}
5556
},
5657
"worklets": {

docs/docs-reanimated/docs/guides/feature-flags.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ Feature flags are available since Reanimated 4.
2222
| [`EXPERIMENTAL_CSS_ANIMATIONS_FOR_SVG_COMPONENTS`](#experimental_css_animations_for_svg_components) | [static](#static-feature-flags) | 4.1.0 | &ndash; | `false` |
2323
| [`USE_SYNCHRONIZABLE_FOR_MUTABLES`](#use_synchronizable_for_mutables) | [static](#static-feature-flags) | 4.1.0 | &ndash; | `false` |
2424
| [`USE_COMMIT_HOOK_ONLY_FOR_REACT_COMMITS`](#use_commit_hook_only_for_react_commits) | [static](#static-feature-flags) | 4.2.0 | &ndash; | `false` |
25+
| [`FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS`](#force_react_render_for_settled_animations) | [static](#static-feature-flags) | 4.2.0 | &ndash; | `false` |
2526

2627
:::info
2728

@@ -152,6 +153,10 @@ This feature flag is supposed to speedup shared value reads on the RN runtime by
152153

153154
This feature flag is supposed to fix performance regressions of animations while scrolling. When enabled, `ReanimatedCommitHook` applies latest animated styles and props only for React commits, which means the logic will be skipped for other commits, including state updates.
154155

156+
### `FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS`
157+
158+
This feature flag enables a mechanism that periodically synchronizes animated style updates back to React by triggering a React render for animated components with accumulated animated styles and evicting them from the registry on the C++ side. It is supposed to improve performance by decreasing the number of `ShadowNode` clone operations in `ReanimatedCommitHook` for React commits. However, for the time being, it also alters the behavior when detaching animated styles from animated components – the animated styles won't be reverted to the original styles. This can cause unwanted side effects in your app's behavior, so please use this feature flag with caution, particularly if some parts of your app rely on detaching animated styles.
159+
155160
## Static feature flags
156161

157162
Static flags are intended to be resolved during code compilation and cannot be changed during application runtime. To enable a static feature flag, you need to:

packages/react-native-reanimated/Common/cpp/reanimated/Fabric/updates/AnimatedPropsRegistry.cpp

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#include <reanimated/Fabric/updates/AnimatedPropsRegistry.h>
2+
#include <reanimated/Tools/FeatureFlags.h>
23

34
#include <memory>
45
#include <utility>
@@ -13,7 +14,7 @@ static inline std::shared_ptr<const ShadowNode> shadowNodeFromValue(
1314
}
1415
#endif
1516

16-
void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operations) {
17+
void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operations, const double timestamp) {
1718
auto operationsArray = operations.asObject(rt).asArray(rt);
1819

1920
for (size_t i = 0, length = operationsArray.size(rt); i < length; ++i) {
@@ -23,11 +24,49 @@ void AnimatedPropsRegistry::update(jsi::Runtime &rt, const jsi::Value &operation
2324

2425
const jsi::Value &updates = item.getProperty(rt, "updates");
2526
addUpdatesToBatch(shadowNode, jsi::dynamicFromValue(rt, updates));
27+
28+
if constexpr (StaticFeatureFlags::getFlag("FORCE_REACT_RENDER_FOR_SETTLED_ANIMATIONS")) {
29+
timestampMap_[shadowNode->getTag()] = timestamp;
30+
}
2631
}
2732
}
2833

2934
void AnimatedPropsRegistry::remove(const Tag tag) {
3035
updatesRegistry_.erase(tag);
3136
}
3237

38+
jsi::Value AnimatedPropsRegistry::getUpdatesOlderThanTimestamp(jsi::Runtime &rt, const double timestamp) {
39+
std::vector<std::pair<Tag, std::reference_wrapper<const folly::dynamic>>> updates;
40+
41+
for (const auto &[viewTag, pair] : updatesRegistry_) {
42+
if (timestampMap_.at(viewTag) < timestamp) {
43+
updates.emplace_back(viewTag, std::cref(pair.second));
44+
}
45+
}
46+
47+
const jsi::Array array(rt, updates.size());
48+
size_t i = 0;
49+
for (const auto &[viewTag, styleProps] : updates) {
50+
const jsi::Object item(rt);
51+
item.setProperty(rt, "viewTag", viewTag);
52+
item.setProperty(rt, "styleProps", jsi::valueFromDynamic(rt, styleProps.get()));
53+
array.setValueAtIndex(rt, i++, item);
54+
}
55+
56+
return jsi::Value(rt, array);
57+
}
58+
59+
void AnimatedPropsRegistry::removeUpdatesOlderThanTimestamp(const double timestamp) {
60+
for (auto it = timestampMap_.begin(); it != timestampMap_.end();) {
61+
const auto viewTag = it->first;
62+
const auto viewTimestamp = it->second;
63+
if (viewTimestamp < timestamp) {
64+
it = timestampMap_.erase(it);
65+
updatesRegistry_.erase(viewTag);
66+
} else {
67+
it++;
68+
}
69+
}
70+
}
71+
3372
} // namespace reanimated

0 commit comments

Comments
 (0)