Skip to content

Commit 82a2ba2

Browse files
martinboothnecolas
authored andcommitted
Share the same Animated.Value amongst transitions that happen in the same commit
Right now, updating state which affects transitioning properties accross multiple components results in each component creating its own Animated.Value and starting the animation in an effect. This results in animations that are not in sync as they should be. In this PR, transitions with the same delay, duration, timing function share the same Animated.Value as long as they're part of the same commit. useLayoutEffect is used to either create the Animated.Value or find an existing suitable one to use. Since all layout effects run before effects, we can assume that by the time any component's effect runs, all Animated.Values have been created or shared. One of the component's effects will then kick off the animation and empty the shared animation config map so that future commits don't reuse any of the Animated.Values that were created. The other component's effects will be no-ops. Finally, reference counting is used to ensure only the last component that unmounts would stop the animation if it was still running since now they are shared it would be disruptive to stop the animation unless the component was the last one relying on it
1 parent e8cdaa5 commit 82a2ba2

1 file changed

Lines changed: 80 additions & 28 deletions

File tree

packages/react-strict-dom/src/native/modules/useStyleTransition.js

Lines changed: 80 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import type {
1414
ReactNativeTransform
1515
} from '../../types/renderer.native';
1616

17-
import { useEffect, useRef, useState } from 'react';
17+
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
1818
import { errorMsg, warnMsg } from '../../shared/logUtils';
1919
import { Animated, Easing } from 'react-native';
2020

@@ -29,6 +29,13 @@ type TransitionMetadata = $ReadOnly<{
2929
shouldUseNativeDriver: boolean
3030
}>;
3131

32+
type AnimatedConfig = {
33+
start: () => void,
34+
dispose: () => void,
35+
value: Animated.Value,
36+
referenceCount: number
37+
};
38+
3239
const INPUT_RANGE: $ReadOnlyArray<number> = [0, 1];
3340

3441
function isNumber(num: mixed): num is number {
@@ -260,6 +267,53 @@ function getAnimation(
260267
});
261268
}
262269

270+
const animatedConfigs = new Map<string, AnimatedConfig>();
271+
272+
function getOrCreateAnimatedConfig(transitionMetadata: TransitionMetadata) {
273+
const key = JSON.stringify(transitionMetadata);
274+
275+
const animatedConfig = animatedConfigs.get(key);
276+
if (animatedConfig != null) {
277+
animatedConfig.referenceCount++;
278+
279+
return animatedConfig;
280+
}
281+
282+
const animatedValue = new Animated.Value(0);
283+
let hasStarted = false;
284+
let animation;
285+
const newAnimatedConfig = {
286+
referenceCount: 1,
287+
value: animatedValue,
288+
start: () => {
289+
if (hasStarted) {
290+
return;
291+
}
292+
hasStarted = true;
293+
const { delay, duration, timingFunction, shouldUseNativeDriver } =
294+
transitionMetadata;
295+
animation = Animated.sequence([
296+
Animated.delay(delay),
297+
getAnimation(
298+
animatedValue,
299+
duration,
300+
timingFunction,
301+
shouldUseNativeDriver
302+
)
303+
]);
304+
animation.start();
305+
},
306+
dispose: () => {
307+
if (--newAnimatedConfig.referenceCount === 0) {
308+
animation?.stop();
309+
}
310+
}
311+
};
312+
animatedConfigs.set(key, newAnimatedConfig);
313+
314+
return newAnimatedConfig;
315+
}
316+
263317
export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
264318
const {
265319
transitionDelay: _delay,
@@ -292,7 +346,7 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
292346
undefined
293347
);
294348

295-
const [animatedValue, setAnimatedValue] = useState<Animated.Value | void>(
349+
const [animatedConfig, setAnimatedConfig] = useState<AnimatedConfig | void>(
296350
undefined
297351
);
298352

@@ -321,28 +375,32 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
321375
]);
322376

323377
// effect to trigger a transition
324-
// REMEMBER: it is super important that this effect's dependency array **only** contains the animated value
378+
// REMEMBER: it is super important that this effect's dependency array **only** contains the animated config
325379
useEffect(() => {
326-
if (animatedValue !== undefined) {
327-
const { delay, duration, timingFunction, shouldUseNativeDriver } =
328-
transitionMetadataRef.current;
380+
if (animatedConfig == null) {
381+
return;
382+
}
383+
animatedConfig.start();
384+
animatedConfigs.clear();
385+
return () => {
386+
animatedConfig.dispose();
387+
};
388+
}, [animatedConfig]);
329389

330-
const animation = Animated.sequence([
331-
Animated.delay(delay),
332-
getAnimation(
333-
animatedValue,
334-
duration,
335-
timingFunction,
336-
shouldUseNativeDriver
337-
)
338-
]);
339-
animation.start();
390+
const transitionStyleHasChangedResult = transitionStyleHasChanged(
391+
transitionStyle,
392+
currentStyle
393+
);
340394

341-
return () => {
342-
animation.stop();
343-
};
395+
useLayoutEffect(() => {
396+
if (transitionStyleHasChangedResult) {
397+
setCurrentStyle(style);
398+
setPreviousStyle(currentStyle);
399+
setAnimatedConfig(
400+
getOrCreateAnimatedConfig(transitionMetadataRef.current)
401+
);
344402
}
345-
}, [animatedValue]);
403+
}, [currentStyle, style, transitionStyleHasChangedResult]);
346404

347405
if (
348406
_delay == null &&
@@ -354,18 +412,12 @@ export function useStyleTransition(style: ReactNativeStyle): ReactNativeStyle {
354412
return style;
355413
}
356414

357-
if (transitionStyleHasChanged(transitionStyle, currentStyle)) {
358-
setCurrentStyle(style);
359-
setPreviousStyle(currentStyle);
360-
setAnimatedValue(new Animated.Value(0));
361-
// This commit will be thrown away due to the above state setters so we can bail out early
362-
return style;
363-
}
364-
365415
if (transitionStyle === undefined) {
366416
return style;
367417
}
368418

419+
const animatedValue = animatedConfig?.value;
420+
369421
const outputAnimatedStyle: AnimatedStyle = Object.entries(
370422
transitionStyle
371423
).reduce<AnimatedStyle>((animatedStyle, [property, value]) => {

0 commit comments

Comments
 (0)