From eecd07c58bc4740e7ad0eedf113efb006ae7efc3 Mon Sep 17 00:00:00 2001 From: Samuel Susla Date: Mon, 16 Jun 2025 04:50:47 -0700 Subject: [PATCH] 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 --- .../Animated/__tests__/Animated-itest.js | 55 ++++++++++++++++++- .../animated/NativeAnimatedNodesManager.cpp | 6 ++ 2 files changed, 59 insertions(+), 2 deletions(-) diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js b/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js index d263ea0cbbc..a53ad8b8100 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-itest.js @@ -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(); + + function MyApp() { + const opacity = useAnimatedValue(1); + _opacity = opacity; + return ( + + ); + } + + const root = Fantom.createRoot(); + + Fantom.runTask(() => { + root.render(); + }); + + 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( + , + ); +}); diff --git a/packages/react-native/ReactCxxPlatform/react/renderer/animated/NativeAnimatedNodesManager.cpp b/packages/react-native/ReactCxxPlatform/react/renderer/animated/NativeAnimatedNodesManager.cpp index d3f8dc105a8..4c5eb9b66ea 100644 --- a/packages/react-native/ReactCxxPlatform/react/renderer/animated/NativeAnimatedNodesManager.cpp +++ b/packages/react-native/ReactCxxPlatform/react/renderer/animated/NativeAnimatedNodesManager.cpp @@ -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(); }