Animations are part of every mobile app. Smooth animations can make the difference between whether a user feels comfortable using an app or not. Essentially, an animation is just the screen rendering again and again, transitioning from one state to another.
This rendering should happen so quickly that the user doesn’t realize the single states of the animation but perceives it as a smooth animation. To take this one step further, animations not only transform from state A to state B over time, but they also react to user interactions such as scrolling, pressing, or swiping.
Most devices have a screen frame rate of 60 frames per second (fps), and modern devices already have 120 fps (at the time of writing, React Native only supports 60 fps, which you can learn about on GitHub at bit.ly/prn-rn-fps). This means that when running an animation, the screen has to be re-rendered at 60 fps.
This is quite challenging because calculating complex animations and re-rendering the screen are some of the most compute-intense operations. Especially on low-end devices, the computing of the animation can become too slow, and the screen refresh rate drops below 60/120 fps. This then makes the animation and the app feel sluggish and slow.
Essentially, you can group animations into two different types:
Since full-screen animations are handled internally by all popular navigation libraries, this chapter will focus on on-screen animations. Full-screen animations have been covered in the Navigating in React Native apps section of Chapter 4, Styling, Storage, and Navigation, Section Navigation.
There are multiple ways to achieve smooth animations in React Native. Depending on the type of project and animations you want to build, you can choose from a wide range of solutions, each with its own advantages and disadvantages. We will discuss the best and most widely used solutions in this chapter.
In this chapter, we will cover the following topics:
Info
There have been some interesting developments about using the Skia rendering engine (which powers Chrome, Firefox, Android, and Flutter) to render animations in React Native, but at the time of writing, this approach is not production ready.
To be able to run the code in this chapter, you have to set up the following things:
The current architecture of React Native is suboptimal when it comes to animations. Think of an animation that scales or moves a title image based on the vertical scroll value of a ScrollView; this animation has to be calculated based on the scroll value of the ScrollView and immediately re-render the image. The following diagram shows what would happen when using the plain React Native architecture:
Figure 6.1 – The React Native architecture while animating based on scroll values
Here, you can see the general React Native architecture. The JavaScript thread is where you write your code. Every command will be serialized and sent via the bridge to the native thread. In this thread, the command is deserialized and executed. The same happens with the user input, but it occurs the other way around.
For our animation, this means that the scroll value would have to be serialized, sent via the bridge, deserialized, transferred via a complex calculation to an animation value, serialized, transferred back via the bridge, deserialized, and then rendered. This whole process has to be done every 16 milliseconds (or 60 times a second).
This round-trip leads to multiple problems:
Because of these problems, it is not a good idea to write animations in your own plain React Native code (for example, by setting a state in a loop). Fortunately, there are multiple production-ready solutions to avoid these problems and achieve high-quality animations.
In the following sections, we will have a look at four different solutions. Every solution has advantages and disadvantages, and which solution should be preferred depends on the project and the use case. Let’s start with the built-in Animated API.
React Native comes with a built-in Animated API. This API is quite powerful, and you can achieve a lot of different animation goals with it. In this section, we will have a brief look at how it works and what advantages and limitations the internal Animated API has.
For a complete tutorial, please have a look at the official documentation at bit.ly/prn-animated-api.
To understand how the Animated API works, let’s start with a simple example.
The following code implements a simple fade-in animation, which makes a view appear over the duration of 2 seconds:
import React, { useRef } from "react"; import { Animated, View, Button } from "react-native"; const App = () => { const opacityValue = useRef(new Animated.Value(0)). current; const showView = () => { Animated.timing(opacityValue, { toValue: 1, duration: 2000 }).start(); }; return ( <> <Animated.View style={{ backgroundColor: 'red', opacity: opacityValue }} /> <Button title="Show View" onPress={showView} /> </> ); } export default App;
The Animated API is based on animated values. These values are changed over time and are used as part of the application styling. In this example, we initialize opacityValue as an Animated.Value component with the initial value of 0.
As you can see, the JSX code contains an Animated.View component whose style uses opacityValue as the opacity property. When running this code, the Animated.View component is completely hidden at the beginning; this is because the opacity is set to 0. When pressing the Show View button, showView is called, which starts an Animated.timing function.
This Animated.timing function expects an Animated.Value component as the first property and a config object as the second parameter. The Animated.Value component is the value that should be changed during the animation. With the config object, you can define the general conditions of the animation.
In this example, we want to change the Animated.Value component to 1 over the duration of 2 seconds (2,000 ms). Then, the Animated.timing function calculates the different states of the animation and takes care of the rendering of the Animated.View component.
Good to know
Essentially, you can animate every part of your UI. The Animated API exports some components directly, such as Animated.View, Animated.Image, Animated.ScrollView, Animated.Text, and Animated.FlatList. But you can animate any component by using Animated.createAnimatedComponent().
While the Animated API does not completely solve the problem of the React Native architecture, it is an improvement over just setting the state again and again and again, as it greatly reduces the payload that has to be transferred from the JavaScript thread to the native thread, but this transfer has to be done every frame. To prevent this transfer in every frame, you have to use the native driver, as shown in the following subsection.
When configuring the animation with the config object, you can set a property called useNativeDriver. This is very important and should be done whenever possible.
When using the native driver with useNativeDriver: true, React Native sends everything to the native thread before starting the animation. This means that the animation runs completely on the native thread, which guarantees a smooth-running animation and no frame drops.
Unfortunately, the native driver is currently limited to non-layout properties. So, things such as transform and opacity can be used in an animation with the native driver, whereas all the Flexbox and position properties, such as height, width, top, or left, can’t be used.
In some cases, you don’t want to use the Animated.Value component directly. This is where interpolation comes into play. Interpolation is a simple mapping of input and output ranges. In the following code example, you can see an interpolation, which adds a position change to the simple example from before:
style={{ opacity: opacityValue, transform: [{ translateY: opacityValue.interpolate({ inputRange: [0, 1], outputRange: [50, 0] }), }], }}
In this code example, we added a transform translateY property to the style object. This property transforms the vertical position of an object. We don’t set a fixed value, nor do we bind opacityValue directly.
We use an interpolate function with a defined inputRange value of [0,1] and a defined outputRange value of [50,0]. Essentially, this means that the translateY value will be 50 when opacityValue (which is our AnimatedValue) is 0 and will be 0 when opacityValue is 1. This results in our AnimatedView moving up 50px to its original position while fading in.
Tip
Try to use interpolation to reduce the number of animated values you have to use in your application. Most of the time, you can use one animated value and just interpolate on it, even in complex animations.
The Animated API interpolate function is quite powerful. You can have multiple values to define the range, extrapolate or clamp beyond the ranges, or specify the easing function of the animation.
The Animated API brings a lot of different options, which give you the possibility to create almost every animation you can imagine:
The following example is very similar to the example in the Understanding the architectural challenge of animations in React Native section of this chapter. The code shows you how to use a scroll value as the driver of an animation:
const App = () => { const scrolling = useRef(new Animated.Value(0)).current; const interpolatedScale = scrolling.interpolate({ inputRange: [-300, 0], outputRange: [3, 1], extrapolate: 'clamp', }); const interpolatedTranslate = scrolling.interpolate({ inputRange: [0, 300], outputRange: [0, -300], extrapolate: 'clamp', }); return ( <> <Animated.Image source={require('sometitleimage.jpg')} style={{ ...styles.header, transform: [ {translateY: interpolatedTranslate}, {scaleY: interpolatedScale}, {scaleX: interpolatedScale} ] }} /> <Animated.ScrollView onScroll={ Animated.event([{nativeEvent: {contentOffset: {y: scrolling,},},}], { useNativeDriver: true }, ) } > <View style={styles.headerPlaceholder} /> <View style={styles.content}> </View> </Animated.ScrollView> </> ); }
In this example, the native scroll event of the ScrollView is connected directly to the Animated.Value component. With the useNativeDriver: true property, the native driver is used; this means that the animation, which is driven by the scroll value, runs completely on the native thread.
The preceding example contains two interpolations of the scroll value: the first one scales the image when the ScrollView has been over-scrolled (which means the ScrollView returns negative scroll values), while the second one moves the image up while scrolling.
Again, due to the use of the native driver, all this interpolation is done on the native thread. This makes the Animated API very performant in this use case. You can read more about running animations based on user gestures in Chapter 7, Handling Gestures in React Native.
The Animated API also provides different easing methods alongside complex spring models. For more detailed information, please take a look at the official documentation at bit.ly/prn-animated-api.
As you can see, the Animated API is really powerful, and you can achieve nearly every animation goal with it. So, why are there even other solutions on the market when this very good animation library is built in? Well, the Animated API is far from perfect for every use case.
The internal React Native Animated API is a very good solution for simple to mid-complexity animations. These are the most important pros of the Animated API:
There are also some cons of the Animated API, which you have to keep in mind when choosing the best animation solution for your project:
All in all, I would recommend the Animated API for small to medium complexity animations, when you don’t already have other animation libraries in your project. However, let’s look at another option: react-native-animatable.
There are a lot of animations that are reused in nearly every app. This is what react-native-animatable is all about. This library is built on top of the internal React Native Animated API and provides a very simple declarative and imperative API to use simple, predefined animations.
The following code example describes a simple fade-in animation with react-native-animatable using the declarative method, along with a simple fade-out animation with react-native-animatable using the imperative method:
import React from "react"; import { View, Text, Pressable } from "react-native"; import * as Animatable from 'react-native-animatable'; const App = () => { const handleRef = ref => this.view = ref; const hideView = () => { this.view.fadeOutDown(2000); } return ( <> <Animatable.View style={{ backgroundColor: 'red' }} ref={handleRef} animation="fadeInUp" duration=2000 /> <Pressable onPress={hideView}> <Text>Hide View</Text> </Pressable> </> ); } export default App;
In this example, Animatable.View is given one of the predefined Animatable animations as the animation property, and a duration that defines how long the animation runs. That is all you have to do to have an entrance animation.
As stated before, Animatable also supports imperative usage, which means that you can call Animatable functions on Animatable components. In this example, this.view contains a reference to Animatable.View, which makes it possible to call Animatable functions on it.
This is done when pressing the Pressable. Here, hideView is called, which then calls the predefined fadeOutDown Animatable function, which makes the view disappear over 2 seconds (2,000 ms).
As we learned in the Using the internal Animated API of React Native section, using the native driver is crucial for achieving smooth animations. Since react-native-animatable is based on the Animated API, you should also configure your animations to use the native driver.
With react-native-animatable, this is done by adding useNativeDriver={true} as a property to the component you run the animation on.
Important note
Please check whether the predefined animation you want to use supports the native driver before using it with the native driver.
The react-native-animatable library is not limited to predefined animations. It also supports defining custom animations with a very simple API. Let’s take a look at how this is done.
The following example shows you how to create a simple fade-in and move-up animation, just as we did in the previous section:
The custom animations of react-native-animatable map the styles to the keyframes. In this example, we start with the first keyframe (0), and set the opacity value to 0 and the translateY value to 50. With the last keyframe (1), the opacity value should be 1 and the translateY value should be 0. Now this animation can be used as the animation property value of any Animatable component instead of the predefined string values.
Built on the React Native Animated API, all pros and cons of the Animated API also apply to react-native-animatable. In addition to that, the following pros are worth mentioning:
Since react-native-animatable is a library built on top of the Animated API, this additional layer also brings some cons:
Essentially, react-native-animatable is a simple library on top of the React Native Animated API. It simplifies working with animations and works best with simple, predefined animations. If you need these simple or standard animations and you are very limited in time to create your animations, react-native-animatable is the best option for you.
If you’d like to create more complex animations, please take a look at the following section.
Reanimated is by far the most complete and mature animation solution for React Native. It was an improved reimplementation of the React Native Animated API in the first place, but with version 2, the API changed and the library’s capabilities increased greatly.
This section covers the following topics:
Let’s get started.
Essentially, the core concept of Reanimated 2 is as simple as the Animated API. There are animation values that can be changed, and these animation values power the animations.
The following code shows an animation that scales in a View component:
import React from "react"; import { Text, Pressable } from "react-native"; import Animated, { useSharedValue, useAnimatedStyle, Easing, withTiming } from 'react-native-reanimated'; const App = () => { const size = useSharedValue(0); const showView = () => { size.value = withTiming(100, { duration: 2000, easing: Easing.out(Easing.exp), }); } const animatedStyles = useAnimatedStyle(() => { return { width: size.value, height: size.value, backgroundColor: 'red' }; }); return ( <> <Animated.View style={animatedStyles} /> <Pressable onPress={showView}> <Text>Show View</Text> </Pressable> </> ); }
When looking at this code, we realize three things:
One of the cool things about Reanimated is that you don’t have to care about the native driver. Every animation with Reanimated is processed on the UI thread. Another cool thing is that every style property can be used.
If you compare this to the limitations of the Animated API, you immediately see how much more powerful Reanimated is.
To understand how this is done, let’s take a look at the Reanimated architecture.
Reanimated 2 is based on animation worklets. In this context, a worklet is a JavaScript function that runs on the UI thread. Reanimated 2 spawns a second, very minimalistic, JavaScript environment on the UI thread that handles these animation worklets.
This means it runs completely independently from the React Native JavaScript thread and the React Native bridge, which guarantees awesome performance even for complex animations. This worklet context uses the new React Native architecture.
Let’s start with gaining an understanding of how to use worklets.
Let’s take a look at the example from the Understanding the architectural challenge of animations in React Native section of this chapter. We have an animation that resizes or moves a title image based on the Y scroll value of a ScrollView. The following figure shows what’s happening when implementing this example with Reanimated 2:
Figure 6.2 – Animation based on the scroll value in Reanimated 2
In Reanimated 2, the animation is created as a worklet on the JavaScript thread. But the whole animation worklet is executed in the worklet context on the UI thread. So, every time a new scroll event is received, it doesn’t have to cross the bridge; instead, it’s processed directly in the worklet context, and the new animation state is passed back to the UI thread for rendering.
To achieve this kind of architecture, Reanimated 2 comes with its own Babel plugin. This Babel plugin extracts every function that is annotated as worklet from the react-native code and makes it runnable in this separate worklet context on the UI thread. The following code example shows you how to annotate a function as a worklet:
function myWorklet() { 'worklet'; console.log("Hey I'm running on the UI thread"); }
This is a simple JavaScript function that contains the worklet annotation in line 2. Based on this annotation, the Reanimated 2 Babel plugin knows that it has to process this function.
Now, this can be run as a standard function on the JavaScript thread, but it can also be run as a worklet on the UI thread, depending on how it is called. If the function is called like a normal function in the JavaScript code, it runs on the JavaScript thread, and if it is called using the Reanimated 2 runOnUI function, it runs asynchronously on the UI thread.
Of course, it is possible to pass parameters to these worklet functions, no matter where it runs.
Understanding this connection is crucial to prevent a lot of errors from happening. Essentially, the JavaScript thread and the worklet context run in completely different environments. This means it is not possible to simply access everything from the JavaScript thread while being in a worklet. The following connections are possible when it comes to worklets:
For more information on worklets, you can take a look at the worklet part of the official documentation at https://bit.ly/prn-reanimated-worklets.
Like in the internal Animated API of React Native, Reanimated 2 works with animation values to drive the animation. These animation values are called Shared Values in Reanimated 2. They are called Shared Values because they can be accessed from both JavaScript environments – the JavaScript thread and the worklet context on the UI thread.
Since these Shared Values are used to drive animations, and these animations run in the worklet context on the UI thread, they are optimized to be updated and read from the worklet context. This means reads and writes of Shared Values from worklets are synchronous, while reads and writes from the JavaScript thread are asynchronous.
You can take a deeper look at Shared Values in the official documentation at https://bit.ly/prn-reanimated-shared-values.
When working with Reanimated 2, there is no need to create worklets for most use cases. Reanimated 2 provides an excellent set of Hooks and functions that can be used to create, run, change, interrupt, and cancel animations. These Hooks take care of transferring the executions of the animation to the worklet context automatically.
This is what was used in the example at the beginning of this section. In that scenario, we created a Shared Value with the useSharedValue Hook, connected the View’s style with the useAnimatedStyle Hook, and started the animation with the withTiming function.
Of course, you can also handle scroll values with Reanimated 2. The following code example shows you how to connect a ScrollView to a Shared Value, to scale and move an image with an animation driven by user scrolling:
function App() { const scrolling = useSharedValue(0); const scrollHandler = useAnimatedScrollHandler((event) => { scrolling.value = event.contentOffset.y; }); const imgStyle = useAnimatedStyle(() => { const interpolatedScale = interpolate( scrolling.value,[-300, 0],[3, 1],Extrapolate.CLAMP ); const interpolatedTranslate = interpolate( scrolling.value,[0, 300],[0, -300],Extrapolate.CLAMP ); return { transform: [ {translateY: interpolatedTranslate}, {scaleY: interpolatedScale}, {scaleX: interpolatedScale} ] }; }); return ( <> <Animated.Image source={require('sometitleimage.jpg')} style={[styles.header,imgStyle]} /> <Animated.ScrollView onScroll={scrollHandler} > <View style={styles.headerPlaceholder} /> <View style={styles.content} /> </Animated.ScrollView> </> ); }
In this example, the ScrollView binds the Y scroll value (content offset) to the animation value using Reanimated’s useAnimatedScrollHandler Hook. This animation value is then interpolated with the interpolate function of Reanimated 2. This is done inside a useAnimatedStyle hook.
This setup makes the animation work, without ever having to send scroll values over the bridge to the JavaScript thread. The whole animation runs inside the worklet context on the UI thread. This makes the animation extremely performant.
Of course, Reanimated 2 offers a wide range of other options. It is possible to use spring-based animations, velocity-based animations, delay or repeat animations, and run animations in sequence, just to name a few.
Since a complete guide on Reanimated 2 would go beyond the scope of this book, please have a look at the official documentation (https://bit.ly/prn-reanimated-docs) and the API reference (https://bit.ly/prn-reanimated-api-reference).
To complete this section, we will have a look at the pros and cons of Reanimated 2.
Reanimated 2 is, by far, the most advanced and complete solution for animations in React Native. There are a lot of reasons to use Reanimated 2. Here is a list of the most important ones:
Reanimated 2 is a very good library, but before using it, you should have a look at the following cons:
If you have an app with a lot of animations, more complex animations, and/or animated layout properties, I would definitely recommend using Reanimated 2. If you only use basic animations, which can be achieved with the internal Animated API, you don’t need Reanimated and can stick to the Animated API.
While Reanimated 2, the Animated API, and even react-native-animatable have a very similar approach, the next library we will get to know works completely differently. Let’s take a look at Lottie.
Lottie is a completely different approach to animations in app and web development. It allows you to render and control prebuilt vector animations. The following figure shows the process of how a Lottie animation is created and played:
Figure 6.3 – The workflow when animating with Lottie
Essentially, Lottie consists of a player, which in the case of React Native is the lottie-react-native library. This library expects a JSON file of a Lottie animation. This file is created with Adobe After Effects (a professional animation software) and exported to JSON with the Bodymovin plugin.
This process completely changes the way we work with animations in apps. The developer is no longer responsible for creating the animations; they only have to include the JSON file. This can save a huge amount of time when working with very complex animations.
All of this becomes clearer when looking at a simple Lottie animation.
The following code example shows the implementation of a loading animation with Lottie:
import React from 'react'; import { View, StyleSheet } from 'react-native'; import LottieView from 'lottie-react-native'; const App = () => { return ( <View style={styles.center}> <LottieView source={require('loading-animation.json')} style={styles.animation} autoPlay/> </View> ); }; const styles = StyleSheet.create({ center: { flex: 1, alignItems: 'center', justifyContent: 'center' }, animation: { width: 150, height: 150 } }); export default App;
This is all the code that is needed to include a loading animation, no matter how complex the animation is. LottieView is imported from the lottie-react-native library and is placed where the animation should occur. The Lottie JSON file is passed as source to LottieView, which can be styled via the style property like a regular React Native view.
However, lottie-react-native is not just a simple player. It gives you programmatic control over the animation. You can start and stop the animation, autoplay it when it loads, and loop it so that it starts again after completion. The last one is especially useful for loading animations.
The best feature of lottie-react-native is that it is possible to bind the progress of an animation to an Animated.Value component of the React Native Animated API. This opens up a lot of different use cases such as Lottie animations running time or spring-based. You can also use easing or create Lottie animations running based on user interaction.
The following code example shows you how to create a Lottie animation driven by an Animated.Value component that is bound to the Y scroll value of a React Native ScrollView:
const App = () => { const scrolling = useRef(new Animated.Value(0)).current; let interpolatedProgress = scrolling.interpolate({ inputRange: [-1000, 0, 1000], outputRange: [1, 0, 1], extrapolate: 'clamp', }); return ( <View style={styles.container}> <Animated.ScrollView onScroll={Animated.event( [{ nativeEvent: { contentOffset: { y: scrolling, }, }, }], { useNativeDriver: true }, )} scrollEventThrottle={16}> <LottieView source={require('looper.json')} style={styles.animation} progress={interpolatedProgress}/> </Animated.ScrollView> </View> ) }
In this example, the Y scroll value of the ScrollView is bound to an Animated.Value component in the onScroll function of the ScrollView. Then, the Animated.Value component is interpolated to get an interpolatedProgress between 0 and 1. This interpolatedProgess is passed to LottieView as a progress property.
Lottie also supports animations of the React Native Animated API, that use the native driver. This is very important for performance reasons. For more on this, please read the Using the internal Animated API of React Native section of this chapter.
While Lottie animations are very easy to include for the developer, someone has to create the Lottie JSON files that contain the animations. There are three ways to get Lottie animation files:
Now that we have a good understanding of how Lottie animations in React Native work, let’s have a look at the pros and cons.
Since the Lottie approach is completely different, there are huge pros and cons you should keep in mind when you consider using Lottie as the animation solution for your project.
The following pros stand out when using Lottie:
However, Lottie also comes with the following cons:
Lottie is an awesome option to include high-quality animations in a React Native project. Especially for complex loading animations, micro-animations, or any animation where no complete programmatic control is needed, Lottie is a great solution.
In this chapter, you learned about the general architectural challenge when it comes to animations in React Native. You understood that there are different solutions to overcome this challenge and create high-quality and performant animations. With Animated, react-native-animatable, Reanimated, and Lottie, we looked at the best and the most widely used animation solutions for React Native’s on-screen animations.
This is important because you will have to use animations in your app to create a high-quality product, and such animation libraries are the only way to create high-quality and performant animations in React Native.
In the next chapter, you will learn how to handle user gestures and also work with more complex user gestures to do different things – for example, to drive animations.
44.211.24.175