react-nativeanimationreact-animated

React native Animated.sequence. Callback in start method not triggered immediately after calling stop


I have a parent component that triggers a toast upon successfully completing an action (copy text in a list) The toast animates in and out (fade and scale) over 2700ms after the text is copied to the clipboard.

The problem I have is that if another item is copied from the parent whilst the animation is in progress, I want to stop the animation and trigger a new toast.

Parent component has a series of CopyableItems:

<CopyableItem value='Item 1' sideValue='Text to be copied' onPress={onInfoCopied} />


const onInfoCopied = (value) => {
    if (value === message) {
        // don't do anything, same item pressed again
    } else {
        setMessage(value);
        setShowToast(true);
    }
};

...and renders the Toast like this:

{message && (<Toast message={`${message} copied`} toastVisible={showToast} endAnimation={() => animationEnded()} />)}

const animationEnded = () => {
    console.log('animation Ended, toast false');
    setShowToast(false);
    setMessage('');
};

Toast component:

useEffect(() => {
    const animation = Animated.sequence([
        Animated.timing(fadeAnim, {
            duration: 300,
            toValue: 1,
            useNativeDriver: true,
        }),
        Animated.timing(scaleAnim, {
            duration: 300,
            toValue: 1.1,
            useNativeDriver: true,
        }),
        Animated.timing(scaleAnim, {
            duration: 300,
            toValue: 1,
            useNativeDriver: true,
        }),
        Animated.delay(duration),
        Animated.timing(fadeAnim, {
            duration: 300,
            toValue: 0,
            useNativeDriver: true,
        }),
    ]);
    if (animationRunning.current) {
        // Why doesn't this trigger the callback in start() below and call endAnimation() immediately!
        animation.stop();
        animation.reset();
    } else {
        animationRunning.current = true;
    }
    animation.start(() => {
        animationRunning.current = false;
        endAnimation();
    });
}, [message]);

My expectation is that the callback in the start() method should be called immediately after the animation.stop() call (when copying another item during a running toast animation)- but it appears to wait until the original animation timing sequence is finished before calling it (2700ms after animation start).

This results in another animation being triggered but the stop callback from the first animation is triggered during the second animation, hiding the second animation early (via the showToast state change in the parent).

Am I making some kind of stupid error here and if not does anyone have a better solution for this? ie. Stopping an animation and re-triggering a new.


Solution

  • For anyone else having the same problem, the solution was to remove the conditional animation.stop() and animation.reset() and add a cleanup function in the useEffect hook that calls animation.reset() instead. By adding a check for 'finished' in the animation.start callback which is only set to true if the animation finishes without interruptions I was able to only call endAnimation() only when the animation properly finished:

    useEffect(() => {
        const animation = Animated.sequence([
            Animated.parallel([
                Animated.timing(fadeAnim, {
                    duration: 300,
                    easing: Easing.out(Easing.quad),
                    toValue: 1.05,
                    useNativeDriver: true,
                }),
                Animated.timing(scaleAnim, {
                    duration: 250,
                    easing: Easing.inOut(Easing.linear),
                    toValue: 1.1,
                    useNativeDriver: true,
                }),
            ]),
            Animated.timing(scaleAnim, {
                duration: 250,
                easing: Easing.inOut(Easing.linear),
                toValue: 1,
                useNativeDriver: true,
            }),
            Animated.delay(duration),
            Animated.timing(fadeAnim, {
                duration: 300,
                easing: Easing.out(Easing.linear),
                toValue: 0,
                useNativeDriver: true,
            }),
        ]);
    
        animation.start(({ finished }) => {
            if (finished) {
                endAnimation();
            }
        });
        // this fixes everything!
        return () => {
            animation.reset();
        };
    }, [message]);