Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @fantom_flags useSharedAnimatedBackend:true
* @fantom_flags useSharedAnimatedBackend:true updateRuntimeShadowNodeReferencesOnCommitThread:*
* @flow strict-local
* @format
*/
Expand All @@ -15,8 +15,8 @@ import type {HostInstance} from 'react-native';

import ensureInstance from '../../../src/private/__tests__/utilities/ensureInstance';
import * as Fantom from '@react-native/fantom';
import {createRef, useEffect, useState} from 'react';
import {Animated, useAnimatedValue} from 'react-native';
import {createRef, memo, useEffect, useMemo, useState} from 'react';
import {Animated, View, useAnimatedValue} from 'react-native';
import {allowStyleProp} from 'react-native/Libraries/Animated/NativeAnimatedAllowlist';
import ReactNativeElement from 'react-native/src/private/webapis/dom/nodes/ReactNativeElement';

Expand Down Expand Up @@ -468,3 +468,97 @@ test('animate width, height and opacity at once', () => {
root.getRenderedOutput({props: ['width', 'height', 'opacity']}).toJSX(),
).toEqual(<rn-view height="200.000000" opacity="0.5" width="200.000000" />);
});

test('animate width with memo and rerender (js sync test)', () => {
const viewRef = createRef<HostInstance>();
allowStyleProp('width');

let _widthAnimation;
let _setState;

function useAnimation() {
const animatedValue = useAnimatedValue(100);

useEffect(() => {
const animation = Animated.timing(animatedValue, {
toValue: 200,
duration: 1000,
useNativeDriver: true,
});
_widthAnimation = animation;
animation.start();

return () => {
animation.stop();
};
}, [animatedValue]);

return animatedValue;
}

const AnimatedComponent = memo(() => {
const animatedValue = useAnimation();

const animatedStyle = useMemo(() => {
return {
width: animatedValue,
};
}, [animatedValue]);

return (
<Animated.View
ref={viewRef}
style={[{backgroundColor: 'green', height: 100}, animatedStyle]}
/>
);
});

function MyApp() {
const [state, setState] = useState(0);
_setState = setState;

return (
<>
<View key={state} />
<AnimatedComponent />
</>
);
}

const root = Fantom.createRoot();

Fantom.runTask(() => {
root.render(<MyApp />);
});

// Animation should have started, check initial state
expect(root.getRenderedOutput({props: ['width', 'height']}).toJSX()).toEqual(
<rn-view height="100.000000" width="100.000000" />,
);

// Advance animation halfway (500ms of 1000ms)
Fantom.unstable_produceFramesForDuration(1000);

// TODO: this shouldn't be necessary since animation should be stopped after duration
Fantom.runTask(() => {
// _widthAnimation?.stop();
});

Fantom.runTask(() => {
_widthAnimation?.stop();
});

expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual(
<rn-view width="200.000000" />,
);

// Trigger another rerender after animation completes
Fantom.runTask(() => {
_setState(s => 1 - s);
});

// Width should remain at 200 after rerender
expect(root.getRenderedOutput({props: ['width']}).toJSX()).toEqual(
<rn-view width="200.000000" />,
);
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#include "AnimatedPropsRegistry.h"

#include <react/debug/react_native_assert.h>
#include <react/featureflags/ReactNativeFeatureFlags.h>
#include <react/renderer/animationbackend/AnimatedPropsSerializer.h>
#include <react/renderer/graphics/Color.h>
#include <chrono>
Expand Down Expand Up @@ -152,8 +153,7 @@ void AnimationBackend::commitUpdates(
return shadowNode.clone(
{.props = newProps,
.children = fragment.children,
.state = shadowNode.getState(),
.runtimeShadowNodeReference = false});
.state = shadowNode.getState()});
}));
},
{.mountSynchronously = true});
Expand All @@ -177,24 +177,45 @@ void AnimationBackend::requestAsyncFlushForSurfaces(
react_native_assert(
jsInvoker_ != nullptr ||
surfaces.empty() && "jsInvoker_ was not provided");
std::weak_ptr<AnimatedPropsRegistry> weakAnimatedPropsRegistry =
animatedPropsRegistry_;
for (const auto& surfaceId : surfaces) {
// perform an empty commit on the js thread, to force the commit hook to
// push updated shadow nodes to react through RSNRU
jsInvoker_->invokeAsync([weakUIManager = uiManager_, surfaceId]() {
auto uiManager = weakUIManager.lock();
if (!uiManager) {
return;
}
uiManager->getShadowTreeRegistry().visit(
surfaceId, [](const ShadowTree& shadowTree) {
shadowTree.commit(
[](const RootShadowNode& oldRootShadowNode) {
return std::static_pointer_cast<RootShadowNode>(
oldRootShadowNode.ShadowNode::clone({}));
},
{.source = ShadowTreeCommitSource::AnimationEndSync});
});
});
jsInvoker_->invokeAsync(
[weakUIManager = uiManager_, surfaceId, weakAnimatedPropsRegistry]() {
auto uiManager = weakUIManager.lock();
if (!uiManager) {
return;
}
uiManager->getShadowTreeRegistry().visit(
surfaceId,
[weakAnimatedPropsRegistry](const ShadowTree& shadowTree) {
auto result = shadowTree.commit(
[weakAnimatedPropsRegistry](
const RootShadowNode& oldRootShadowNode) {
return std::static_pointer_cast<RootShadowNode>(
oldRootShadowNode.ShadowNode::clone({}));
},
{.source = ShadowTreeCommitSource::AnimationEndSync});
// To clear the registry, the updates neeed to be propagated to
// React with RSNRU. Without
// updateRuntimeShadowNodeReferencesOnCommitThread this won't
// happen if we do any commits on the main thread, since the
// runtimeShadowNodeReference_ is not propagated to nodes cloned
// outside of the JS thread. So when the flag is disabled we
// keep the updates in the registry and we will reapply them in
// a commit hook triggered by a rerender.
if (result == ShadowTree::CommitStatus::Succeeded &&
ReactNativeFeatureFlags::
updateRuntimeShadowNodeReferencesOnCommitThread()) {
if (auto animatedPropsRegistry =
weakAnimatedPropsRegistry.lock()) {
animatedPropsRegistry->clear(shadowTree.getSurfaceId());
}
}
});
});
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

#include "TesterAnimationChoreographer.h"
#include <react/renderer/core/ReactPrimitives.h>
#include <react/renderer/core/ShadowNode.h>
#include <react/runtime/ReactInstanceConfig.h>

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

void TesterAnimationChoreographer::runUITick(AnimationTimestamp timestamp) {
if (!isPaused_) {
ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(false);
onAnimationFrame(timestamp);
ShadowNode::setUseRuntimeShadowNodeReferenceUpdateOnThread(true);
}
}

Expand Down
Loading