React-native's Animated library is a simple and powerful library to add animations in your react-native app. But it has few limitations when it comes to gesture-based interactions. To overcome these limitations, we have an alternative, react-native-reanimated library by Krzysztof Magiera

Using react-native-reanimated, we declare our animations in JavaScript, in our React Native application code, but run them on UI thread. It provides much greater flexibility and control over our animations and is also backward compatible with the Animated library. But this flexibility comes at a cost of declarative syntax which involves a bit of learning curve.

In this blog, we'll get familiar with the declarative syntax and some basic blocks of react-native-reanimated by building a simple Accordion component. Our goal of this blog is to understand the fundamentals and get used to the declarative syntax of the library.

So without further ado, let's get started.

Pre-requisites:

  • Basic knowledge of react-native
  • Basic knowledge of Animated library and the workflow of creating an animation.

Initialize project:

Let's create our project by running

react-native init rnr_accordion

We will be using the following dependencies to create our app:

Next, we install react-native-reanimated as follows:

yarn add react-native-reanimated

For iOS, we install pod dependencies for react-native-reanimated as follows:

cd ./ios && pod install

In our root/src/components, we create a component called Accordion. Our folder structure looks as follows:

folder-structure

In our App.js, we call our Accordion component as follows:

Create a basic structure for Accordion:

Let us create a basic structure of our Accordion. Our Accordion will have 3 Rows, each Row will have a title and content part. The title part will always be visible, interactive and will animate the visibility of the content.

In our Accordion class, add following code:

Let us analyze our code a bit.

  • Lines 2 and 3 import APIs and components needed.
  • Lines 5-31 create Row component taking title and content as props. Note, it is rendered inside Animated.View for animation.
  • Lines 9-25 is our title part. It is made of TouchableOpacity so that on its onPress event we will trigger animation later.
  • Lines 26-29 is our content part. It is inside ScrollView so that it can accommodate larger content.
  • Lines 55-66 creates three Rows.

With this, we have the basic structure of our Accordion ready.

basic Accordion structure

Next, we add state variables for each Row as follows,

We now add event handlers for the title's onPress event which will help us later in toggling respective content part along with animation.

We just toggle the showContent flag of the respective Row which has been pressed and set showContent flags of other Rows to false. This can be interpreted as closing another Row's opened contents when a particular Row has been pressed. These flags will help achieve correct behaviour along with animation later.

Next, we pass respective methods as prop toggleContent to Row component

and call as title's onPress event handler.

Animate Content's height:

Now, we'll use react-native-reanimated to animate toggling of content's height.

Extract the following nodes from Animated API of react-native-reanimated after import statements. We'll eventually learn each of its use.

Next, in our constructor, we create Animated.Value nodes for each row initialized to 50. Observe that this value is equal to the height of the Row's title container. This is so because we want our title to be always visible and our content to be hidden initially. We'll bind these nodes to height style property of the rows.

We pass Value nodes of height to respective rows,

and bind them to the height of the Row in the style object of Animated.View

This way, we hide the content part for each Row by setting Row's height equal to title's height.

content hidden only Rows visible

To apply animation, we need timing function of react-native-reanimated which animates a value over time using some Easing function. We define a wrapper function called runTiming inside which we will define configurations for our animation and return animation transformations.

Initially, we'll animate the first Row so that it becomes easy to understand what's going on under the hood.

So, in first Row's toggleContent() i.e toggleRow1Content() we call runTiming() before setState()

runTiming takes 3 parameters.

  • clock: needed by timing function to drive animation.
  • value: this will be the initial value of position node which will be the minimum height of Row i.e 50.
  • dest: this will be the final value of position node. We will set it to 200

Here is the code for runTiming()

  • Lines 2-7 initialize the state of our animation.

    • finished: gives information about whether the animation is finished i.e position node reaches the destination value or yet to be started. It is initialized to 0 stating animation is yet to be started.
    • position: is initialized to value. Position nodes are updated by timing function from value to dest frame by frame.
    • time and frameTime: represent the progress of the animation.
  • Lines 9-13 define config for animation which includes

    • duration: this will be duration our animation will last
    • toValue: this will be the final value of position node. It is initialized to 200 i.e. height of Row we want after the animation completes.
    • easing: defines Easing function for animation

Calling runTiming() returns a sequence of instructions or group of commands called block of nodes which describe animation and returns position node.
Lines 15-29 covers block nodes

  • Line 17: cond node is equivalent to if node in react-native-reanimated's declarative syntax. clockRunning(clock) checks whether the instance of clock we provided is started or not. Here, cond(clockRunning(clock), 0, [ ... ] is equivalent of
if(!clockRunning(clock)){


}
  • Lines 19-23 we initialize some nodes using set node when clock is not running. set node is equivalent of assignment opeartor =
    • Line 19: sets finished node to 0 meaning animation is not started yet
    • Line 20: sets time node to 0.
    • Line 21: sets position node to value
    • Line 22: sets frameTime node to 0
    • Line 23: sets toValue to dest

Finally, at line 24 we start the clock using startClock(clock)

  • Line 26: we call timing() with clock, state, and config passed as parameters.

  • Line 27: Again a cond block checking whether the animation is finished. If yes, we stop the clock. Stopping the clock ends the animation as well.

  • Line 28: we return state.position node value updated to 51(depends on config.duration) and which is mapped to heightRow1 variable that is provided to style props of the first Row.

Explanation:

  • This block of nodes runs again and again until the animation is finished and the clock is stopped.

  • The timing function updates state.position node (like 52,53 and so on)
    every frame, until config.toValue(200) is reached.

  • The updated state.position value is returned every frame and mapped to this.heightRow1.

  • The progress of animation is known with the help of state.frameTime and state.time which also helps to know whether config.duration is reached after every frame. You can go through the timing() function's code from here if you want to know how these calculations are done.

  • When the animation completes i.e. when the value of state.position equals the config.toValue, the timing() function updates the value of state.finished to 1. With this, clock() is stopped and the animation process ends.

This is how first Row's animation looks:

That's all about how animation is performed. Let's modify our code to achieve correct Accordion behaviour. We'll just update toggleContent() for first Row.The code for the other 2 Rows will be intuitive.

In toggleRow1Content() we add following code just above setting state variables,

Here, onPress of Row1, toggleRow1Content() is called. Inside toggleRow1Content() we check whether its content is opened or closed by checking showContent1 flag. If it is closed i.e showContent1 is false, we increase the Row height by calling runTiming() from initial value 50 to destination value 200. This increase in height makes our first Row's content part visible. Else, that means content of the Row is already opened, we decrease the Row height by calling runTiming() from initial value 200 to destination value 50 and thereby hiding its content. That's how we toggle the visibility of content by animating height and using our showContent flags.

Next, for the other two Row components, we check whether their content is open and hide them by calling runTiming() from initial value 200 to destination value 50.

Along similar lines, we update the toggleRow2Content() and toggleRow3Content() as follows:

and here we have our Accordion ready.

react-native-reanimated's declarative syntax may be difficult to grasp initially but these syntaxes help in performing animations in the UI thread. Hope this blog helps you in understanding these syntaxes in a better way by applying them.

You can clone this Github repo and experiment with the same.

Update: For the version of code using functional components and hooks, switch to the branch hooks-version in the repo.

In Part 2 we'll learn how to do translation transformation in react-native-reanimated by creating a shiny effect on progress bar.

Thank you for reading.

References: