The Widlarz Group Blog

Reanimated 2 - the new approach to creating animations in React Native

August 27, 2020

react native

reanimated 2

animations

hooks

Introduction

Recently, I’ve got a chance to work with Reanimated 2 library. The new version is completely different than v1 as it exposes React hook based API. This guide uses 2.0.0-rc.2 version and has been recently updated from alpha. We promise to keep updating each time a new version is released.

I must admit, that working with the new API was quite a pleasure, firstly the new hooks set is really intuitive for React developers, secondly, animations are smooth and performant. I encourage you to start using Reanimated 2 in your React Native projects and slowly forgetting about the old syntax. Slowly, because it’s still not in a stable production release, but by using it you can help improve the library.

In this article, we are not going to focus so much on theory. If you are looking for that kind of knowledge, I refer you to the great documentation. Our aim is to take a look at Reanimated 2 hooks and learn how to use them in code. We are going to build practical examples of animated sliders using the new Reanimated 2 API and our custom useSlider hook.

You don’t need to be familiar with the previous version to make animations with Reanimated 2!

Below you can see a preview of what we will build together, step by step.

Article demo animation

At the end of each step, there’s a link to the current code (in case you’ve got stuck).
Let’s start then!

Project Setup

There is a starter repo on Github for this tutorial. We start by cloning the repo

git clone -b starter git@github.com:TheWidlarzGroup/reanimated2-slider-article.git

then install packages

cd reanimated2-slider-article && yarn

and if you develop on iOS simulator:

cd ios && pod install

Intro

The starter project consists of 3 sliders. Each slider is built with 3 fundamental View components.

<View style={styles.slider}>
  <View style={styles.progress} />
  <View style={styles.knob} />
</View>
  1. slider - wrapper
  2. progress - absolute positioned View, for displaying slider progress
  3. knob - draggable element

Our first task is to animate Slider1 using Reanimated 2 hooks. Then we will build custom hook useSlider with reusable logic.

Let’s start our project

  yarn ios
  // or
  yarn android

And move to Slider1.js file

useSharedValue

Probably you are familiar with Animated.Value concept. When it comes to Reanimated 2 we use the useSharedValue hook to keep animation ‘state’. Shared means that the value is accessible across 2 threads: UI and JS Thread.

In our example we keep two values in ‘state’:

  • translateX - a distance from beginning to the end of the slider.
  • isSliding - a boolean which tells us whether we are currently sliding or not.
import { useSharedValue } from 'react-native-reanimated'

//...

const translateX = useSharedValue(0)
const isSliding = useSharedValue(false)

Our animations are based on changing shared values. What’s important to mention, to change shared values we use .value syntax:

For instance:

isSliding.value = true
// or
translateX.value = 100

useAnimatedGestureHandler

Reanimated 2 provides us with a hook useAnimatedGestureHandler to deal with Gesture Handlers easily. The hook accepts an object where we can configure events like:

  • onStart, onActive, onEnd, onCancel, onFail, onFinish

Each event has access to 2 parameters:

  • gesture event - an object that consists of information like translate, velocity, current position, and more, dependent on Gesture Handler we use.
  • context - a plain JS object to store data between events. We can keep in context any information we want.

In return from hook, we get an object, which needs to be passed to Gesture Handler (onGestureEvent).

In our example, we use PanGestureHandler as a wrapper for Knob to make it draggable across the slider.

// import {useSharedValue} from 'react-native-reanimated'
import Animated, {useSharedValue, useAnimatedGestureHandler} from 'react-native-reanimated'
import {PanGestureHandler} from 'react-native-gesture-handler'

//... configuring PanGestureHandler here

const onGestureEvent = useAnimatedGestureHandler({
  onStart: (_, ctx) => {
    ctx.offsetX = translateX.value
  },
  onActive: (event, ctx) => {
    isSliding.value = true
    translateX.value = event.translationX + ctx.offsetX
  },
  onEnd: () => {
    isSliding.value = false
  },
})

//... Wrapping our Knob with PanGestureHandler

<PanGestureHandler onGestureEvent={onGestureEvent}>
 <Animated.View style={styles.knob}/>
