React Native Animations — Zero to Hero
By: Ethan Sharabi
Tags: Animation, JavaScript, React Native, Mobile App Development

After briefly covering Animated , useNativeDriver and LayoutAnimation . Next on the menu is — interpolation.

One of the more interesting and useful features to know when talking about animation in React Native is Interpolation. Animated allows us to interpolate the animated value and provide some logic for how our transition should behave. If, for instance, I would like to create a fade animation, I would animate the opacity prop, changing its value from 1 to 0 . It should look something like this

Animated.timing(this.state.fadeAnimation,

{

toValue: 1,

duration: 300,

}).start(); <View style={{opacity: this.state.fadeAnimation}}/>

While this is a common and simple use case sometimes the range of values we want to animate is more complex.

Let’s say we want to animate a clock hand, we would want to use the rotate transformation and move in a circle shape.

In this case, the values we’ll be using here are degrees ( 0deg–360deg ). Because an Animated value can’t hold these types of values, we will use a simple number between 0–360 instead, then interpolate this value into degrees. It should look something like this…

renderClockHand() {

const clockSize = 150;

const rotation = this.timeAnimation.interpolate({

inputRange: [0, 360],

outputRange: ['0deg', '360deg'],

}); return ( // rotating container

<Animated.View

width={clockSize}

height={clockSize}

style={{transform: [{rotate: rotation}]}}> // clock hand

<View

width={clockSize / 2}

height={clockSize / 2}

style={{borderRightWidth: 3}} />

</Animated.View>

);

}

Unfortunately, react-native doesn’t support transform-origin, that’s why we’re rotating a container and placing the clock hand in its first quarter, creating an illusion of rotating clock hand.

Interpolation generates a transition function for your animation. It can be a linear function like the one we used for the clock-hand or a non-linear function, depends on the given inputRange and outputRange .

When performance hits

In some cases, even when following the rules and when you think you got everything right, there are factors you can’t control.

We were going after this animation

Again, a pretty trivial use case, which can’t be too difficult to implement, and it is, not until you add performance to the equation.

In our case, this is the first screen our users see when they open the app. While this screen is being loaded lots of other things happening in the background, especially because it’s the initial phase of the app when usually network calls and other initialization logic occurs. This, of course, affects the (RN) bridge and performance in general.

Now, if we’ll go back to our animation. How would we tackle this type of animation? Well, the first approach with this animation was to trigger each card entrance separately with a delay of 100ms using timeout. I guess this is a pretty intuitive solution most people will come up with. Improving this can be done by using Animated.stagger API. Which behind the scenes handle it the same way — using JS timeout.

But why is that a problem? Because we rely on a delay of 100ms between each card entrance and because each card entrance animation is being created and invoked one at a time in the JS thread we might get a flaky animation just because the bridge is overloaded what can cause some inconsistency in the cards entrances.

You can see here what’s happening when the JS thread is busy while the entrance animations are happening.

To simplify, imagine you’re in line for a roller coaster ride. On this ride, each person goes up alone and sits in a solo cart. The premise is to send a single person each minute.

Now, here comes a group of 4 friends. They want to enjoy the ride together, but unfortunately, on this ride, each one of them will go up alone and have to wait for the rest of his friends at the end of the ride. Excepts for riding alone, this ride premise of sending a person each minute is not doable. Since the staff is slow and lazy, and the line is crowded you might have to wait a couple of minutes more.

What will be the solution then? Bigger carts! By having a cart of fours, a group of friends can enjoy the ride together and guarantee to finish together.

This is exactly how we should treat our animation. Instead of seeing it as 4 different card animations, we should see it as a single entrance animation of the whole screen.

If the ride’s line is the JS thread and the actual ride is the native thread, then by moving a group of animations as one, the animation starts once and goes all the way through till it ends on that native thread achieving a smooth, performant animation.

How all of this should be reflected in our code? The Animated Value! Instead of having 4 different animated values, one for each card, and instead of transitioning 4 values, we should have a single animated value — a single transition.

Master of Interpolation

Ok, enough talking. How are we going from 4 animations to 1? First, we create a single animate value in our screen

this.entranceAnimation = new Animated.Value(0)

Next, we start the transition, in our case, when the screen mount.

componentDidMount() {

Animated.timing(this.entranceAnimation, {

toValue: 100,

duration: 1300,

useNativeDriver: true

}).start()

}

Here, we’re going from 0 to 100 in 1300ms . Meaning our whole animation will take 1.3 seconds. Why did I pick to transition to the value of 100 ? cause it’s an easy number to work with.

The way I see it, our animated value represents a timeline, and during this timeline different animations are triggered. With interpolation, we can have different output values for each card by using different input values. So for example, our code for the first card entrance will look like this

renderFirstCardForNow() {

translateX = this.entranceAnimation.interpolate({

inputRange: [0, 25],

outputRange: [-100, 0] // the card will translate from the left side of the screen to its natural position

}); return

(<Animated.View style={{transform: [{translateX}]}}>

// Card content

</Animate.View>) }

Let’s take a look at the following illustration to understand how the whole animation takes place.

Each card’s animation starts at a different time — on a different animated value, therefore the inputRange for each card is different, for the first card it’s [0, 25] , for the second card it’s [25, 50] and so on.

Now, the actual animation effect is similar for each card, meaning all cards entrance are the same. They just run at different times. So the outputRange should also be the same, for our example we use [-100, 0] . And the code for rendering each card should look like this

renderCard(cardIndex) {

translateX = this.entranceAnimation.interpolate({

inputRange: [cardIndex * 25, cardIndex * 25 + 25],

outputRange: [-100, 0]

}); return

(<Animated.View style={{transform: [{translateX}]}}>

// Card content

</Animate.View>)

}

If you would like to make the animation look even slicker you can play with the easing of the animation and try overlapping the interpolation’s inputRange of each card. The final result should feel smooth and clean, even when the JS thread is busy.

For this animation, I used the following ranges

const translateX = this.entranceAnimation.interpolate({

inputRange: [

index * 25, (100 - index * 25) / 2 + index * 25,

100

],

outputRange: [-30, 10 - index * 10, 0],

extrapolate: 'clamp'

});

A bit confusing, I know, but if we’ll take a look at the timeline illustration, it should look something like this

New Players

Recently, two amazing libraries: react-native-reanimated & react-native-gesture-handler by Krzysztof Magiera had become the number one go-to for creating sophisticated, native-like components based on complex animations and native gestures.

Those libraries might not seem as trivial at first, but after diving into their API and learning how to use them correctly you might get to a new level of components and animations.

I will not go into them (it will probably require a whole different post), but I recommend checking them out if you feel safe enough with everything else we’ve talked about.