Skip to content

Commit 5e636ba

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
Keep non-layout animations on the synchronous path while another view animates layout (#58772)
Summary: Pull Request resolved: #58772 The shared animation backend decided per surface whether a frame's animated props go through a shadow tree commit or straight to the mounted views. As soon as one view animated a layout prop, every animated view on that surface went through the commit path for the whole animation, including views that only animate `transform` or `opacity`. Besides the extra commit work, on Android this moved those views from `updatePropsSynchronously` to regular mounts. There the synchronous mount props cache (`overrideBySynchronousMountPropsAtMountingAndroid`) replaced the incoming transform with its last synchronously written value, so the view froze while the other view's layout animation ran and jumped when it ended. The decision is now made per view. `SurfaceUpdates` holds a surface's mutations keyed by view tag, and `applySurfaceUpdates` splits them into views with layout updates, which go through `commitUpdates`, and the rest, which are applied synchronously. `AnimationMutation` and `AnimationMutations` move to `AnimationMutation.h`, still included by `AnimationBackend.h`. `AnimatedPropsRegistry::update` reads the frame's batches instead of the merged per-surface map. When the same view receives several mutations in one frame, they are merged, with later values winning, so the mounted view and the registry stay in sync. ## Changelog: [General] [Fixed] - A view animating only non-layout props on the shared animation backend no longer freezes while another view on the surface animates a layout prop Differential Revision: D122570616
1 parent c1401a1 commit 5e636ba

16 files changed

Lines changed: 213 additions & 139 deletions

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

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,69 @@ test('animate marginLeft layout prop', () => {
7777
);
7878
});
7979