</PanGestureHandler>

useAnimatedStyle

The next and one of the most important hook is useAnimatedStyle one. Inside this hook, we define style properties we want to be animated. Then we need to pass whatever hook has returned to an Animated component as a style property.

In our case, we created two animated styles.

  1. scrollTranslationStyle - to show the Knob translation
  2. progressStyle - to animate progress bar when the knob is being dragged

We also tell RN, that our views are Animated.

// import Animated, {useSharedValue, useAnimatedGestureHandler} from 'react-native-reanimated'
import Animated, {
  useSharedValue,
  useAnimatedGestureHandler,
  useAnimatedStyle,
} from 'react-native-reanimated'

const scrollTranslationStyle = useAnimatedStyle(() => {
  return { transform: [{ translateX: translateX.value }] }
})

const progressStyle = useAnimatedStyle(() => {
  return {
    width: translateX.value + KNOB_WIDTH,
  }
})

return (
  <View style={styles.slider}>
    {/* <View style={styles.progress} /> */}
    <Animated.View style={[styles.progress, progressStyle]} />
    <PanGestureHandler onGestureEvent={onGestureEvent}>
      {/*  <Animated.View style={styles.knob} /> */}
      <Animated.View style={[styles.knob, scrollTranslationStyle]} />
    </PanGestureHandler>
  </View>
)

If you’ve followed to this point, you should be able to move the Knob.

Moving Knob

current code here.

Quick fix and “worklet” directive

There is a problem with our slider. We can move the knob beyond our slider. Let’s fix that by adding the clamp function, that would not allow the knob to leave defined bounds.

//...
onActive: (event, ctx) => {
  const clamp = (value, lowerBound, upperBound) => {
    return Math.min(Math.max(lowerBound, value), upperBound)
  }

  isSliding.value = true
  //translateX.value = event.translationX + ctx.offsetX
  translateX.value = clamp(
    event.translationX + ctx.offsetX,
    0,
    SLIDER_WIDTH - KNOB_WIDTH
  )
}

Indeed, there is no need to define a clamp function inside the useAnimatedGestureHandler. For cleanliness, we move function definition to a separate file utils.js. Also, don’t forget to import it in our Slider1.js file.

// utils.js

export const clamp = (value, lowerBound, upperBound) => {
  'worklet'
  return Math.min(Math.max(lowerBound, value), upperBound)
}

Perhaps you have noticed that the new string appeared - "worklet". There is no need to write "worklet" directive inside reanimated 2 hooks. Each of reanimated 2 hooks, uses worklet directive under the hood.

If we want to run Javascript functions on the UI thread, we need to place "worklet" directive on the top of a function definition. (similar to our clamp method)

Link to current code

useDerivedValue and useAnimatedProp

useDerivedValue - we use this hook, to calculate a new value, based on changing shared value. In our example we will calculate step and then return its value as a string, to animate it in AnimatedText component.

//import Animated, {useSharedValue, useAnimatedGestureHandler, useAnimatedStyle } from 'react-native-reanimated'
import Animated, {
  useSharedValue,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useDerivedValue,
} from 'react-native-reanimated'

const MAX_RANGE = 20

//...

const stepText = useDerivedValue(() => {
  const sliderRange = SLIDER_WIDTH - KNOB_WIDTH
  const oneStepValue = sliderRange / MAX_RANGE
  const step = Math.ceil(translateX.value / oneStepValue)

  return String(step)
})

then create a new file AnimatedText.js

useAnimatedProp - is similar to useAnimatedStyle. The difference is that we use useAnimatedProp when we want to animate properties, which are not style properties (background, opacity, etc.)

//AnimatedText.js

import * as React from 'react'
import Animated, { useAnimatedProps } from 'react-native-reanimated'
import { TextInput } from 'react-native-gesture-handler'

const AnimatedTextInput = Animated.createAnimatedComponent(TextInput)

const AnimatedText = ({ text }) => {
  const animatedProps = useAnimatedProps(() => {
    return {
      text: text.value,
    }
  })

  return (
    <AnimatedTextInput
      underlineColorAndroid="transparent"
      editable={false}
      value={text.value}
      animatedProps={animatedProps}
    />
  )
}

