Completing animation before unmounting (via exiting animation?) #7755
Replies: 3 comments 1 reply
-
To finish the current progress loop before unmounting you can manually manage the entire process to have full control over it. The implementation below might be what you are looking for: const isUnmounting = useRef(false)
const loop = useCallback(() => {
translateX.value = withTiming(
width,
{ duration: animationDuration },
finished => {
if (!finished || isUnmounting.current) return
translateX.value = -barSize
loop()
}
)
}, [])
useEffect(() => {
loop() // Start endless (recursive) loop
return () => {
// Stop scheduling new loops
isUnmounting.current = true
// Grab current position
cancelAnimation(translateX)
const remaining = width - translateX.value
const remDur= (remaining / (width + barSize)) * animationDuration
// One final slide to 100%, then call onFullyHidden()
translateX.value = withTiming(
width,
{ duration: remDur },
finished => {
if (finished && onFullyHidden) runOnJS(onFullyHidden)() // Whatever you wanna do at the end of the animation
}
)
}
}, []) |
Beta Was this translation helpful? Give feedback.
-
I posted a solution that doesn't use layout animations in the comment above. Here is a solution that utilizes Layout Animations for your requested case: Code snippetimport React, { useCallback, useEffect, useState } from 'react';
import { Button, StyleSheet, View } from 'react-native';
import Animated, {
Easing,
EntryExitAnimationFunction,
interpolate,
useAnimatedStyle,
useDerivedValue,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated';
const animationDuration = 1000;
export default function EmptyExample() {
const [isMounted, setIsMounted] = useState(true);
const progress = useSharedValue(0);
const currentIteration = useSharedValue(0);
const translateX = useDerivedValue<`${number}%`>(
() => `${interpolate(progress.value, [0, 1], [-50, 100])}%`
);
useEffect(() => {
progress.value = withRepeat(
withTiming(
1,
{ duration: animationDuration, easing: Easing.linear },
(finished) => {
if (finished) {
currentIteration.value++;
}
}
),
-1,
true
);
}, [progress, currentIteration]);
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}));
const customExiting = useCallback<EntryExitAnimationFunction>(() => {
'worklet';
let targetX: `${number}%`, remainingDuration: number;
if (currentIteration.value % 2 === 0) {
targetX = '100%';
remainingDuration = (1 - progress.value) * animationDuration;
} else {
targetX = '-50%';
remainingDuration = progress.value * animationDuration;
}
return {
initialValues: {
transform: [{ translateX: translateX.value }],
},
animations: {
transform: [
{
translateX: withTiming(targetX, {
duration: remainingDuration,
easing: Easing.linear,
}),
},
],
},
};
}, [progress, currentIteration, translateX]);
return (
<View style={styles.container}>
<Button title="Unmount" onPress={() => setIsMounted(false)} />
{isMounted && (
<Animated.View
style={[styles.box, animatedStyle]}
exiting={customExiting}
/>
)}
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
alignItems: 'center',
justifyContent: 'center',
},
box: {
width: 100,
height: 100,
backgroundColor: 'red',
},
}); Simulator.Screen.Recording.-.iPhone.16.Pro.-.2025-07-07.at.11.16.41.1.mp4 |
Beta Was this translation helpful? Give feedback.
-
Thanks for the input, my experiments with For now I settled for not unmounting the component. I use a boolean prop instead (adding spin delay was also easier this way). import { useEffect } from 'react'
import { Dimensions, StyleSheet } from 'react-native'
import Animated, {
cancelAnimation,
Easing,
useAnimatedStyle,
useSharedValue,
withRepeat,
withTiming,
} from 'react-native-reanimated'
import { useSpinDelay } from 'spin-delay'
const { width } = Dimensions.get('window')
const barDivider = 2
const barSize = width / barDivider
const animationDuration = 1500
type Props = {
visible: boolean
delay?: number
minDuration?: number
}
export function ProgressBar({ visible, delay, minDuration }: Props) {
const shouldRender = useSpinDelay(visible, {
delay: delay ?? 300,
minDuration: minDuration ?? 700,
})
const translateX = useSharedValue(-barSize)
const isVisible = useSharedValue(false)
const opacity = useSharedValue(0)
useEffect(() => {
isVisible.value = shouldRender
if (shouldRender) {
opacity.value = 1
translateX.value = -barSize
translateX.value = withRepeat(
withTiming(
width,
{
duration: animationDuration,
easing: Easing.inOut(Easing.quad),
},
() => {
if (!isVisible.value) {
opacity.value = withTiming(0, { duration: 200, easing: Easing.inOut(Easing.quad) })
cancelAnimation(translateX)
}
}
),
-1,
false
)
}
}, [shouldRender, translateX, isVisible, opacity])
const animatedStyle = useAnimatedStyle(() => ({
transform: [{ translateX: translateX.value }],
}))
return (
<Animated.View
className="h-1 absolute bottom-0 w-full z-50"
pointerEvents="box-none"
style={{ opacity, backgroundColor: '#bec3c1' }}
>
<Animated.View
className="absolute bottom-0 bg-primary rounded-full h-1 z-50"
style={[styles.animatedStrip, animatedStyle]}
/>
</Animated.View>
)
}
const styles = StyleSheet.create({
animatedStrip: {
width: barSize,
},
}) |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
Hey there,
I have a linear progress (indeterminate) that moves from -50% width to 100% width on the screen (repeating, 1s each).
When the component unmounts, I would like to wait for the current iteration of the animation to finish.
Could this be done with
exiting
animations?Found this in the docs, but I'm not sure whether this is something useful here:
https://docs.swmansion.com/react-native-reanimated/docs/layout-animations/custom-animations/#remarks
Help appreciated, thanks
Beta Was this translation helpful? Give feedback.
All reactions