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
11 changes: 9 additions & 2 deletions packages/react-native/Libraries/Animated/animations/Animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,9 @@ export default class Animation {
_useNativeDriver: boolean;

__active: boolean;
// Set when the value is explicitly assigned while this animation is running,
// so that its (asynchronous) native end result does not overwrite that value.
__isValueOverridden: boolean = false;
__isInteraction: boolean;
__isLooping: ?boolean;
__iterations: number;
Expand Down Expand Up @@ -148,14 +151,18 @@ export default class Animation {
const {value, offset} = result;
const syncBeforeCallback =
ReactNativeFeatureFlags.animatedShouldSyncValueBeforeStartCallback();
if (syncBeforeCallback && value != null) {
if (
syncBeforeCallback &&
value != null &&
!this.__isValueOverridden
) {
animatedValue.__onAnimatedValueUpdateReceived(value, offset);
}

this.__notifyAnimationEnd(result);

if (value != null) {
if (!syncBeforeCallback) {
if (!syncBeforeCallback && !this.__isValueOverridden) {
animatedValue.__onAnimatedValueUpdateReceived(value, offset);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,7 @@ export default class AnimatedValue extends AnimatedWithChildren {
*/
setValue(value: number): void {
if (this._animation) {
this._animation.__isValueOverridden = true;
this._animation.stop();
this._animation = null;
}
Expand Down Expand Up @@ -225,6 +226,9 @@ export default class AnimatedValue extends AnimatedWithChildren {
* See https://reactnative.dev/docs/animatedvalue#resetanimation
*/
resetAnimation(callback?: ?(value: number) => void): void {
if (this._animation) {
this._animation.__isValueOverridden = true;
}
this.stopAnimation(callback);
this._value = this._startingValue;
if (this.__isNative) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import typeof TNativeAnimatedModule from '../../specs_DEPRECATED/modules/NativeA
import {create, unmount, update} from '@react-native/jest-preset/jest/renderer';
import {format} from 'node:util';
import * as React from 'react';
import {createRef} from 'react';
import {Activity, createRef} from 'react';

describe('Native Animated', () => {
let NativeAnimatedModule: Exclude<TNativeAnimatedModule, null | void>;
Expand Down Expand Up @@ -1487,6 +1487,63 @@ describe('Native Animated', () => {
expect(NativeAnimatedModule.stopAnimation).toBeCalledWith(animationId);
});

it.each([
// $FlowFixMe[unclear-type]
['resetAnimation', (value: any) => value.resetAnimation(), 1],
// $FlowFixMe[unclear-type]
['setValue', (value: any) => value.setValue(0.5), 0.5],
])(
'does not let a stopped animation overwrite the value set by %s',
(_name, assignValue, expectedValue) => {
const {Animated} = importModules();

const value = new Animated.Value(1);
Animated.timing(value, {
toValue: 0.2,
duration: 50,
useNativeDriver: true,
}).start();

assignValue(value);

// Native reports where the animation was stopped, asynchronously.
// $FlowFixMe[prop-missing]
NativeAnimatedModule.startAnimatingNode.mock.calls[0][3]({
finished: false,
value: 0.2,
});

expect(value.__getValue()).toBe(expectedValue);
},
);

it('commits the latest value when revealed after animating while hidden', async () => {
const {Animated} = importModules();

const opacity = new Animated.Value(0.2);
// A stable element, so that nothing else re-renders it when revealed.
const child = <Animated.View style={{opacity}} />;

const root = await create(<Activity mode="visible">{child}</Activity>);
expect(root.toJSON()?.props.style.opacity).toBe(0.2);

await update(root, <Activity mode="hidden">{child}</Activity>);

Animated.timing(opacity, {
toValue: 1,
duration: 50,
useNativeDriver: true,
}).start();
// $FlowFixMe[prop-missing]
NativeAnimatedModule.startAnimatingNode.mock.calls[0][3]({
finished: true,
value: 1,
});

await update(root, <Activity mode="visible">{child}</Activity>);
expect(root.toJSON()?.props.style.opacity).toBe(1);
});

it('calls stopAnimation callback with native value', () => {
const {Animated} = importModules();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
useContext,
useEffect,
useInsertionEffect,
useLayoutEffect,
useReducer,
useRef,
} from 'react';
Expand Down Expand Up @@ -97,6 +98,19 @@ export default function createAnimatedPropsHook(

useAnimatedPropsLifecycle(node);

// `onUpdateRef` is cleared while hidden (e.g. by `Activity`), so values that
// changed in the meantime have not been committed. Layout effects with these
// dependencies only re-run for the same `node` when it is revealed.
const disconnectedNodeRef = useRef<AnimatedProps | null>(null);
useLayoutEffect(() => {
if (disconnectedNodeRef.current === node) {
scheduleUpdate();
}
return () => {
disconnectedNodeRef.current = node;
};
}, [node]);

// TODO: This "effect" does three things:
//
// 1) Call `setNativeView`.
Expand Down
Loading