Skip to content

Commit d04e152

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
Introduce AnimationBackendCommitHook (facebook#54138)
Summary: Pull Request resolved: facebook#54138 This diff introduces `AnimationBackendCommitHook`. It is responsible for keeping animation updates in sync with React. When React does its updates, it doesn't look at the last mounted tree. Instead it hold its own references to ShadowNodes, and commits them whenever there is a (JS) rendering update. Any props that were commited from outside of React (e.g. by Reanimated) are lost. To work around this we have the Commit Hook. It enables us to override any rendering updates that react does, just before the ShadowTree enters the layout phase. In this implementation we utilize RNSRU to push those updates back to React (whenever there is a commit originating from React). So after a render React will now hold references to ShadowNodes with the current animation state (in props). This is a new approach, in Reanimated's current implementation this is disabled, and we reapply the changes via the Commit Hook on each React commit. # Changelog [General] [Added] - Add `AnimationBackendCommitHook` and `AnimatedPropsRegistry [General] [Changed] - Use the commit hook to reconcile the UI updates with react Reviewed By: sammy-SC, zeyap Differential Revision: D84250600 fbshipit-source-id: c5e4b2030e55830ac5df29bde492d33a10f2de35
1 parent d7ef651 commit d04e152

File tree

9 files changed

+462
-19
lines changed

9 files changed

+462
-19
lines changed

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

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import type {HostInstance} from 'react-native';
1515

1616
import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
1717
import * as Fantom from '@react-native/fantom';
18-
import {createRef} from 'react';
18+
import {createRef, useEffect, useState} from 'react';
1919
import {Animated, useAnimatedValue} from 'react-native';
2020
import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
2121
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';
@@ -141,3 +141,160 @@ test('animate layout props', () => {
141141
<rn-view height="100.000000" />,
142142
);
143143
});
144+
145+
test('animate layout props and rerender', () => {
146+
const viewRef = createRef<HostInstance>();
147+
allowStyleProp('height');
148+
149+
let _animatedHeight;
150+
let _heightAnimation;
151+
let _setWidth;
152+
153+
function MyApp() {
154+
const animatedHeight = useAnimatedValue(0);
155+
const [width, setWidth] = useState(100);
156+
_animatedHeight = animatedHeight;
157+
_setWidth = setWidth;
158+
return (
159+
<Animated.View
160+
ref={viewRef}
161+
style={[
162+
{
163+
width: width,
164+
height: animatedHeight,
165+
},
166+
]}
167+
/>
168+
);
169+
}
170+
171+
const root = Fantom.createRoot();
172+
173+
Fantom.runTask(() => {
174+
root.render(<MyApp />);
175+
});
176+
177+
Fantom.runTask(() => {
178+
_heightAnimation = Animated.timing(_animatedHeight, {
179+
toValue: 100,
180+
duration: 1000,
181+
useNativeDriver: true,
182+
}).start();
183+
});
184+
185+
Fantom.unstable_produceFramesForDuration(500);
186+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
187+
<rn-view height="50.000000" width="100.000000" />,
188+
);
189+
190+
Fantom.runTask(() => {
191+
_setWidth(200);
192+
});
193+
194+
// TODO: this shouldn't be neccessary since animation should be stopped after duration
195+
Fantom.runTask(() => {
196+
_heightAnimation?.stop();
197+
});
198+
199+
// TODO: getFabricUpdateProps is not working with the cloneMutliple method
200+
// expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(50);
201+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
202+
<rn-view height="50.000000" width="200.000000" />,
203+
);
204+
});
205+
206+
test('animate layout props and rerender in many components', () => {
207+
const viewRef = createRef<HostInstance>();
208+
allowStyleProp('height');
209+
210+
let _animatedHeight;
211+
let _heightAnimation;
212+
let _setWidth;
213+
const N = 100;
214+
215+
function AnimatedComponent() {
216+
const animatedHeight = useAnimatedValue(0);
217+
218+
useEffect(() => {
219+
Animated.timing(animatedHeight, {
220+
toValue: 100,
221+
duration: 1000,
222+
useNativeDriver: true,
223+
}).start();
224+
});
225+
return (
226+
<Animated.View
227+
ref={viewRef}
228+
style={[
229+
{
230+
width: 100,
231+
height: animatedHeight,
232+
},
233+
]}
234+
/>
235+
);
236+
}
237+
238+
function MyApp() {
239+
const animatedHeight = useAnimatedValue(0);
240+
const [width, setWidth] = useState(100);
241+
_animatedHeight = animatedHeight;
242+
_setWidth = setWidth;
243+
return (
244+
<Animated.View
245+
ref={viewRef}
246+
style={[
247+
{
248+
width: width,
249+
height: animatedHeight,
250+
},
251+
]}>
252+
{Array.from({length: N}, (_, i) => (
253+
<AnimatedComponent key={i} />
254+
))}
255+
</Animated.View>
256+
);
257+
}
258+
259+
const root = Fantom.createRoot();
260+
261+
Fantom.runTask(() => {
262+
root.render(<MyApp />);
263+
});
264+
265+
Fantom.runTask(() => {
266+
_heightAnimation = Animated.timing(_animatedHeight, {
267+
toValue: 100,
268+
duration: 1000,
269+
useNativeDriver: true,
270+
}).start();
271+
});
272+
273+
Fantom.unstable_produceFramesForDuration(500);
274+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
275+
<rn-view height="50.000000" width="100.000000">
276+
{Array.from({length: N}, (_, i) => (
277+
<rn-view key={i} height="50.000000" width="100.000000" />
278+
))}
279+
</rn-view>,
280+
);
281+
282+
Fantom.runTask(() => {
283+
_setWidth(200);
284+
});
285+
286+
// TODO: this shouldn't be neccessary since animation should be stopped after duration
287+
Fantom.runTask(() => {
288+
_heightAnimation?.stop();
289+
});
290+
291+
// TODO: getFabricUpdateProps is not working with the cloneMutliple method
292+
// expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(50);
293+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
294+
<rn-view height="50.000000" width="200.000000">
295+
{Array.from({length: N}, (_, i) => (
296+
<rn-view key={i} height="50.000000" width="100.000000" />
297+
))}
298+
</rn-view>,
299+
);
300+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
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+
8+
#include "AnimatedPropsRegistry.h"
9+
#include <react/renderer/core/PropsParserContext.h>
10+
#include "AnimatedProps.h"
11+
12+
namespace facebook::react {
13+
14+
void AnimatedPropsRegistry::update(
15+
const std::unordered_map<SurfaceId, SurfaceUpdates>& surfaceUpdates) {
16+
auto lock = std::lock_guard(mutex_);
17+
for (const auto& [surfaceId, updates] : surfaceUpdates) {
18+
auto& surfaceContext = surfaceContexts_[surfaceId];
19+
auto& pendingMap = surfaceContext.pendingMap;
20+
auto& pendingFamilies = surfaceContext.pendingFamilies;
21+
22+
auto& updatesMap = updates.propsMap;
23+
auto& updatesFamilies = updates.families;
24+
25+
for (auto& family : updatesFamilies) {
26+
pendingFamilies.insert(family);
27+
}
28+
29+
for (auto& [tag, animatedProps] : updatesMap) {
30+
auto it = pendingMap.find(tag);
31+
if (it == pendingMap.end()) {
32+
it = pendingMap.insert_or_assign(tag, std::make_unique<PropsSnapshot>())
33+
.first;
34+
}
35+
auto& snapshot = it->second;
36+
auto& viewProps = snapshot->props;
37+
38+
for (const auto& animatedProp : animatedProps.props) {
39+
snapshot->propNames.insert(animatedProp->propName);
40+
cloneProp(viewProps, *animatedProp);
41+
}
42+
}
43+
}
44+
}
45+
46+
std::pair<std::unordered_set<const ShadowNodeFamily*>&, SnapshotMap&>
47+
AnimatedPropsRegistry::getMap(SurfaceId surfaceId) {
48+
auto lock = std::lock_guard(mutex_);
49+
auto& [pendingMap, map, pendingFamilies, families] =
50+
surfaceContexts_[surfaceId];
51+
52+
for (auto& family : pendingFamilies) {
53+
families.insert(family);
54+
}
55+
for (auto& [tag, propsSnapshot] : pendingMap) {
56+
auto currentIt = map.find(tag);
57+
if (currentIt == map.end()) {
58+
map.insert_or_assign(tag, std::move(propsSnapshot));
59+
} else {
60+
auto& currentSnapshot = currentIt->second;
61+
for (auto& propName : propsSnapshot->propNames) {
62+
currentSnapshot->propNames.insert(propName);
63+
updateProp(propName, currentSnapshot->props, *propsSnapshot);
64+
}
65+
}
66+
}
67+
pendingMap.clear();
68+
pendingFamilies.clear();
69+
70+
return {families, map};
71+
}
72+
73+
void AnimatedPropsRegistry::clear(SurfaceId surfaceId) {
74+
auto lock = std::lock_guard(mutex_);
75+
76+
auto& surfaceContext = surfaceContexts_[surfaceId];
77+
surfaceContext.families.clear();
78+
surfaceContext.map.clear();
79+
}
80+
81+
} // namespace facebook::react
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
8+
#pragma once
9+
10+
#include <folly/dynamic.h>
11+
#include <react/renderer/components/view/BaseViewProps.h>
12+
#include <react/renderer/core/ReactPrimitives.h>
13+
#include <react/renderer/uimanager/UIManager.h>
14+
#include <react/renderer/uimanager/UIManagerCommitHook.h>
15+
#include "AnimatedProps.h"
16+
17+
namespace facebook::react {
18+
19+
struct PropsSnapshot {
20+
BaseViewProps props;
21+
std::unordered_set<PropName> propNames;
22+
};
23+
24+
struct SurfaceContext {
25+
std::unordered_map<Tag, std::unique_ptr<PropsSnapshot>> pendingMap, map;
26+
std::unordered_set<const ShadowNodeFamily *> pendingFamilies, families;
27+
};
28+
29+
struct SurfaceUpdates {
30+
std::unordered_set<const ShadowNodeFamily *> families;
31+
std::unordered_map<Tag, AnimatedProps> propsMap;
32+
};
33+
34+
using SnapshotMap = std::unordered_map<Tag, std::unique_ptr<PropsSnapshot>>;
35+
36+
class AnimatedPropsRegistry {
37+
public:
38+
void update(const std::unordered_map<SurfaceId, SurfaceUpdates> &surfaceUpdates);
39+
void clear(SurfaceId surfaceId);
40+
std::pair<std::unordered_set<const ShadowNodeFamily *> &, SnapshotMap &> getMap(SurfaceId surfaceId);
41+
42+
private:
43+
std::unordered_map<SurfaceId, SurfaceContext> surfaceContexts_;
44+
std::mutex mutex_;
45+
};
46+
47+
inline void updateProp(const PropName propName, BaseViewProps &viewProps, const PropsSnapshot &snapshot)
48+
{
49+
switch (propName) {
50+
case OPACITY:
51+
viewProps.opacity = snapshot.props.opacity;
52+
break;
53+
54+
case WIDTH:
55+
viewProps.yogaStyle.setDimension(
56+
yoga::Dimension::Width, snapshot.props.yogaStyle.dimension(yoga::Dimension::Width));
57+
break;
58+
59+
case HEIGHT: {
60+
auto d = snapshot.props.yogaStyle.dimension(yoga::Dimension::Height);
61+
viewProps.yogaStyle.setDimension(yoga::Dimension::Height, d);
62+
break;
63+
}
64+
65+
case TRANSFORM:
66+
viewProps.transform = snapshot.props.transform;
67+
break;
68+
69+
case BORDER_RADII:
70+
viewProps.borderRadii = snapshot.props.borderRadii;
71+
break;
72+
73+
case FLEX:
74+
viewProps.yogaStyle.setFlex(snapshot.props.yogaStyle.flex());
75+
break;
76+
77+
case BACKGROUND_COLOR:
78+
viewProps.backgroundColor = snapshot.props.backgroundColor;
79+
break;
80+
}
81+
}
82+
83+
} // namespace facebook::react

0 commit comments

Comments
 (0)