Getting started with React Native animations

Here are the tools you need to overcome performance challenges when implementing React Native animations.
376 readers like this.
Woman programming

WOCinTech Chat. Modified by Opensource.com. CC BY-SA 4.0

React Native animation is a popular topic for workshops and classes, perhaps because many developers find it challenging to work with. While many online blogs and resources focus on the performance aspects of React Native, few take you through the basics. In this article, I will discuss the fundamentals of how to implement React Native animations.

First, let's review some background and history.

History and evolution

To work with cross-platform JavaScript code, native components of your phone must communicate information via an element called a bridge. The bridge is asynchronous, which causes JavaScript-based React Native apps to lag because asynchronous requests over the bridge clog the path of JavaScript code interacting with the native parts.

To achieve high performance, animations must be rendered on a native UI thread. Since data needs to be serialized over the bridge, this often blocks the JavaScript thread, causing a drop in frames. This problem has been prevalent since 2015 when animations posed one of the biggest limitations in React Native.

Fortunately, the situation has improved since then thanks to overwhelming support from community members. Achieving 60 frames per second (FPS) is now common for React Native animations. Declarative APIs such as Animated have eased the process of implementing interactive animations.

Using the Animated API to improve performance

Still, it is not unusual for developers to encounter performance issues, especially when they are working on complex animations.

As mentioned above, performance bottlenecks in React Native animations are caused by heavy workloads on JavaScript threads, which reduces the frame rate and causes a sluggish user experience. To overcome this problem, you need to maintain a frame rate of 60 frames per second.

Using the Animated API is the best solution to this as it optimizes the required serialization/deserialization time. It does this by using a declarative API to describe animations. The idea behind this is to declare the entire animation at once in advance so that the declaration in JavaScript can be serialized and sent to the bridge. A driver then executes the animations frame by frame.

How to implement animations in React Native

There are several ways to implement animations in React Native. Here are some that I find most useful.

Animated values

Animated values tops my list as the building block for animations in any React Native app. These generally point to a real value, which is converted back to a real number when passed with an animated component.

Let’s look at an example:

Animated.timing(this.valueToAnimate, {
    toValue: 42;
    duration: 1000;
}).start()

In the above example, I have declared value.ToAnimate as 42, which will be executed after 1000 milliseconds.

You can also use Animated values to declare properties such as opacity or position. Here’s an example implementation of opacity with Animated values:

<Animated.View style={{ opacity: myAnimatedOpacity }} />  

Animation drivers: Animated.timing, Animated.event, Animated.decay

Think of drivers like nodes in a graph that changes an Animated value with each frame. For instance, Animated.timing will increase a value, while Animated.decay will reduce a value over each frame.

Let’s look at an example:

Animated.decay(this.valueToAnimate, {
   velocity: 2.0,
   deceleration: 0.9
}).start();

This example launches the animation at a particular velocity and gradually decelerates at a particular time. I like to do this in a cross-platform app when docs on the material design initially surface. It feels pretty great, and there are many ways to make the animation deliver a memorable experience.

You can also use Animated.event to drive a value as your user scroll:

<ScrollView onScroll={Animated.event(
  [{nativeEvent: {contentOffset: {y: this.state.scrollY}}}]
)}
>
</ScrollView>

In the above example, Animated.event returns a function that sets the scrollView's nativeEvent.contentOffset.y to your current scrollY state.

All in all, animated drivers can be used in association with Animated values or other Animated drivers.

As a side note, keep in mind that when a driver updates each frame, the new value will instantly update the View property. So be careful while declaring the variables and mindful of their scope while using them.

Transform methods

Transform methods enable you to convert an Animated value to a new Animated value.

You can use methods such as Animated.add(), Animated.multiply(), or Animated.interpolate() to implement transform operations. You can execute a transform operation on any node in the Animated graph with this:

newAnimated.Value(55).interpolate(.....) // Transformation operation using Animated.interpolate() method

Animated props

Animated props are special nodes that map an Animated value to a prop on a component. It is generated when you render an Animated.view and assign it properties.

Let’s look at the following code snippet:

Var opacity = new Animated.Value(0.7);
<Animated.View style={{ opacity }} />

