Skip to content

Commit c516cab

Browse files
Bartlomiej Bloniarzfacebook-github-bot
authored andcommitted
Introduce AnimationBackendCommitHook (#54138)
Summary: 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. Differential Revision: D84250600
1 parent 2eaadb4 commit c516cab

9 files changed

Lines changed: 465 additions & 19 deletions

File tree

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';
@@ -125,3 +125,160 @@ test('animate layout props', () => {
125125
<rn-view height="100.000000" />,
126126
);
127127
});
128+
129+
test('animate layout props and rerender', () => {
130+
const viewRef = createRef<HostInstance>();
131+
allowStyleProp('height');
132+
133+
let _animatedHeight;
134+
let _heightAnimation;
135+
let _setWidth;
136+
137+
function MyApp() {
138+
const animatedHeight = useAnimatedValue(0);
139+
const [width, setWidth] = useState(100);
140+
_animatedHeight = animatedHeight;
141+
_setWidth = setWidth;
142+
return (
143+
<Animated.View
144+
ref={viewRef}
145+
style={[
146+
{
147+
width: width,
148+
height: animatedHeight,
149+
},
150+
]}
151+
/>
152+
);
153+
}
154+
155+
const root = Fantom.createRoot();
156+
157+
Fantom.runTask(() => {
158+
root.render(<MyApp />);
159+
});
160+
161+
Fantom.runTask(() => {
162+
_heightAnimation = Animated.timing(_animatedHeight, {
163+
toValue: 100,
164+
duration: 1000,
165+
useNativeDriver: true,
166+
}).start();
167+
});
168+
169+
Fantom.unstable_produceFramesForDuration(500);
170+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
171+
<rn-view height="50.000000" width="100.000000" />,
172+
);
173+
174+
Fantom.runTask(() => {
175+
_setWidth(200);
176+
});
177+
178+
// TODO: this shouldn't be neccessary since animation should be stopped after duration
179+
Fantom.runTask(() => {
180+
_heightAnimation?.stop();
181+
});
182+
183+
// TODO: getFabricUpdateProps is not working with the cloneMutliple method
184+
// expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(50);
185+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
186+
<rn-view height="50.000000" width="200.000000" />,
187+
);
188+
});
189+
190+
test('animate layout props and rerender in many components', () => {
191+
const viewRef = createRef<HostInstance>();
192+
allowStyleProp('height');
193+
194+
let _animatedHeight;
195+
let _heightAnimation;
196+
let _setWidth;
197+
const N = 100;
198+
199+
function AnimatedComponent() {
200+
const animatedHeight = useAnimatedValue(0);
201+
202+
useEffect(() => {
203+
Animated.timing(animatedHeight, {
204+
toValue: 100,
205+
duration: 1000,
206+
useNativeDriver: true,
207+
}).start();
208+
});
209+
return (
210+
<Animated.View
211+
ref={viewRef}
212+
style={[
213+
{
214+
width: 100,
215+
height: animatedHeight,
216+
},
217+
]}
218+
/>
219+
);
220+
}
221+
222+
function MyApp() {
223+
const animatedHeight = useAnimatedValue(0);
224+
const [width, setWidth] = useState(100);
225+
_animatedHeight = animatedHeight;
226+
_setWidth = setWidth;
227+
return (
228+
<Animated.View
229+
ref={viewRef}
230+
style={[
231+
{
232+
width: width,
233+
height: animatedHeight,
234+
},
235+
]}>
236+
{Array.from({length: N}, (_, i) => (
237+
<AnimatedComponent key={i} />
238+
))}
239+
</Animated.View>
240+
);
241+
}
242+
243+
const root = Fantom.createRoot();
244+
245+
Fantom.runTask(() => {
246+
root.render(<MyApp />);
247+
});
248+
249+
Fantom.runTask(() => {
250+
_heightAnimation = Animated.timing(_animatedHeight, {
251+
toValue: 100,
252+
duration: 1000,
253+
useNativeDriver: true,
254+
}).start();
255+
});
256+
257+
Fantom.unstable_produceFramesForDuration(500);
258+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
259+
<rn-view height="50.000000" width="100.000000">
260+
{Array.from({length: N}, (_, i) => (
261+
<rn-view key={i} height="50.000000" width="100.000000" />
262+
))}
263+
</rn-view>,
264+
);
265+
266+
Fantom.runTask(() => {
267+
_setWidth(200);
268+
});
269+
270+
// TODO: this shouldn't be neccessary since animation should be stopped after duration
271+
Fantom.runTask(() => {
272+
_heightAnimation?.stop();
273+
});
274+
275+
// TODO: getFabricUpdateProps is not working with the cloneMutliple method
276+
// expect(Fantom.unstable_getFabricUpdateProps(viewElement).height).toBe(50);
277+
expect(root.getRenderedOutput({props: ['height', 'width']}).toJSX()).toEqual(
278+
<rn-view height="50.000000" width="200.000000">
279+
{Array.from({length: N}, (_, i) => (
280+
<rn-view key={i} height="50.000000" width="100.000000" />
281+
))}
282+
</rn-view>,
283+
);
284+
});
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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
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+
}
78+
79+
} // namespace facebook::react

0 commit comments

Comments
 (0)