Set final value via direct manipulation to fix animation glitches (#52012)

Summary:
Pull Request resolved: https://github.com/facebook/react-native/pull/52012

changelog: [internal]

On iOS, once a prop on a view is controlled by animated, the control is never released to Fabric or React. That's why it is important to use direct manipulation to commit even final value.

Reviewed By: lenaic

Differential Revision: D76601913

fbshipit-source-id: ea02219e158f28977018b34ac7152b899723b35a
This commit is contained in:
Samuel Susla
2025-06-16 04:50:47 -07:00
committed by Facebook GitHub Bot
parent 0e42d33cbc
commit eecd07c58b
2 changed files with 59 additions and 2 deletions
@@ -32,11 +32,9 @@ test('moving box by 100 points', () => {
{
width: 100,
height: 100,
backgroundColor: 'red',
},
{transform: [{translateX}]},
]}
testID="box"
/>
);
}
@@ -146,3 +144,56 @@ test('animation driven by onScroll event', () => {
// TODO(T226364699): this should `toBe(100)` but we are not syncing shadow tree yet.
expect(viewElement.getBoundingClientRect().y).toBe(0);
});
test('animated opacity', () => {
let _opacity;
const viewRef = createRef<HostInstance>();
function MyApp() {
const opacity = useAnimatedValue(1);
_opacity = opacity;
return (
<Animated.View
ref={viewRef}
style={[
{
width: 100,
height: 100,
opacity: opacity,
},
]}
/>
);
}
const root = Fantom.createRoot();
Fantom.runTask(() => {
root.render(<MyApp />);
});
const viewElement = ensureInstance(viewRef.current, ReactNativeElement);
expect(viewElement.getBoundingClientRect().x).toBe(0);
Fantom.runTask(() => {
Animated.timing(_opacity, {
toValue: 0,
duration: 30,
useNativeDriver: true,
}).start();
});
Fantom.unstable_produceFramesForDuration(30);
// $FlowFixMe[incompatible-use]
expect(Fantom.unstable_getDirectManipulationProps(viewElement).opacity).toBe(
0,
);
// TODO: this shouldn't be neccessary but C++ Animated still schedules a React state update.
Fantom.runWorkLoop();
expect(root.getRenderedOutput({props: ['opacity']}).toJSX()).toEqual(
<rn-view opacity="0" />,
);
});
@@ -756,6 +756,12 @@ bool NativeAnimatedNodesManager::commitProps() {
if (fabricCommitCallback_ != nullptr) {
if (!updateViewProps_.empty()) {
// Must call direct manipulation to set final values on components.
if (directManipulationCallback_ != nullptr) {
for (const auto& [viewTag, props] : updateViewProps_) {
directManipulationCallback_(viewTag, folly::dynamic(props));
}
}
fabricCommitCallback_(updateViewProps_);
updateViewProps_.clear();
}