Here, I’ve added an Animated prop that converts the value 0.7 to a property. If a method updates the value, the change will be reflected in the View’s property.

The methods described above work in conjunction with, and play a crucial role in, animating objects in React Native.

The animated value for every frame of the animation is changed by the animation driver (Animated.Timing, Animated.Event, or Animated.Decay). The result is then passed along to any transformation operation, where it gets stored as the prop of the view (opacity or transform value).

The result is then handed over to the native realm by JavaScript, where the view gets updated while calling setNativeProps. Finally, it is passed over to iOS or Android, where UIView or Android.View gets updated.

Implementing animations using the Animated API and Native Driver

Since the inception of the React Native Animated API, a JavaScript driver has been used for frame execution, but it resulted in frame drop as the business logic directly falls on the JavaScript thread.

request_animation_frame.png

To address frame drops, the latest version of the driver was made purely native, and it is now capable of executing animations frame by frame in native realm.

The Native Driver, when used alongside the Animated API, allows the native animated module to update views directly without the need to calculate the value in JavaScript.

In order to use Native Driver, you must specify useNativeDriver to be true while configuring your animations:

useNativeDriver: true

Using PanResponder for handling gestures in React Native

The React Native Animated API can do most of the "grunt work" for you while implementing React Native animations. However, it has a key limitation when it comes to implementing gestures in animations: It is unable to respond to gestures beyond the scope of a ScrollView.

While you can do many things with a simple ScrollView component, you will likely agree that mobile is incomplete without gestures, which are the actions users perform with animations, such as scroll or pan.

In React Native, gestures can be handled seamlessly by using PanResponder with the Animated API.

PanResponder combines various touches into a specific gesture. It makes a single touch responsive to extra touches so that gestures function smoothly.

By default, PanResponder consists of an InteractionManager handle, which blocks the events running on the JS thread from interrupting the gestures.

Improving uptime for slow Navigator transitions

Any React Native animation that involves moving from one app screen to another should usually be done using navigation components. Navigation components such as React Navigation are generally used for navigation transitions.

In React Native, navigation transitions usually happen in JavaScript threads, which can result in slow transitions in for low-end/low-memory devices (typically Androids, as iOS devices handle these more effectively). Slow navigation transitions usually happen when React Native is trying to render a new screen while an animation is still executing in the background.

To avoid such situations, InteractionManager allows long-running tasks to be scheduled after an animation or interaction has executed in the JavaScript thread.

Layout animations

LayoutAnimation is a simple API that automatically animates the view to the next consecutive position when the next layout happens. It works on the UI thread, which makes it highly performant.

Animations configured using LayoutAnimation will apply on all the components once it is called, in contrast to Animated, in which you control the specific values to animate. LayoutAnimation is capable of animating everything that changes on the next rendering, so you should call it before calling setState.

Configuring a layout animation before calling setState will ensure smooth animations in native thread and prevent your animations from being affected if code in another setState diff is triggered (under normal conditions, this would compromise your app's animation).

Another way of using LayoutAnimation is to call it inside the component WillReceiveProps method. Simply call LayoutAnimation.configureNext by passing the appropriate parameters for animation configuration, as shown below:

LayoutAnimation.configureNext(animationConfiguration, callbackCompletionMethod); 
this.setState({ stateToChange: newStateValue }); 

LayoutAnimation supports only two properties: opacity and scalability.

It identifies views by using their unique keys and computing their expected position. Moreover, it animates the frame changes as long as the view keeps the same key between changes of states.

Animations implemented using LayoutAnimation happen natively, which is good from a performance perspective, but it can be challenging if all properties need to be animated between states.

Closing thoughts

This article only scratches the surface of React Native animations. A rule of thumb when working in React Native is to use Animated API wherever possible. For gestures, use PanResponder alongside Animated API.

Leveraging Native Driver along with Animated API will eliminate most of the issues you may encounter—but if performance still lags, stick to LayoutAnimation.

User profile image.
Open source frameworks and Rock music are something which drives Rakshit. The other half of him helps startups and organizations adopt technologies. Get in touch with him @RakshitSoral on Twitter. 

2 Comments

Creative Commons LicenseThis work is licensed under a Creative Commons Attribution-Share Alike 4.0 International License.