Let’s Get Moving: An Introduction to React Native Animations — Part 2

Shane Boyar
7 min readMar 6, 2019

--

If you want to start completely from scratch, here’s a link to Part 1 where we used LayoutAnimation to animate the repositioning of a single element on screen.

Let’s just jump right back into it.

In this article we are going to move away from using LayoutAnimation which is a great tool for things like repositioning elements in lists that are being updated, hiding/revealing a menu, etc, but what if you want a bit more granularity in your animations? Well that’s where React Native’s Animated API comes in handy.

Let’s take Jake from the last article, but lets decide we want him to rotate in place, and also that we want to toggle off this rotating based on a piece of state. Arbitrary, sure, but this is my article so I’m in charge here.

We’ll start from where we left off with our code at the end of part one, lock Jake to the center of the screen, add in some state to decide if he’s spinning or not, and get rid of some buttons and a few other lines to clean things up.

// App.jsimport React, { Component } from "react";
import {
Button,
StyleSheet,
Image,
View
} from "react-native";
import jake from "./jake.png";export default class App extends Component {
constructor(props) {
super(props);
this.state = {
spinning: false
};
}
render() {
return (
<View style={[styles.container]}>
<Image source={jake} />
<View style={styles.buttonsContainer}>
<Button
style={styles.button}
title={
this.state.spinning ? "Turn Spinning Off" : "Turn Spinning On"
}
onPress={() => {}}
/>
</View>
</View>
);
}
}
const styles = StyleSheet.create({
container: {
flex: 1,
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F5FCFF",
paddingTop: 64,
paddingBottom: 32
},
buttonsContainer: {
flexDirection: "row",
justifyContent: "space-evenly",
position: "absolute",
bottom: 16,
width: "100%"
},
button: {
width: 100
}
});
Your app should look similar to this.

Now let’s make a method that toggles our spinning state and attach it to the button.

// App.js{...}toggleSpinning = () => {
this.setState({
spinning: !this.state.spinning
});
};
render() {
return (
<View style={styles.container}>
<Image source={jake} />
<View style={styles.buttonsContainer}>
<Button
onPress={() => this.toggleSpinning()}
buttonStyle={styles.button}
title={this.state.spinning ? "Turn off spinning" : "Turn on spinning"}
/>
</View>
</View>
);
}
{...}

Refresh the app and check to see if your button title toggles back and forth. Of course, nothing else will be happening yet. To get our Jake to spin, we need to supply him with something that will update his transform: [{rotate: ... }] value. React Native’s Animated API gives us a neat way of keeping track of a number (or a pair of numbers) that we can manipulate and then apply to whatever element we choose, and it will handle all the updating that comes along with that.

We need to create what is called an Animated.Value, and though the documentation shows storing this value in state, I tend to not do that since we will be mutating this value directly (as opposed to through setState).

Let’s import Animated from "react-native" and create this value which we will call this._rotationAnimation directly in our constructor, then add a callback to our current setState call to update this new Animated.Value

// App.js{...}constructor(props) {
super(props);
this._rotationAnimation = new Animated.Value(0); this.state = {
spinning: false
};
}
}
{...}toggleSpinning = () => {
this.setState({ spinning: !this.state.spinning }, () => {
Animated.timing(this._rotationAnimation, {
toValue: 1,
duration: 2000
});
});
};
{...}

What’s happening here is that after our spinning state is successfully toggled, we take our rotationAnimation which holds an Animated.Value set to 0 , and have it increase to a value of 1 over a duration of 2000 milliseconds.

If you refresh your app and toggle the state, well, nothing will happen yet. We need to somehow translate the increase in this value from 0 to 1 into something we can use to rotate our Jake. Luckily, Animated comes with a tool to do something just like this.

Animated.Value().interpolate is a good lil function that maps two sets of numbers to each other. For instance if you give it an inputRange of [0,1] and an outputRange of [0, 100] and bind it to your _rotationAnimation, when _rotationAnimation === 0 your interpolation function will return 0, but when _rotationAnimation === 1, it will return 100. When it is 0.5, it will return 50, and so on. Remember, we are increasing our _rotationAnimation from 0 to 1 over the course of 2 seconds, so it will increase our returned result at the same rate. You can interpolate surprising number of output ranges (which are listed in the docs). For instance, to make this project work we will use the output range [‘0deg’, ‘360deg’]. If we apply that output to a rotate css rule, our icon should rotate!

We will create a separate method to return this interpolated value and then use that in our styling.

// App.js{...}getRotationAnimation = () => {
const rotate = this._rotationAnimation.interpolate({
inputRange: [0, 1],
outputRange: ["0deg", "360deg"]
});
return { rotate };
};
{...}<Image
source={jake}
style={{ transform: [this.getRotationAnimation()] }}
/>
{...}