export default AnimatedText

and finally in our Slider1.js

import AnimatedText from './AnimatedText'

//...

// <Animated.View style={[styles.knob, scrollTranslationStyle]} />
<Animated.View style={[styles.knob, scrollTranslationStyle]}>
  <AnimatedText text={stepText} />
</Animated.View>
Animated Text animation

You can find the code for slider with animated text here.

Adding onDragComplete handler and runOnJS method

Often, we want to add some feedback when the knob is being dragged to the end of the slider. With Reanimated 2 it’s quite simple to implement. Let’s assume that dragging is completed just after we have dropped the knob, and the distance from the knob to the max bound is less than 3 pt.

We call the onDragSuccess function on the UI thread when we’ve finished dragging the knob. In that case, we need to wrap our function with the runOnJS method. If we forget about wrapping our Javascript callback, Reanimated 2 will inform us about that fact by displaying an appropriate error. You can read more about runOnJS here.

// import {StyleSheet, View} from 'react-native'
import { StyleSheet, View, Alert } from 'react-native'
// import Animated, { useSharedValue, useAnimatedGestureHandler, useAnimatedStyle, useDerivedValue } from 'react-native-reanimated'
import Animated, {
  useSharedValue,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useDerivedValue,
  runOnJS,
} from 'react-native-reanimated'

const onDraggedSuccess = () => {
  Alert.alert('dragged')
}

//...

onEnd: () => {
  isSliding.value = false

  if (translateX.value > SLIDER_WIDTH - KNOB_WIDTH - 3) {
    runOnJS(onDraggedSuccess)()
  }
}
onDragComplete

Code with added onDraggedSuccess here

Custom hook - useSlider

So far, we have built a slider animation using Reanimated 2 hooks. As the React world loves hooks, we will move a step forward and create custom hook - useSlider. We will use this useSlider hook to animate our second slider. That would be a good test for how our shared values behave when we passed them across components.

Let’s create a new file for our hook useSlider.js. We will take our animation logic from Slider1. To increase the reusability, we will be passing following arguments to our hook:

  1. sliderWidth (number)
  2. knobWidth (number),
  3. onDraggedSuccess (callback).
  4. maxRange (number) - to track a current step, optional
  5. initialValue (number) - if we want to set the initial position of knob inside a slider, optional

The useSlider hook returns:

  1. onGestureEvent
  2. values (translateX, isSliding, stepText)
  3. styles (scrollTranslationStyle , progressStyle)
// useSlider.js

import {
  useSharedValue,
  useAnimatedGestureHandler,
  useAnimatedStyle,
  useDerivedValue,
  runOnJS,
} from 'react-native-reanimated'
import { clamp } from './utils'

export const useSlider = (
  sliderWidth,
  knobWidth,
  onDraggedSuccess,
  maxRange = 10,
  initialValue = 0
) => {
  const SLIDER_RANGE = sliderWidth - knobWidth
  const STEP = SLIDER_RANGE / maxRange ?? 1

  const translateX = useSharedValue(STEP * initialValue)
  const isSliding = useSharedValue(false)

  const onGestureEvent = useAnimatedGestureHandler({
    onStart: (_, ctx) => {
      ctx.offsetX = translateX.value
    },
    onActive: (event, ctx) => {
      isSliding.value = true

      translateX.value = clamp(
        event.translationX + ctx.offsetX,
        0,
        SLIDER_RANGE
      )
    },
    onEnd: () => {
      isSliding.value = false

      if (translateX.value > SLIDER_RANGE - 3) {
        runOnJS(onDraggedSuccess)()
      }
    },
  })

  const scrollTranslationStyle = useAnimatedStyle(() => {
    return { transform: [{ translateX: translateX.value }] }
  })

  const progressStyle = useAnimatedStyle(() => {
    return {
      width: translateX.value + knobWidth,
    }
  })

  const stepText = useDerivedValue(() => {
    const step = Math.ceil(translateX.value / STEP)
    return String(step)
  })

  return {
    onGestureEvent,
    values: {
      isSliding,
      translateX,
      stepText,
    },
    styles: {
      scrollTranslationStyle,
      progressStyle,
    },
  }
}

