React Native Animations — Zero to Hero

Getting around with RN animations — starting basic all the way to advanced

Ethan Sharabi
Wix Engineering

--

Image: ramotion

In this post, I’ll briefly cover basic-advance topics of RN Animations and dive into a real use-case issue we had and how we solved it. Hopefully, this will give you some ideas to solve similar animation-related issues.

Prerequisites

  • Familiarity with RN’s Animated library
  • Familiarity with JS Thread vs Native thread and the bridge

One of the things can make an app stands out is how slick it is and how it feels under your thumb.

Writing react native apps become more and more common recently, but the big question still stands, is it a worthy competitor to a fully written native app?

After all, the big main difference when comparing a react native app and a native one is their performance. And where are performance is most noticeable? You guessed right — Animations!

Whether it’s a screen transition or a button press feedback, a list item entrance or toast appearance, it’s important to have a smooth, 60fps animation.

Working on React Native about 4 years now, you learn a trick or two. When reaching a threshold I found in most cases thinking outside the box can result in great solutions, some can enrich your app looks & feel.

Use the Native Driver

If you’re not familiar with the useNativeDriver prop when using RN’s Animated library I urge you to go and learn about it before reading on.

In short, it uses the native thread to do all the magic, making your animation smooth sailing.

Animated.timing(this.state.fadeAnim, {
toValue: 1,
duration: 300,
useNativeDriver: true,
}).start();

By not using useNativeDriver, on each frame of the animation, we’ll have to go through the bridge, wasting time and energy, ending up with a flickery, laggy animation caused by lost frames.

Our first obstacle

But your job does not end here. Passing the useNativeDriver prop is a quick win in only some cases, for instance, this prop would only work when trying to animate non-layout properties. Things like backgroundColor, opacity and transformations.

My first advice will be to learn how to use transformation. You can translate, rotate and scale your view as you like, with these in hand you can cover most of the use cases you need.

There are already some libraries that provide quick ways to do these type of animations with much less code like react-native-animatable.

But we want more

Sometimes scaling or moving things around is just not enough. We came across the following use case

We needed to animate the button when its content changes, moving back and forth from a loading state. While this seems fairly simple animation to do, that wasn’t the case. Two problems we needed to solve here

  • Animating the size (width) of an element cannot be done with the useNativeDriver prop as mentioned before — and we don’t want to compromise on a laggy animation.
  • We do not know the button’s content, and we don’t know what’s the initial width of the button and what width will it have when the animation ends. The only way to do it dynamically is using onLayout callback — meaning we have to wait for the layout (size of the button) to change, go back in time and animate to that value. If only we could travel in time.

LayoutAnimation to the rescue. This nifty tool is a lifesaver not to mention how easy it is to use.

In short, we will trigger the layout animation for the next render cycle, it will add a nice transition to the layout change depends on the configuration you’ve set. In our case — the button’s change of size. And that’s it — we’re done!

Careful though! triggering the layout animation will affect all layout changes happening on the screen at that moment, even if triggered from inside of a specific component. To avoid unnecessary triggers of layout animation, it is recommended to use it in lifecycle methods like componentDidUpdate and to verify the relevant props have changed.

It should look something like this…

componentDidUpdate(prevProps, prevState) {
if (prevState.loading !== this.state.loading){ LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
}
}
render() {
return (
<Button label={this.state.loading ? '' : 'Submit'}>
{this.state.loading && <Loader/>}
</Button>)
}

The last 10%

If this post had a progress bar, we would have been stuck on 90%, moving slowly towards the end.

So far we’ve covered, in my opinion, most of the use cases you’ll ever need in a basic-intermediate app. The last use case I’m gonna show is a more advanced and mostly use what we’ve talked about so far, but, still requires some out of the box thinking.

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.

So… we learned how to approach animations, the different types of animations and how to overcome performance issues with a little out-of-the-box thinking. I invite you to comment with cool animations you find interesting and how you manage to make them happen.

--

--

Ethan Sharabi
Wix Engineering

Full Stack developer. UI enthusiast currently @ Wix.com working on react-native-ui-lib.