When you refresh the app, you should see an error like this: console.error: “Unhandled JS Exception: Invariant Violation: Transform with key of “rotate” must be a string: {“rotate”:”0deg”}

This is because we have not told React Native that Jake is an Element that we want to animate. Unlike LayoutAnimation we need to explicitly declare which elements will be Animated. There are two ways of doing this. Animated comes packaged with a few elements that are ready to be Animated, Animated.View, Animated.Image, Animated.ScrollView, and Animated.Text, which can just be directly swapped out for their non-Animated counterparts. Alternatively, there is a function, Animated.createAnimatedComponent, which takes a non-Animated component and returns you an Animated one.

Right now all we need to do is switch Jake for an Animated.Image.

<Animated.Image
source={jake}
style={{ transform: [this.getRotationAnimation()] }}
/>

Now our icon animates on toggle? No, because I made the same mistake I almost always do, so I wanted to point it out here because it can be endlessly frustrating when you’re being a bit sloppy. Animated.timing(…) returns an object, but the animation doesn’t start until .start() is run on that object.

Update our toggleSpinning like so:

toggleSpinning = () => {
this.setState({ spinning: !this.state.spinning }, () => {
Animated.timing(this._rotationAnimation, {
toValue: 1,
duration: 2000
}).start();
});
};

Now we’re in business, but we don’t yet have a loop. In fact, even toggling the spinning state does nothing after the first rotation and that is because once our Animated.Value is incremented to 1, we never set it back to 0 and just keep trying to change it from 1 to 1, which does nothing. Luckily, in this case Animated provides another simple solution.

We just wrap our Animation we made inside an Animated.loop and start that instead:

toggleSpinning = () => {
this.setState({ spinning: !this.state.spinning }, () => {
Animated.loop(
Animated.timing(this._rotationAnimation, {
toValue: 1,
duration: 2000
})
).start();
});
};

Our Jake is now rotating in a loop, but it feels a bit weird. This is because the default animation, as defined by something called an “Easing function”, is set to easeInEaseOut. This means that the animation accelerates along something like a sine wave.

Robert Gummesson has a great article showing what these easing functions look like in detail.

To make our loop smoother, we need a linear easing function so it says at the same speed the whole time. To achieve this we import Easing from "react-native" which provides us with some convenient shortcuts for various easing functions. In this case we want to use Easing.linear. We apply that to our config block with the key easing:.

toggleSpinning = () => {
this.setState({ spinning: !this.state.spinning }, () => {
Animated.loop(
Animated.timing(this._rotationAnimation, {
toValue: 1,
duration: 2000,
easing: Easing.linear
})
).start();
});
};

This does the trick for a smoothly, infinitely spinning Jake, but toggling the spinning state doesn’t stop his rotation, it just restarts
him, since our callback function just starts our loop. We can fix that, no problem.

Lets extract out our looping animation for clarity:

startLoopAnimation = () => {
Animated.loop(
Animated.timing(this._rotationAnimation, {
toValue: 1,
duration: 2000,
easing: Easing.linear
})
).start();
};

and let’s just go ahead and create a stopping method:

stopLoopAnimation = () => {
this._rotationAnimation.stopAnimation();
};

and update our toggle method accordingly:

toggleSpinning = () => {
this.setState({ spinning: !this.state.spinning }, () => {
this.state.spinning
? this.startLoopAnimation()
: this.stopLoopAnimation();
});
};

Great, now he stops and starts, but there’s a new annoying thing in that he jumps back to the starting location each time we toggle him back on. To be honest I’m not super clear on why the starting value of the animation stays at 0, but fortunately our stopAnimation provides us with a callback and passes it the current Animated.Value when the animation stops, so that we can use it to offset our animation when it starts back up.

We create a variable to hold that number called _rotationOffset and set it to 0.

constructor(props) {
super(props);
this._rotationAnimation = new Animated.Value(0);
this._rotationOffset = 0;
this.state = {
spinning: false
};
}

Then modify our stop and start methods to take advantage of it:

startLoopAnimation = () => {
this._rotationAnimation.setOffset(this._rotationOffset);
Animated.loop(
Animated.timing(this._rotationAnimation, {
toValue: 1,
duration: 2000,
easing: Easing.linear
})
).start();
};
stopLoopAnimation = () => {
this._rotationAnimation.stopAnimation(currentValue => {
this._rotationOffset = currentValue;
});
};

And that does it! Now we can toggle off and on our spinning Jake and he just does what we want him to.

You can see the final code here.

Animated's interpolation in conjunction with various configurations of Animated.timing along with other methods not covered in this article like Animated.sequence and Animated.parallel can be used to create intricate Animations that look incredible. Get to experimenting!

In the next and final(?) part in the series I will go over using a user’s touch input to animate an element around on the screen.

Thanks for reading!

--

--

Shane Boyar

Shane is a software engineer, home-brewer, bread baker, and writer living in Richmond, VA. Currently writing code @ RTSLabs