Animate Slider2 with useSlider hook

To show a more interesting example, we will animate Slider2 with our useSlider hook. The idea of this example comes from a slider, which already has been animated with Reanimated 1, by William Candilon in this video

Firstly, uncomment in App.js these lines:

import Slider2 from './Slider2';

<View style={{margin: 50}} />
<Slider2 />

then let’s use our custom hook in Slider2.js

// import {StyleSheet, View, Text} from 'react-native'
import { StyleSheet, View, Alert } from 'react-native'
import Animated from 'react-native-reanimated'
import { PanGestureHandler } from 'react-native-gesture-handler'
import { useSlider } from './useSlider'
import AnimatedText from './AnimatedText'

// ...

const Slider2 = () => {
  const onDragCompleteHandler = () => {
    Alert.alert(stepText.value, String(translateX.value))
  }

  const {
    onGestureEvent,
    values: { translateX, isSliding, stepText },
    styles: { scrollTranslationStyle, progressStyle },
  } = useSlider(SLIDER_WIDTH, KNOB_WIDTH, onDragCompleteHandler, STEP)

  return (
    <>
      <View style={styles.slider}>
        <Animated.View style={[styles.progress, progressStyle]} />
        <PanGestureHandler onGestureEvent={onGestureEvent}>
          <Animated.View style={[styles.knobContainer, scrollTranslationStyle]}>
            <Knob isSliding={isSliding} />
          </Animated.View>
        </PanGestureHandler>
      </View>

      <View style={{ marginTop: 40 }}>
        <AnimatedText text={stepText} />
      </View>
    </>
  )
}

Also in Knob.js we animate the opacity of two images:

  1. penguin with open eyes (assets/up.png)
  2. penguin with closed eyes (assets/down.png)

based on isSliding.value which can be true or false.

import Animated, { useAnimatedStyle } from 'react-native-reanimated'

const Knob = ({ isSliding }) => {
  const knobUpStyle = useAnimatedStyle(() => {
    return {
      opacity: isSliding.value ? 1 : 0,
    }
  })

  const knobDownStyle = useAnimatedStyle(() => {
    return {
      opacity: isSliding.value ? 0 : 1,
    }
  })

  return (
    <View style={styles.container}>
      {/* <Image source={require("./assets/up.png")} style={styles.image} /> */}
      <Animated.Image
        source={require('./assets/up.png')}
        style={[styles.image, knobUpStyle]}
      />
      {/*  <Image source={require("./assets/down.png")} style={styles.image} /> */}
      <Animated.Image
        source={require('./assets/down.png')}
        style={[styles.image, knobDownStyle]}
      />
    </View>
  )
}

If you’ve done everything correctly, you could be able to see that animation

Slider 2 animation

Code from this step

Interpolation

In this part, we will add some rotation styling to our penguin and interpolate color of the progress bar. To achieve this we use the interpolate method from the Reanimated 2 library.

Interpolation helps you to create good animations by mapping input ranges to output ranges. We use it like this:

// Example

const value = interpolate(
  sharedValue, // e.g translateX.value
  inputRange, // e.g : [0, 100]
  outputRange, // e.g : [360, 1000]
  extrapolation // Clamp, Extend, Identity
)

In Slider2.js let’s add

// import Animated from 'react-native-reanimated'
import Animated, {useAnimatedStyle, interpolate, Extrapolate} from 'react-native-reanimated'

// ..
// below useSlider hook

  const rotateStyle = useAnimatedStyle(() => {
    const rotate = interpolate(
      translateX.value,
      [0, SLIDER_RANGE], // between the beginning and end of the slider
      [0, 4 * 360], // penguin will make 4 full spins
      Extrapolate.CLAMP,
    )

    return {
      transform: [{rotate: `${rotate}deg`}],
    }
  })

  //..

  // <Knob isSliding={isSliding} />
  <Knob isSliding={isSliding} rotateStyle={rotateStyle} />

and in Knob.js

