Skip to content

Commit b38428c

Browse files
Bartlomiej Bloniarzmeta-codesync[bot]
authored andcommitted
Fix js sync on animation end in Animation Backend (#55566)
Summary: Pull Request resolved: #55566 In Animation Backend, we allow animation frameowrks to request a js-thread sync of the current animation state. It is used by c++ Animated, and is meant to serve as a way to push animation changes to react through RSNRU, after the animation finsishes. This way we ensure that subsequent rerenders of the component don't bring back the old style value. This approach is currently broken when the animation performs any main-thread commits, as in this case the `runtimeShadowNodeReference_` is not copied to new node revisions, so the js-thread sync commit cannot use RSNRU properly. The bug was not visible, because we don't clean up the registry in that case, we only do it for react commits. This PR fixes the issue for the case when `updateRuntimeShadowNodeReferencesOnCommitThread` is enabled, as this fixes the RSNRU propagation, so we can clean-up the registry. If the flag is disabled, we don't cleanup the registry, as we want the next react commit to make sure the animation state is not overwritten. # Changelog [General][Added] - test for the Animation Backend js sync [General][Changed] - TesterAnimationChoreographer changes the thread_local RSNRU flag when running animation update, to better simulate the real application use-case [General][Changed] - AnimationBackend now cleans-up the AnimatedPropsRegistry after the js sync when `updateRuntimeShadowNodeReferencesOnCommitThread` is enabled Reviewed By: zeyap Differential Revision: D93414839 fbshipit-source-id: 1c2566d117c2da563ae35e95cefc5ea08313fc7d
1 parent 44901aa commit b38428c

3 files changed

Lines changed: 131 additions & 20 deletions

File tree

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

Lines changed: 90 additions & 3 deletions
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
7+
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:*
88
* @flow strict-local
99
* @format
1010
*/
@@ -15,8 +15,8 @@ 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, useEffect, useState} from 'react';
19-
import {Animated, useAnimatedValue} from 'react-native';
18+
import {createRef, memo, useEffect, useMemo, useState} from 'react';
19+
import {Animated, View, 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';
2222

@@ -468,3 +468,90 @@ test('animate width, height and opacity at once', () => {
468468
root.getRenderedOutput({props: ['width', 'height', 'opacity']}).toJSX(),
469469
).toEqual(<rn-view height="200.000000" opacity="0.5" width="200.000000" />);
470470
});
471+
472+
test('animate width with memo and rerender (js sync test)', () => {
473+
const viewRef = createRef<HostInstance>();
474+
allowStyleProp('width');
475+
476+
let _widthAnimation;
477+
let _setState;
478+
479+
function useAnimation() {
480+
const animatedValue = useAnimatedValue(100);
481+
482+
useEffect(() => {
483+
const animation = Animated.timing(animatedValue, {
484+
toValue: 200,
485+
duration: 1000,
486+
useNativeDriver: true,
487+
});
488+
_widthAnimation = animation;
489+
animation.start();
490+
491+
return () => {
492+
animation.stop();
493+
};
494+
}, [animatedValue]);
495+
496+
return animatedValue;
497+
}
498+
499+
const AnimatedComponent = memo(() => {
500+
const animatedValue = useAnimation();
501+
502+
const animatedStyle = useMemo(() => {
503+
return {
504+
width: animatedValue,
505+
};
506+
}, [animatedValue]);
507+
508+
return (
509+
<Animated.View
510+
ref={viewRef}
511+
style={[{backgroundColor: 'green', height: 100}, animatedStyle]}
512+
/>
513+
);
514+
});
515+
516+
function MyApp() {
517+
const [state, setState] = useState(0);
518+
_setState = setState;
519+
520+
return (
521+
<>
522+
<View key={state} />
523+
<AnimatedComponent />
524+
</>
525+
);
526+
}
527+
528+
const root = Fantom.createRoot();
529+
530+
Fantom.runTask(() => {
531+
root.render(<MyApp />);
532+
});
533+
534+
expect(root.getRenderedOutput({props: ['width', 'height']}).toJSX()).toEqual(
535+
<rn-view height="100.000000" width="100.000000" />,
536+
);
537+
538+
Fantom.unstable_produceFramesForDuration(1000);
539+
540+
// TODO: this shouldn't be necessary since animation should be stopped after duration
541+
Fantom.runTask(() => {
542+
_widthAnimation?.stop();
543+
});
544+
545+
expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual(
546+
<rn-view width="200.000000" />,
547+
);
548+
549+
// Trigger rerender after animation completes to see if animation state gets overwritten
550+
Fantom.runTask(() => {
551+
_setState(s => 1 - s);
552+
});
553+
554+
expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual(
555+
<rn-view width="200.000000" />,
556+
);
557+
});

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

