React Native

An Introduction To React-Native-Reanimated

If you are familiar with animations on React-Native, you probably have dealt with the Animated API provided by default.

Krzysztof Magiera, a core contributor of react-native, has released recently the version 1 of a 1,5k star ? project for making new kind of animations: react-native-reanimated.

To sum up, this library provides a nicer API along with substantial performance improvements because all the animations run on the native side. I advise you to check this link for a complete overview of the advantages.

The Challenge

As an introduction to the reanimated API, we are going to implement a collapsible scrollview header. When the user scrolls down the header of the page disappears and then appears again when the user scrolls up. Still, a draw is more explanative than a text, here is an example on Airbnb's app:

Let's get started!

Install

First, we have to install react-native-reanimated:

++pre>++code class="has-line-data" data-line-start="22" data-line-end="25">npm install react-native-reanimated
react-native link react-native-reanimated
++/code>++/pre>

A utility that I like to add along with reanimated is react-native-redash which provides some useful functions. You can see it as a lodash for react-native-reanimated.

++pre>++code class="has-line-data" data-line-start="29" data-line-end="31">npm install react-native-redash
++/code>++/pre>

Architecture

We are going to architecture our solution in 3 parts:

  • A parent View 1??
  • A header placed at position absolute. The challenge is to animate his translation according to the scroll 2??
  • A ScrollView (with a list of items) that is going to drive the animation 3??

 

Code

Following the architecture scheme, our code should have to start a parent which contains a header and a list:

++pre>++code class="has-line-data" data-line-start="48" data-line-end="64">/* Parent.js */

import * as React from "react"
import { View } from "react-native"
import { Header } from "./Header"
import { List } from "./List"

export const Parent = () => {
 return (
<View>
<Header />
<List />
</View>
 )
}
++/code>++/pre>

As for the header, it is going to have an absolute position, because we want the ScrollView to take the entire screen and not bothered by the header:

++pre>++code class="has-line-data" data-line-start="68" data-line-end="94">/* Header.js */

import * as React from "react"
import { View } from "react-native"

const HEADER_HEIGHT = 60

export const Header = () => {
 return (
<View
     style={ {
       height: HEADER_HEIGHT,
       position: "absolute",
       top: 0,
       width: "100%",
       zIndex: 2,
       backgroundColor: "#ffb74d",
       justifyContent: "center",
       alignItems: "center",
     } }
>
<Text>Header</Text>
</View>
 )
}
++/code>++/pre>

Finally, we have the list component fetching some Lorem Pictum images placed inside a ScrollView. We add here the prop ++code>scrollEventThrottle++/code> to have a smooth animation while scrolling.

++pre>++code class="has-line-data" data-line-start="98" data-line-end="120">/* List.js */

import * as React from "react"
import { ScrollView, Image } from "react-native"

export const List = () => {
 return (
<ScrollView
     scrollEventThrottle={16}
     contentContainerStyle={ { paddingTop: 50 } }
>
     {Array.from({ length: 10 }, (v, k) => (
<Image
         style={ { width: "100%", height: 200, marginTop: 50 } }
         key={k + ""}
         source={ { uri: "https://picsum.photos/200/300" } }
       />
     ))}
</ScrollView>
 )
}
++/code>++/pre>

Now this is how your app should look like:


But we are lacking the scroll driving the animation, so let's add it using react-native-reanimated. First, we define an animated value in the parent file that is going to be shared with the children. We are going to call this value ++code>y++/code> since it is going to represent the translation in the vertical axis of the header.

++pre>++code class="has-line-data" data-line-start="130" data-line-end="150">/* Parent.js */

import * as React from "react"
import { View } from "react-native"
import { Header } from "./Header"
import { List } from "./List"
import Animated from "react-native-reanimated"

export const Parent = () => {
 // Create an "y" animated value and pass it down to the children
 const y = new Animated.Value(0)

 return (
<View>
<Header y={y} />
<List y={y} />
</View>
 )
}
++/code>++/pre>

Now, let's drive this "y" value by the ScrollView. This last one got a prop called ++code>onScroll++/code> that is used to bind the scroll to a value. We are also going to use ++code>react-native-redash++/code> here to get the benefit of a concise syntax for this binding:

++pre>++code class="has-line-data" data-line-start="154" data-line-end="180">/* List.js */