// const Knob = ({isSliding}) => {
const Knob = ({isSliding, rotateStyle}) => {

// <View style={styles.container}>
<Animated.View style={[styles.container, rotateStyle]}>
{/* ... */}
</Animated.View>

Color Interpolation

There is a handy way in Reaniamted2 to interpolate colors based on shared value changes. For that purpose, we use the interpolateColor method. Its way of working is similar to the interpolation method, except that this time our output array consists of colors. The creators of the library made sure that it is possible to pass colors in the most popular formats. Let’s use RGB format here.

// Slider2.js

// import Animated, {useAnimatedStyle, interpolate, Extrapolate } from 'react-native-reanimated'
import Animated, {useAnimatedStyle, interpolate, Extrapolate, interpolateColor} from 'react-native-reanimated'

//..

const backgroundStyle = useAnimatedStyle(() => {
  const backgroundColor = interpolateColor(
    translateX.value,
    [0, SLIDER_RANGE],
    ['rgb(129,212,250)', 'rgb(3,169,244)'],
  );

  return {
    backgroundColor,
  };
});

//...

// <Animated.View style={[styles.progress, progressStyle]}/>
<Animated.View style={[styles.progress, progressStyle, backgroundStyle]}/>

At the end of this section, we are able to see the penguin rotation and the progress background color animation.

Rotation and progress color animations

Code can be found here

withTiming and Easing functions

This is the last part of the article. We will make a small experiment by moving our penguin programmatically by changing trasnlateX.value, in the button onPress method. Additionally, we will use timing functions from Reanimated 2.

Let’s take a look, how we can animate shared values by the withTiming method.

//  Example usage of withTiming

//  withTiming takes 3 arguments
//  1. new target value
//  2. animation config object
//  3. callback function, which will be executed right after the animation is complete

translate.value = withTiming(
  300,      
  {        
    duration: 1000,
    easing: Easing.linear,
  },
  () => {  
    // do sth
  },
);

And in our project add these lines:

// Slider2.js

// import {StyleSheet, View, Alert} from 'react-native'
import { StyleSheet, View, Alert, Button } from 'react-native'
// import Animated, {useAnimatedStyle, interpolate, Extrapolate, interpolateColor } from 'react-native-reanimated'
import Animated, {
  useAnimatedStyle,
  interpolate,
  Extrapolate,
  interpolateColor,
  withTiming,
  Easing,
} from 'react-native-reanimated'

//... in return statement, below Animated Text let's add 2 Buttons
<View>
  <Button
    title='Slide to beginning'
    onPress={() => {
      isSliding.value = true
      translateX.value = withTiming(
        0,
        {
          duration: 3000,
          easing: Easing.bounce,
        },
        () => {
          isSliding.value = false
        },
      )
    }}
  />
  <Button
    title='Slide to end'
    onPress={() => {
      isSliding.value = true
      translateX.value = withTiming(
        SLIDER_RANGE,
        {
          duration: 1000,
          easing: Easing.linear,
        },
        () => {
          isSliding.value = false
        },
      )
    }}
  />
</View>

Now, you can see two extra buttons, and move the slider by clicking them, like on the video below.

Buttons to animate programatically
You can find the whole code here.

Reanimated 2 offers us more methods to create interesting animations like withSpring, withDelay, withRepeat, cancelAnimation, and more. I encourage you to give it a try. We have left a Slider3.js file, with a static slider. Feel free to make your experiment using Reanimated 2 API. Hope you guys have fun using it.

Conclusions

To start writing code with the new Reanimated 2 API, you don’t need to have prior experience with react-native-reanimated library. The library offers us the powerful API to create eye-catching and effective animations.

The first version of this article was created in August 2020 and used the alpha version. Since then, the library has been in the intensive development phase all the time. The developers have made every effort to ensure that the API was stable and did not cause errors. On the day of writing the article update, the library has the “release candidate” status. I hope we will see it in the stable release as soon as possible.

In my opinion, Reanimated2 will be setting new trends and will be the first choice by React Native developers when it comes to choosing a library for building animations.

By reading this article, you’ve gained some practical knowledge about how you can use the new Reanimated 2 API. I hope you’ve enjoyed reading it. For more examples, I refer you to the stimulating YT channel


Written by Bartek Bajda.