Lines changed: 38 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include "AnimatedPropsRegistry.h"
1010

1111
#include <react/debug/react_native_assert.h>
12+
#include <react/featureflags/ReactNativeFeatureFlags.h>
1213
#include <react/renderer/animationbackend/AnimatedPropsSerializer.h>
1314
#include <react/renderer/graphics/Color.h>
1415
#include <chrono>
@@ -152,8 +153,7 @@ void AnimationBackend::commitUpdates(
152153
return shadowNode.clone(
153154
{.props = newProps,
154155
.children = fragment.children,
155-
.state = shadowNode.getState(),
156-
.runtimeShadowNodeReference = false});
156+
.state = shadowNode.getState()});
157157
}));
158158
},
159159
{.mountSynchronously = true});
@@ -177,24 +177,45 @@ void AnimationBackend::requestAsyncFlushForSurfaces(
177177
react_native_assert(
178178
jsInvoker_ != nullptr ||
179179
surfaces.empty() && "jsInvoker_ was not provided");
180+
std::weak_ptr<AnimatedPropsRegistry> weakAnimatedPropsRegistry =
181+
animatedPropsRegistry_;
180182
for (const auto& surfaceId : surfaces) {
181183
// perform an empty commit on the js thread, to force the commit hook to
182184
// push updated shadow nodes to react through RSNRU
183-
jsInvoker_->invokeAsync([weakUIManager = uiManager_, surfaceId]() {
184-
auto uiManager = weakUIManager.lock();
185-
if (!uiManager) {
186-
return;
187-
}
188-
uiManager->getShadowTreeRegistry().visit(
189-
surfaceId, [](const ShadowTree& shadowTree) {
190-
shadowTree.commit(
191-
[](const RootShadowNode& oldRootShadowNode) {
192-
return std::static_pointer_cast<RootShadowNode>(
193-
oldRootShadowNode.ShadowNode::clone({}));
194-
},
195-
{.source = ShadowTreeCommitSource::AnimationEndSync});
196-
});
197-
});
185+
jsInvoker_->invokeAsync(
186+
[weakUIManager = uiManager_, surfaceId, weakAnimatedPropsRegistry]() {
187+
auto uiManager = weakUIManager.lock();
188+
if (!uiManager) {
189+
return;
190+
}
191+
uiManager->getShadowTreeRegistry().visit(
192+
surfaceId,
193+
[weakAnimatedPropsRegistry](const ShadowTree& shadowTree) {
194+
auto result = shadowTree.commit(
195+
[weakAnimatedPropsRegistry](
196+
const RootShadowNode& oldRootShadowNode) {
197+
return std::static_pointer_cast<RootShadowNode>(
198+
oldRootShadowNode.ShadowNode::clone({}));
199+
},
200+
{.source = ShadowTreeCommitSource::AnimationEndSync});
201+
// To clear the registry, the updates neeed to be propagated to
202+
// React with RSNRU. Without
203+
// updateRuntimeShadowNodeReferencesOnCommitThread this won't
204+
// happen if we do any commits on the main thread, since the
205+
// runtimeShadowNodeReference_ is not propagated to nodes cloned
206+
// outside of the JS thread. So when the flag is disabled we
207+
// keep the updates in the registry and we will reapply them in
208+
// a commit hook triggered by a rerender.
209+
if (result == ShadowTree::CommitStatus::Succeeded &&
210+
ReactNativeFeatureFlags::
211+
updateRuntimeShadowNodeReferencesOnCommitThread()) {
212+
if (auto animatedPropsRegistry =
213+
weakAnimatedPropsRegistry.lock()) {
214+
animatedPropsRegistry->clear(shadowTree.getSurfaceId());
215+
}
216+
}
217+
});
218+
});
198219
}
199220
}
200221

private/react-native-fantom/tester/src/TesterAnimationChoreographer.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
#include "TesterAnimationChoreographer.h"
99
#include <react/renderer/core/ReactPrimitives.h>
10+
#include <react/renderer/core/ShadowNode.h>
1011
#include <react/runtime/ReactInstanceConfig.h>
1112

1213
namespace facebook::react {
@@ -20,7 +21,9 @@ void TesterAnimationChoreographer::pause() {
2021

2122
void TesterAnimationChoreographer::runUITick(AnimationTimestamp timestamp) {
2223
if (!isPaused_) {
24+
ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(false);
2325
onAnimationFrame(timestamp);
26+
ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(true);
2427
}
2528
}
2629

0 commit comments

Comments
 (0)