80+
test('non-layout props stay on the direct path while another view animates layout', () => {
81+
const movingRef = createRef<HostInstance>();
82+
83+
let _translateX;
84+
let _translateXAnimation;
85+
let _siblingHeight;
86+
let _siblingHeightAnimation;
87+
88+
function MyApp() {
89+
const translateX = useAnimatedValue(0);
90+
const siblingHeight = useAnimatedValue(10);
91+
_translateX = translateX;
92+
_siblingHeight = siblingHeight;
93+
return (
94+
<View collapsable={false}>
95+
<Animated.View
96+
ref={movingRef}
97+
style={{width: 100, height: 100, transform: [{translateX}]}}
98+
/>
99+
<Animated.View style={{width: 100, height: siblingHeight}} />
100+
</View>
101+
);
102+
}
103+
104+
const root = Fantom.createRoot();
105+
106+
Fantom.runTask(() => {
107+
root.render(<MyApp />);
108+
});
109+
110+
Fantom.runTask(() => {
111+
_translateXAnimation = Animated.timing(_translateX, {
112+
toValue: 100,
113+
duration: 200,
114+
useNativeDriver: true,
115+
}).start();
116+
_siblingHeightAnimation = Animated.timing(_siblingHeight, {
117+
toValue: 110,
118+
duration: 200,
119+
useNativeDriver: true,
120+
}).start();
121+
});
122+
123+
Fantom.unstable_produceFramesForDuration(100);
124+
125+
// The sibling's height went through a commit; the transform did not.
126+
expect(root.getRenderedOutput({props: ['height']}).toJSX()).toEqual(
127+
<rn-view>
128+
<rn-view key={0} height="100" />
129+
<rn-view key={1} height="60" />
130+
</rn-view>,
131+
);
132+
expect(
133+
Fantom.unstable_getDirectManipulationProps(nullthrows(movingRef.current))
134+
.transform,
135+
).toEqual([{translateX: 50}]);
136+
137+
Fantom.runTask(() => {
138+
_translateXAnimation?.stop();
139+
_siblingHeightAnimation?.stop();
140+
});
141+
});
142+
80143
test('animated opacity', () => {
81144
let _opacity;
82145
let _opacityAnimation;

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.cpp‎

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,28 @@
66
*/
77

88
#include "AnimatedPropsRegistry.h"
9+
#include <react/debug/react_native_assert.h>
910
#include <react/renderer/core/PropsParserContext.h>
1011
#include "AnimatedProps.h"
1112

1213
namespace facebook::react {
1314

1415
void AnimatedPropsRegistry::update(
15-
const std::unordered_map<SurfaceId, SurfaceUpdates>& surfaceUpdates) {
16+
const std::vector<AnimationMutations>& batches) {
1617
auto lock = std::lock_guard(mutex_);
17-
for (const auto& [surfaceId, updates] : surfaceUpdates) {
18-
auto contextIt = surfaceContexts_.find(surfaceId);
19-
if (contextIt == surfaceContexts_.end()) {
20-
continue;
21-
}
22-
auto& surfaceContext = contextIt->second;
23-
auto& pendingMap = surfaceContext.pendingMap;
24-
auto& pendingFamilies = surfaceContext.pendingFamilies;
25-
26-
auto& updatesMap = updates.propsMap;
27-
auto& updatesFamilies = updates.families;
28-
29-
for (auto& family : updatesFamilies) {
30-
pendingFamilies.insert(family);
31-
}
32-
33-
for (auto& [tag, animatedProps] : updatesMap) {
18+
for (const auto& mutations : batches) {
19+
for (const auto& mutation : mutations.batch) {
20+
const auto& family = mutation.family;
21+
react_native_assert(family != nullptr);
22+
auto contextIt = surfaceContexts_.find(family->getSurfaceId());
23+
if (contextIt == surfaceContexts_.end()) {
24+
continue;
25+
}
26+
auto& surfaceContext = contextIt->second;
27+
auto& pendingMap = surfaceContext.pendingMap;
28+
surfaceContext.pendingFamilies.insert(family);
29+
const auto tag = mutation.tag;
30+
const auto& animatedProps = mutation.props;
3431
auto it = pendingMap.find(tag);
3532
if (it == pendingMap.end()) {
3633
it = pendingMap.insert_or_assign(tag, std::make_unique<PropsSnapshot>())

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimatedPropsRegistry.h‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <react/renderer/uimanager/UIManager.h>
1616
#include <react/renderer/uimanager/UIManagerCommitHook.h>
1717
#include "AnimatedProps.h"
18+
#include "AnimationMutation.h"
1819

1920
namespace facebook::react {
2021

@@ -29,17 +30,11 @@ struct SurfaceContext {
2930
std::unordered_set<std::shared_ptr<const ShadowNodeFamily>> pendingFamilies, families;
3031
};
3132

32-
struct SurfaceUpdates {
33-
std::unordered_set<std::shared_ptr<const ShadowNodeFamily>> families;
34-
std::unordered_map<Tag, AnimatedProps> propsMap;
35-
bool hasLayoutUpdates{false};
36-
};
37-
3833
using SnapshotMap = std::unordered_map<Tag, std::unique_ptr<PropsSnapshot>>;
3934

4035
class AnimatedPropsRegistry {
4136
public:
42-
void update(const std::unordered_map<SurfaceId, SurfaceUpdates> &surfaceUpdates);
37+
void update(const std::vector<AnimationMutations> &batches);
4338
void initializeSurface(SurfaceId surfaceId);
4439
void clear(SurfaceId surfaceId);
4540
void clearOnSurfaceStop(SurfaceId surfaceId);

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.cpp‎

Lines changed: 71 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <react/featureflags/ReactNativeFeatureFlags.h>
1313
#include <react/renderer/animationbackend/AnimatedPropsSerializer.h>
1414
#include <react/renderer/graphics/Color.h>
15+
#include <algorithm>
1516
#include <chrono>
1617
#include <utility>
1718

@@ -48,6 +49,26 @@ static inline Props::Shared cloneProps(
4849
return newProps;
4950
}
5051

52+
// Applies `incoming` on top of `existing` for the same view, later values win.
53+
static void mergeMutation(
54+
AnimationMutation& existing,
55+
AnimationMutation&& incoming) {
56+
auto& props = existing.props;
57+
for (auto& animatedProp : incoming.props.props) {
58+
props.props.push_back(std::move(animatedProp));
59+
}
60+
if (incoming.props.rawProps) {
61+
if (props.rawProps) {
62+
auto merged = props.rawProps->toDynamic();
63+
merged.merge_patch(incoming.props.rawProps->toDynamic());
64+
props.rawProps = std::make_unique<RawProps>(std::move(merged));
65+
} else {
66+
props.rawProps = std::move(incoming.props.rawProps);
67+
}
68+
}
69+
existing.hasLayoutUpdates |= incoming.hasLayoutUpdates;
70+
}
71+
5172
AnimationBackend::AnimationBackend(
5273
std::shared_ptr<AnimationChoreographer> animationChoreographer,
5374
std::shared_ptr<UIManager> uiManager)
@@ -80,14 +101,13 @@ void AnimationBackend::unpackMutations(
80101
std::unordered_map<SurfaceId, SurfaceUpdates>& surfaceUpdates,
81102
std::set<SurfaceId>& asyncFlushSurfaces) {
82103
for (auto& mutation : mutations.batch) {
83-
const auto family = mutation.family;
84-
react_native_assert(family != nullptr);
85-
86-
auto& [families, updates, hasLayoutUpdates] =
87-
surfaceUpdates[family->getSurfaceId()];
88-
hasLayoutUpdates |= mutation.hasLayoutUpdates;
89-
families.insert(family);
90-
updates[mutation.tag] = std::move(mutation.props);
104+
auto& updates = surfaceUpdates[mutation.family->getSurfaceId()];
105+
const auto tag = mutation.tag;
106+
if (auto it = updates.find(tag); it != updates.end()) {
107+
mergeMutation(it->second, std::move(mutation));
108+
} else {
109+
updates.emplace(tag, std::move(mutation));
110+
}
91111
}
92112

93113
asyncFlushSurfaces.merge(mutations.asyncFlushSurfaces);
@@ -96,23 +116,34 @@ void AnimationBackend::unpackMutations(
96116
void AnimationBackend::applySurfaceUpdates(
97117
std::unordered_map<SurfaceId, SurfaceUpdates>& surfaceUpdates,
98118
const std::set<SurfaceId>& asyncFlushSurfaces) {
99-
animatedPropsRegistry_->update(surfaceUpdates);
100-
101119
for (auto& [surfaceId, updates] : surfaceUpdates) {
102-
if (updates.hasLayoutUpdates) {
103-
commitUpdates(surfaceId, updates);
104-
} else {
105-
synchronouslyUpdateProps(updates.propsMap);
120+
SurfaceUpdates layoutUpdates;
121+
std::unordered_map<Tag, AnimatedProps> directProps;
122+
for (auto& [tag, mutation] : updates) {
123+
if (mutation.hasLayoutUpdates) {
124+
layoutUpdates.emplace(tag, std::move(mutation));
125+
} else {
126+
directProps.emplace(tag, std::move(mutation.props));
127+
}
128+
}
129+
if (!layoutUpdates.empty()) {
130+
commitUpdates(surfaceId, layoutUpdates);
131+
}
132+
if (!directProps.empty()) {
133+
synchronouslyUpdateProps(directProps);
106134
}
107135
}
108136

109137
requestAsyncFlushForSurfaces(asyncFlushSurfaces);
110138
}
111139

112-
void AnimationBackend::applyMutations(AnimationMutations mutations) {
140+
void AnimationBackend::applyMutations(std::vector<AnimationMutations> batches) {
141+
animatedPropsRegistry_->update(batches);
113142
std::unordered_map<SurfaceId, SurfaceUpdates> surfaceUpdates;
114143
std::set<SurfaceId> asyncFlushSurfaces;
115-
unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces);
144+
for (auto& mutations : batches) {
145+
unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces);
146+
}
116147
applySurfaceUpdates(surfaceUpdates, asyncFlushSurfaces);
117148
}
118149

@@ -124,13 +155,17 @@ void AnimationBackend::onAnimationFrame(AnimationTimestamp timestamp) {
124155
callbacksCopy = callbacks;
125156
}
126157

127-
std::unordered_map<SurfaceId, SurfaceUpdates> surfaceUpdates;
128-
std::set<SurfaceId> asyncFlushSurfaces;
129-
for (auto& callbackWithId : callbacksCopy) {
130-
auto mutations = callbackWithId.callback(timestamp);
131-
unpackMutations(mutations, surfaceUpdates, asyncFlushSurfaces);
132-
}
133-
applySurfaceUpdates(surfaceUpdates, asyncFlushSurfaces);
158+
// Sized up front rather than grown: MSVC's std::set move isn't noexcept, so
159+
// growing a vector of AnimationMutations would try to copy move-only props.
160+
std::vector<AnimationMutations> batches(callbacksCopy.size());
161+
std::transform(
162+
callbacksCopy.begin(),
163+
callbacksCopy.end(),
164+
batches.begin(),
165+
[timestamp](const CallbackWithId& callbackWithId) {
166+
return callbackWithId.callback(timestamp);
167+
});
168+
applyMutations(std::move(batches));
134169
}
135170

136171
CallbackId AnimationBackend::start(const Callback& callback) {
@@ -169,8 +204,9 @@ void AnimationBackend::trigger() {
169204

170205
void AnimationBackend::pushAnimationMutations(const Callback& callback) {
171206
auto timestamp = animationChoreographer_->now();
172-
auto mutations = callback(timestamp);
173-
applyMutations(std::move(mutations));
207+
std::vector<AnimationMutations> batches(1);
208+
batches[0] = callback(timestamp);
209+
applyMutations(std::move(batches));
174210
}
175211

176212
void AnimationBackend::commitUpdates(
@@ -181,24 +217,28 @@ void AnimationBackend::commitUpdates(
181217
return;
182218
}
183219

184-
auto& surfaceFamilies = surfaceUpdates.families;
185-
auto& updates = surfaceUpdates.propsMap;
220+
std::unordered_set<std::shared_ptr<const ShadowNodeFamily>> surfaceFamilies;
221+
for (const auto& [tag, mutation] : surfaceUpdates) {
222+
surfaceFamilies.insert(mutation.family);
223+
}
186224

187225
uiManager->getShadowTreeRegistry().visit(
188-
surfaceId, [&surfaceFamilies, &updates](const ShadowTree& shadowTree) {
226+
surfaceId,
227+
[&surfaceFamilies, &surfaceUpdates](const ShadowTree& shadowTree) {
189228
shadowTree.commit(
190229
[&surfaceFamilies,
191-
&updates](const RootShadowNode& oldRootShadowNode) {
230+
&surfaceUpdates](const RootShadowNode& oldRootShadowNode) {
192231
return std::static_pointer_cast<RootShadowNode>(
193232
oldRootShadowNode.cloneMultiple(
194233
surfaceFamilies,
195-
[&surfaceFamilies, &updates](
234+
[&surfaceFamilies, &surfaceUpdates](
196235
const ShadowNode& shadowNode,
197236
const ShadowNodeFragment& fragment) {
198237
auto newProps = ShadowNodeFragment::propsPlaceholder();
199238
if (surfaceFamilies.contains(
200239
shadowNode.getFamilyShared())) {
201-
auto& animatedProps = updates.at(shadowNode.getTag());
240+
auto& animatedProps =
241+
surfaceUpdates.at(shadowNode.getTag()).props;
202242
newProps = cloneProps(animatedProps, shadowNode);
203243
}
204244
return shadowNode.clone(

‎packages/react-native/ReactCommon/react/renderer/animationbackend/AnimationBackend.h‎

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -21,22 +21,13 @@
2121
#include "AnimatedPropsRegistry.h"
2222
#include "AnimationBackendCommitHook.h"
2323
#include "AnimationChoreographer.h"
24+
#include "AnimationMutation.h"
2425

2526
namespace facebook::react {
2627

2728
class AnimationBackend;
2829

29-
struct AnimationMutation {
30-
Tag tag;
31-
std::shared_ptr<const ShadowNodeFamily> family;
32-
AnimatedProps props;
33-
bool hasLayoutUpdates{false};
34-
};
35-
36-
struct AnimationMutations {
37-
std::vector<AnimationMutation> batch;
38-
std::set<SurfaceId> asyncFlushSurfaces;
39-
};
30+
using SurfaceUpdates = std::unordered_map<Tag, AnimationMutation>;
4031

4132
using Callback = std::function<AnimationMutations(AnimationTimestamp)>;
4233

@@ -74,7 +65,7 @@ class AnimationBackend : public UIManagerAnimationBackend {
7465
void applySurfaceUpdates(
7566
std::unordered_map<SurfaceId, SurfaceUpdates> &surfaceUpdates,
7667
const std::set<SurfaceId> &asyncFlushSurfaces);
77-
void applyMutations(AnimationMutations mutations);
68+
void applyMutations(std::vector<AnimationMutations> batches);
7869
std::vector<CallbackWithId> callbacks;
7970
std::shared_ptr<AnimatedPropsRegistry> animatedPropsRegistry_;
8071
std::shared_ptr<AnimationChoreographer> animationChoreographer_;
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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 <react/cxxstableapi/FrameworksGuard.h>
11+
12+
#include <React/RendererCore.h>
13+
#include <memory>
14+
#include <set>
15+
#include <vector>
16+
#include "AnimatedProps.h"
17+
18+
namespace facebook::react {
19+
20+
struct AnimationMutation {
21+
Tag tag;
22+
std::shared_ptr<const ShadowNodeFamily> family;
23+
AnimatedProps props;
24+
bool hasLayoutUpdates{false};
25+
};
26+
27+
struct AnimationMutations {
28+
std::vector<AnimationMutation> batch;
29+
std::set<SurfaceId> asyncFlushSurfaces;
30+
};
31+
32+
} // namespace facebook::react

0 commit comments

Comments
 (0)