Michael Cheng
Michael Cheng

Reputation: 10451

How to stop a looping animation in React Native?

I have a simple looping animation in my component like this:

runAnimation() {
    console.log('run animation');
    this.state.angle.setValue(0);
    Animated.timing(this.state.angle, {
        toValue: 360,
        duration: 8000,
        easing: Easing.linear
    }).start(() => this.runAnimation());
}

...

<Animated.Image
    style={[
        styles.rotate,
        { transform: [
            { rotate: this.state.angle.interpolate({
                inputRange: [0, 360],
                outputRange: ['0deg', '360deg']
            })},
        ]}
    ]}
    source={require('./spinning_ball.png')}
/>

How would I stop this animation? For example, when navigating away to another screen or after a user clicks on a button.

I tried using this.state.angle.stopAnimation() but noticed run animation still being printed in the console. Is there a different stop method I should be calling to prevent the start callback from being executed?

Upvotes: 12

Views: 21636

Answers (6)

imagio
imagio

Reputation: 1490

I like to encapsulate my animations in hooks, it makes for a much cleaner, easier, and more reusable pattern than class components. Here's how I do a looping animation with easy start/stop control in typescript:

export const useBounceRotateAnimation = (running: boolean = true, rate: number = 300) => {
    //Example of making an infinite looping animation and controlling it with a boolean
    //Note that this assumes the "rate" is constant -- if you wanted to change the rate value after creation the implementation would have to change a bit

    //Only create the animated value once
    const val = useRef(new Animated.Value(0))

    //Store a reference to the animation since we're starting-stopping an infinite loop instead of starting new animations and we aren't changing any animation values after creation. We only want to create the animation object once.
    const anim = useRef(
        Animated.loop(
            Animated.timing(val.current, {
                toValue: 1,
                duration: rate,
                easing: Easing.linear,
                useNativeDriver: true,
                isInteraction: false,
            })
        )
    ).current

    //Interpolate the value(s) to whatever is appropriate for your case
    const interpolatedY = val.current.interpolate({
        inputRange: [0, 0.5, 1],
        outputRange: [0, 6, 0],
    })
    const interpolatedRotate = val.current.interpolate({
        inputRange: [0, 0.25, 0.5, 1],
        outputRange: ["0deg", "-3deg", "3deg", "0deg"],
    })

    //Start and stop the animation based on the value of the boolean prop
    useEffect(() => {
        if (running) {
            anim.start()
        } else {
            //When stopping reset the value to 0 so animated item doesn't stop in a random position
            anim.stop()
            val.current.setValue(0)
        }

        //Return a function from useEffect to stop the animation on unmount
        return () => anim.stop()
     //This useEffect should rerun if "running" or "anim" changes (but anim won't change since its a ref we never modify)
    }, [running, anim])

     //Return the animated values. Use "as const" const assertion to narrow the output type to exactly the two values being returned. 
    return [interpolatedY, interpolatedRotate] as const
}

I use this actual hook implementation to make a character image "walk" in my game. Now in my component it's very easy to use:

const MyComponent = () => {
    const [isRunning, setIsRunning] = useState(true)

    const [translation, rotation] = useBounceRotateAnimation(isRunning)

    return (
        <Animated.View
            style={{
                transform: [{ translateY: translation}, { rotate: rotation}],
            }}
        >
             ....rest of your component here, just setIsRunning(false) to stop animation whenever you need
        </Animated.View>
    )
}

As you can see this pattern is very clean and reusable. The animated component doesn't need to know anything about the animation, it just consumes the animated values and tells the animation when to run.

Upvotes: 11

Ikram Ud Daula
Ikram Ud Daula

Reputation: 1321

The simplest way to stop looping animation by calling setValue() method and assign a value. like:

const [opacity, setOpacity] = useState(new Animated.value(0));

// declare your looping animated function

// Any time to stop the looping animated function just call setValue() 
// on your stop animation event or function using opcacity reference. like
opacity.setValue(0)

For more details https://reactnative.dev/docs/animatedvaluexy#setvalue

Upvotes: 1

Quizzy
Quizzy

Reputation: 46

You can stop the loop using:

componentWillUnmount() {
   Animated.timing(this.state.angle).stop();
}

Upvotes: 1

max23_
max23_

Reputation: 6689

Based on my comment in Nguyên Hoàng's answer. Here is another way to stop the looping animation if you call this.state.angle.stopAnimation():

runAnimation() {
  this.state.angle.setValue(0);
  Animated.timing(this.state.angle, {
    toValue: 360,
    duration: 8000,
    easing: Easing.linear
  }).start((o) => {
    if(o.finished) {
      this.runAnimation();
    }
  });
}

Upvotes: 21

Ritesh Vishwakarma
Ritesh Vishwakarma

Reputation: 41

Set a global variable

let dataloaded = false;

To stop animation override the variable (dataloaded) whenever you want

componentWillUnmount() {
    dataloaded = true;
}

onPress={() => {
    dataloaded = true;
}}

final code will look like

runAnimation() {
    console.log('run animation');
    this.state.angle.setValue(0);
    Animated.timing(this.state.angle, {
        toValue: 360,
        duration: 8000,
        easing: Easing.linear
    })
    .start(() => {
        if ( ! dataloaded) {
            this.runAnimation();
        }
    })
}

Upvotes: 0

Nguy&#234;n Ho&#224;ng
Nguy&#234;n Ho&#224;ng

Reputation: 1053

You can create a variable stopAnimation to stop Animation whenever you want by only when stopAnimation === false then callback runAnimation function. Like example:

this.state = { stopAnimation: false }

runAnimation() {
  this.state.spinValue.setValue(0);
  Animated.timing(
    this.state.spinValue,
    {
      toValue: 1,
      duration: 3000,
      easing: Easing.linear
    }
  ).start( () => {
    if(this.state.stopAnimation === false) {
      this.runAnimation();
    }
  });
}

So you just need to create a button which call function this.state = { stopAnimation: true } to stop Animation.

Example here: https://rnplay.org/apps/Lpmh8A.

Upvotes: 5

Related Questions