import * as React from "react"
import { Image } from "react-native"
import { onScroll } from "react-native-redash"
import Animated from "react-native-reanimated"

export const List = props => {
 return (
   // Use onScroll to update the y value
<Animated.ScrollView
     onScroll={onScroll({ y: props.y })}
     scrollEventThrottle={16}
     contentContainerStyle={ { paddingTop: 50 } }
>
     {Array.from({ length: 10 }, (v, k) => (
<Image
         style={ { width: "100%", height: 200, marginTop: 50 } }
         key={k + ""}
         source={ { uri: "https://picsum.photos/200/300" } }
       />
     ))}
</Animated.ScrollView>
 )
}
++/code>++/pre>

Finally, let's translate the header according to the ++code>y++/code> animated value. We need to convert the previous ++code>View++/code> to an ++code>Animated.View++/code> in order to animate that part of the UI. Then, we just need to pass the y value to the translateY key in the transform property. This is going to translate the ++code>Animated.View++/code> according to the y value updated in real-time by the ++code>ScrollView++/code>

++pre>++code class="has-line-data" data-line-start="184" data-line-end="214">/* Header.js */

import * as React from "react"
import { Text } from "react-native"
import Animated from "react-native-reanimated"

const HEADER_HEIGHT = 60

export const Header = props => {
 return (
   // Use Animated.View instead of View
<Animated.View
     style={ {
       height: HEADER_HEIGHT,
       position: "absolute",
       top: 0,
       width: "100%",
       zIndex: 2,
       backgroundColor: "#ffb74d",
       justifyContent: "center",
       alignItems: "center",
       /* Translate the View according to y */
       transform: [{ translateY: props.y }],
     } }
>
<Text>Header</Text>
</Animated.View>
 )
}
++/code>++/pre>

Good, so one last thing to be done is to fix the direction of the translation and tell the header to not leave the boundaries of his height. You may be familiar with the ++code>Animated.interpolate++/code> from ++code>react-native++/code> and the one in ++code>react-native-reanimated++/code> works pretty much the same. In our case we are going to do:

++pre>++code class="has-line-data" data-line-start="223" data-line-end="228">const translateY = interpolate(props.y, {
 inputRange: [0, HEADER_HEIGHT],
 outputRange: [0, -HEADER_HEIGHT]
});
++/code>++/pre>

Which can be translated by: "Whenever y equals X, translateY should equal minus X". Here we are fixing the orientation of the header's translation.
The last part is to tell the header to not leave the boundaries of his position and to always stay on defined edges when you scroll. The collapsible scrollview header is the typical example of this behavior, and we are going to use the diffClamp method.

++pre>++code class="has-line-data" data-line-start="233" data-line-end="235">const diffClampY = diffClamp(props.y, 0, HEADER_HEIGHT);
++/code>++/pre>

Which means: "Create a new Animated value that is limited between 2 values (0 and HEADER_HEIGHT). It uses the difference between the last value so even if the value is far from the bounds it will start changing when the value starts getting closer again."
So, let's put the glue about all the concepts we just told, and this is what is going to be the header file:

++pre>++code class="has-line-data" data-line-start="240" data-line-end="274">/* Header.js */

import * as React from "react"
import { Text } from "react-native"
import Animated from "react-native-reanimated"

const HEADER_HEIGHT = 60
const { diffClamp, interpolate } = Animated

export const Header = props => {
 const diffClampY = diffClamp(props.y, 0, HEADER_HEIGHT)
 const translateY = interpolate(diffClampY, {
   inputRange: [0, HEADER_HEIGHT],
   outputRange: [0, -HEADER_HEIGHT],
 })
 return (
<Animated.View
     style={ {
       height: HEADER_HEIGHT,
       position: "absolute",
       top: 0,
       width: "100%",
       zIndex: 2,
       backgroundColor: "#ffb74d",
       justifyContent: "center",
       alignItems: "center",
       transform: [{ translateY: translateY }],
     } }
>
<Text>Header</Text>
</Animated.View>
 )
}
++/code>++/pre>

And so finally, here it comes, this is how we get as a final animation 🎉 🎉 🎉.

Conclusion

Congratulations! You have made your first animation with the reanimated API by making the collapsible header scrollview animation. I hope you will enjoy the powerfulness of this library.


Go to the Github Repository to have a quick overview of the code developed in this article.

Développeur mobile ?

Rejoins nos équipes