From 887678fd4b50ed64cbcc040fe6c99802ae35e5af Mon Sep 17 00:00:00 2001 From: Tim Yung Date: Sat, 28 Jun 2025 12:14:09 -0700 Subject: [PATCH] Animated: Trigger Callback on Stopping Native Loops (#52274) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/52274 Currently, `callback` in `Animated.loop(animation).start(callback)` does not get triggered when calling `stop()` if `animation` uses native driver. This diff implements that functionality. Changelog: [General][Changed] - Stopping an `Animated.loop` for a native animation will now correctly trigger completion callbacks supplied to `start()`. Reviewed By: javache Differential Revision: D77329013 --- .../Animated/AnimatedImplementation.js | 16 ++++++------ .../Libraries/Animated/AnimatedMock.js | 2 +- .../Animated/__tests__/Animated-test.js | 25 +++++++++++++++++++ 3 files changed, 34 insertions(+), 9 deletions(-) diff --git a/packages/react-native/Libraries/Animated/AnimatedImplementation.js b/packages/react-native/Libraries/Animated/AnimatedImplementation.js index cdff0a30480..88152fa7565 100644 --- a/packages/react-native/Libraries/Animated/AnimatedImplementation.js +++ b/packages/react-native/Libraries/Animated/AnimatedImplementation.js @@ -42,7 +42,7 @@ export type CompositeAnimation = { start: (callback?: ?EndCallback, isLooping?: boolean) => void, stop: () => void, reset: () => void, - _startNativeLoop: (iterations?: number) => void, + _startNativeLoop: (iterations: number, callback: ?EndCallback) => void, _isUsingNativeDriver: () => boolean, ... }; @@ -192,9 +192,9 @@ const springImpl = function ( value.resetAnimation(); }, - _startNativeLoop: function (iterations?: number): void { + _startNativeLoop(iterations: number, callback: ?EndCallback): void { const singleConfig = {...config, iterations}; - start(value, singleConfig); + start(value, singleConfig, callback); }, _isUsingNativeDriver: function (): boolean { @@ -246,9 +246,9 @@ const timingImpl = function ( value.resetAnimation(); }, - _startNativeLoop: function (iterations?: number): void { + _startNativeLoop(iterations: number, callback: ?EndCallback): void { const singleConfig = {...config, iterations}; - start(value, singleConfig); + start(value, singleConfig, callback); }, _isUsingNativeDriver: function (): boolean { @@ -288,9 +288,9 @@ const decayImpl = function ( value.resetAnimation(); }, - _startNativeLoop: function (iterations?: number): void { + _startNativeLoop(iterations: number, callback: ?EndCallback): void { const singleConfig = {...config, iterations}; - start(value, singleConfig); + start(value, singleConfig, callback); }, _isUsingNativeDriver: function (): boolean { @@ -484,7 +484,7 @@ const loopImpl = function ( callback && callback({finished: true}); } else { if (animation._isUsingNativeDriver()) { - animation._startNativeLoop(iterations); + animation._startNativeLoop(iterations, callback); } else { restart(); // Start looping recursively on the js thread } diff --git a/packages/react-native/Libraries/Animated/AnimatedMock.js b/packages/react-native/Libraries/Animated/AnimatedMock.js index 87338979f96..0257dc687dd 100644 --- a/packages/react-native/Libraries/Animated/AnimatedMock.js +++ b/packages/react-native/Libraries/Animated/AnimatedMock.js @@ -65,7 +65,7 @@ export type CompositeAnimation = { start: (callback?: ?EndCallback) => void, stop: () => void, reset: () => void, - _startNativeLoop: (iterations?: number) => void, + _startNativeLoop: (iterations: number, callback: ?EndCallback) => void, _isUsingNativeDriver: () => boolean, ... }; diff --git a/packages/react-native/Libraries/Animated/__tests__/Animated-test.js b/packages/react-native/Libraries/Animated/__tests__/Animated-test.js index 53cb752e58c..73f9d5e81a7 100644 --- a/packages/react-native/Libraries/Animated/__tests__/Animated-test.js +++ b/packages/react-native/Libraries/Animated/__tests__/Animated-test.js @@ -683,6 +683,31 @@ describe('Animated', () => { expect(animation.reset).toHaveBeenCalledTimes(1); expect(cb).toBeCalledWith({finished: false}); }); + + it('stops looping native animations', () => { + const value = new Animated.Value(0); + const animation = Animated.timing(value, { + toValue: 1, + useNativeDriver: true, + }); + + jest.spyOn(animation, '_startNativeLoop'); + jest.spyOn(animation, 'stop'); + + const callback = jest.fn(); + + const loop = Animated.loop(animation); + loop.start(callback); + + expect(animation._startNativeLoop).toBeCalledTimes(1); + expect(animation.stop).not.toBeCalled(); + expect(callback).not.toBeCalled(); + + loop.stop(); + + expect(animation.stop).toBeCalled(); + expect(callback).toBeCalledWith({finished: false}); + }); }); it('does not reset animation in a loop if resetBeforeIteration is false', () => {