First steps with React Native animations

cellphone inside react native molecule logo

Recently I was given the opportunity to work in a React Native project, as a developer, most of my experience was with React, so I was thrilled with expanding my knowledge in this area.

As I started studying the platform, I got really interested in how to make animations using React Native, and in this blogpost, I am going to share my experience, and create a guide for beginners on how to start animating your React Native apps. In this guide, we are going to create a Burger Button that when activated spins and turns into an X button. For this, introductory knowledge of React Native will be required.

To follow the code, use the instructions in Getting Started with React Native, and then we can start our guide.

The Library

The library we are going to use is the Animated API, from React Native’s standard library. It has two drivers for animating, the first one uses the JavaScript engine created by React Native, and the second one creates animations using the native driver. The library is still being worked on and intends on expanding the number of native animations that it provides.

There are different techniques provided by this library to animate your component’s properties, the most basic being `Animated.timing`. To show this we are going to begin creating a simple burger button.

import React from 'react'
import { StyleSheet, View, TouchableWithoutFeedback } from 'react-native'

const App = () => (
  <View style={styles.container}>
    <TouchableWithoutFeedback onPress={() => {}}>
      <View style={styles.burgerButton}>
        <View style={styles.inner} />
        <View style={styles.inner} />
        <View style={styles.inner} />
      </View>
    </TouchableWithoutFeedback>
  </View>
)

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center'
  },
  burgerButton: {
    display: 'flex',
    flexDirection: 'column',
    justifyContent: 'space-between',
    alignItems: 'center',
    width: 100,
    height: 100,
    paddingVertical: 20,
    borderRadius: 50,
    backgroundColor: 'green'
  },
  inner: {
    width: '60%',
    height: 10,
    borderRadius: 10,
    backgroundColor: 'black'
  }
})

export default App

 

 

Inactive button

 

Any resemblance with a music playing app is purely coincidental. But before we can begin animating this component, we have to add a wrapper to the views we want to animate, so we can dynamically change its properties. This wrapper is provided by Animated, and it needs to be used for all of the components you wish to animate.

import React from 'react'
import {
  [...],
  Animated
} from 'react-native'

const App = () => (
  <View style={styles.container}>
    <TouchableWithoutFeedback onPress={() => {}}>
      <View style={styles.burgerButton}>
        <Animated.View style={styles.inner} />
        <Animated.View style={styles.inner} />
        <Animated.View style={styles.inner} />
      </View>
    </TouchableWithoutFeedback>
  </View>
)

[...]

Now that we have setup the basis, we are going to use the `Animated.timing` function to begin animating the relevant properties. This function takes a property with a value created by Animated’s method Animated.Value, and updates it according to instructions. First we are going to translate the upper and lower parts of the button to the middle. Why we need to do this for our animation will be explained later.

 

[...]
const App = () => {
  const [activated, setActivated] = useState(false)
  const [upperAnimation, setUpperAnimation] = useState(new Animated.Value(0))
  const [lowerAnimation, setLowerAnimation] = useState(new Animated.Value(0))

  const startAnimation = () => {
    setActivated(!activated)

    Animated.timing(upperAnimation, {
      toValue: activated ? 0 : 25,
      duration: 300
    }).start()
    Animated.timing(lowerAnimation, {
      toValue: activated ? 0 : -25,
      duration: 300
    }).start()
  }

  const animatedStyles = {
    lower: {
      transform: [
        {
          translateY: lowerAnimation
        }
      ]
    },
    upper: {
      transform: [
        {
          translateY: upperAnimation
        }
      ]
    }
  }

  return (
    <View style={styles.container}>
      <TouchableWithoutFeedback onPress={startAnimation}>
        <View style={styles.burgerButton}>
          <Animated.View style={[styles.inner, animatedStyles.upper]} />
          <Animated.View style={styles.inner} />
          <Animated.View style={[styles.inner, animatedStyles.lower]} />
        </View>
      </TouchableWithoutFeedback>
    </View>
  )
}
[...]

 

 

 

With this, we have our first animation in React Native. As you can see, we created a function that uses Animated Timing to change the value of our translateY property, and passed this value to our Animated Views through an object called animatedStyles.

Interpolation

Now we are going to see how to animate more complicated properties, such as color, and rotation. To achieve such animations, we are going to have to use the mathematical concept of Interpolation, which consists of constructing new data points within the range of a discrete set of known data points (according to Wikipedia).

Fortunately, we don’t need any mathematical knowledge for this, we can use Animated to interpolate our data, which means, it can calculate which colors would make sense in the animation between two given colors. For example, this is how it would look like if we wanted our button to change colors from green to red when activated. Additionally, we are going to use interpolation to clean up our code, and remove the repeated values in the translate animations.

 

[...]
const App = () => {
  const [activated, setActivated] = useState(false)
  const [animation, setAnimation] = useState(new Animated.Value(0))

  const startAnimation = () => {
    const toValue = activated ? 0 : 1

    setActivated(!activated)
    Animated.timing(animation, {
      toValue,
      duration: 300
    }).start()
  }

  const animatedStyles = {
    lower: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, -25]
          })
        }
      ]
    },
    upper: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 25]
          })
        }
      ]
    },
    burgerButton: {
      backgroundColor: animation.interpolate({
        inputRange: [0, 1],
        outputRange: ['green', 'red']
      })
    }
  }

  return (
    <View style={styles.container}>
      <TouchableWithoutFeedback onPress={startAnimation}>
        <Animated.View
          style={[styles.burgerButton, animatedStyles.burgerButton]}
        >
          <Animated.View style={[styles.inner, animatedStyles.upper]} />
          <Animated.View style={styles.inner} />
          <Animated.View style={[styles.inner, animatedStyles.lower]} />
        </Animated.View>
      </TouchableWithoutFeedback>
    </View>
  )
}
[...]

 


To interpolate our data, we have to use our animated property’s method `interpolate`, and pass into it an input range and an output range. The input range must be the value of the range of our animated property, and output range the initial and final value of the component’s property we wish to animate.

Interpolate supports mapping strings, this way, you can pass rotation values, and colors (either by names or RGB hex codes). To continue the animation of our button, now we are going to make the middle part of the button disappear as the animation occurs.

[...]
const App = () => {
  const [activated, setActivated] = useState(false)
  const [animation, setAnimation] = useState(new Animated.Value(0))

  const startAnimation = () => {
    const toValue = activated ? 0 : 1

    setActivated(!activated)
    Animated.timing(animation, {
      toValue,
      duration: 300
    }).start()
  }

  const animatedStyles = {
    lower: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, -25]
          })
        }
      ]
    },
    upper: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 25]
          })
        }
      ]
    },
    middle: {
      height: animation.interpolate({
        inputRange: [0, 1],
        outputRange: [10, 0]
      })
    },
    burgerButton: {
      backgroundColor: animation.interpolate({
        inputRange: [0, 1],
        outputRange: ['green', 'red']
      })
    }
  }

  return (
    <View style={styles.container}>
      <TouchableWithoutFeedback onPress={startAnimation}>
        <Animated.View
          style={[styles.burgerButton, animatedStyles.burgerButton]}
        >
          <Animated.View style={[styles.inner, animatedStyles.upper]} />
          <Animated.View style={[styles.inner, animatedStyles.middle]} />
          <Animated.View style={[styles.inner, animatedStyles.lower]} />
        </Animated.View>
      </TouchableWithoutFeedback>
    </View>
  )
}
[...]

 

Additionally, we can pass an array with more than only two numbers to both input and output range parameters, with this we could make an animation that has different interactions in the output depending on the range of the input, like an animation that is really fast in the beginning, and slower in the end, similar to keyframe animations in CSS.

Composing Animations

For now we have only used Animated’s timing method, but there are other possibilities to animate your values. For the rotation we are going to use Animated.Spring, which is an animation function that does not take a duration, it only needs a toValue to work, but you can set friction and tension or speed and bounciness to improve your animation. The spring animation imitates a physical spring, and so, it overshoots the toValue that you provide, the amount it overshoots depends on the values we set before.

To make this animation run in parallel with our previous animation we need to use Animated’s parallel function, which takes an array of Animated methods and runs them in parallel. In our example we are going to use high tension and low friction on our spring, so it will be really bouncy:

 

[...]
const App = () => {
  const [activated, setActivated] = useState(false)
  const [animation, setAnimation] = useState(new Animated.Value(0))
  const [rotation, setRotation] = useState(new Animated.Value(0))
  const startAnimation = () => {
    const toValue = activated ? 0 : 1
    setActivated(!activated)
    Animated.parallel([
      Animated.timing(animation, {
        toValue,
        duration: 300
      }),
      Animated.spring(rotation, {
        toValue,
        friction: 2,
        tension: 140
      })
    ]).start()
  }
  const animatedStyles = {
    lower: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, -25]
          })
        },
        {
          rotate: rotation.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '45deg']
          })
        }
      ]
    },
    upper: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 25]
          })
        },
        {
          rotate: rotation.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '-45deg']
          })
        }
      ]
    },
    middle: {
      height: animation.interpolate({
        inputRange: [0, 1],
        outputRange: [10, 0]
      })
    },
    burgerButton: {
      backgroundColor: animation.interpolate({
        inputRange: [0, 1],
        outputRange: ['green', 'red']
      })
    }
  }
  return (
    <View style={styles.container}>
      <TouchableWithoutFeedback onPress={startAnimation}>
        <Animated.View
          style={[styles.burgerButton, animatedStyles.burgerButton]}
        >
          <Animated.View style={[styles.inner, animatedStyles.upper]} />
          <Animated.View style={[styles.inner, animatedStyles.middle]} />
          <Animated.View style={[styles.inner, animatedStyles.lower]} />
        </Animated.View>
      </TouchableWithoutFeedback>
    </View>
  )
}
[...]

 

 

With this, we have finished our animation. Other possibilities for composing animations are sequence, that waits until one animation ends for the other to begin, and delay, that waits a given amount a time after an animation begins to start the next one.

Comparison with CSS animations

If you have any experience with CSS animations, you can see the similarities. With CSS animations we can declare a transition which will watch for a given CSS property change and automagically interpolate the values for this property to pass during the transition, or we can declare a keyframe animation and decide what we want to happen in the progression of our animation.

With React Native we have to use an approach similar to the CSS keyframe animations, and declare how we want our animation to progress. The main advantage of CSS animation vs React Native Animation is that not all of the properties of CSS are in React Native styles, one example we have on our burger button is the transform origin CSS property.

If we wanted to create the rotation animation of our button, we could declare the transform origin of our upper part of the burger, for example, as left and simply rotate it to 45 degrees, and reach the same animation as the one we did in this course, which needed a translateY to the final position where rotating 45 degrees from the center of the bar would reach the position we want.

But this is not a problem with Animated, it’s a problem with React Native in general, but more and more CSS properties are being added to React Native, and I believe, eventually it will reach a realm of possibilities very similar to that of CSS3 in general.

Native Animations

In the introduction of the blogpost I talked about the possibility of creating native animations, but until now we haven’t used any of it. Not everything we can do with Animated is supported by the native driver, as a rule of thumb, we cannot animate layout properties, we can animate things like transform and opacity.

In our project, we can use native driver for anything, except the background color and the height of the middle part of the button. But Animated only supports one driver per animation, so we cannot declare a Native Driver for this animations that do not support it, we have to declare a separate, non native driver for them. The way we use the native driver is very simple, we simply have to set a boolean in the animation to true, as follows:

[...]
const App = () => {
  const [activated, setActivated] = useState(false)
  const [animation, setAnimation] = useState(new Animated.Value(0))
  const [jsAnimation, setJsAnimation] = useState(new Animated.Value(0))
  const [rotation, setRotation] = useState(new Animated.Value(0))
  const startAnimation = () => {
    const toValue = activated ? 0 : 1
    setActivated(!activated)
    Animated.parallel([
      Animated.timing(animation, {
        toValue,
        duration: 300,
        useNativeDriver: true
      }),
      Animated.spring(rotation, {
        toValue,
        friction: 2,
        tension: 140,
        useNativeDriver: true
      }),
      Animated.timing(jsAnimation, {
        toValue,
        duration: 300
      })
    ]).start()
  }
  const animatedStyles = {
    lower: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, -25]
          })
        },
        {
          rotate: rotation.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '45deg']
          })
        }
      ]
    },
    upper: {
      transform: [
        {
          translateY: animation.interpolate({
            inputRange: [0, 1],
            outputRange: [0, 25]
          })
        },
        {
          rotate: rotation.interpolate({
            inputRange: [0, 1],
            outputRange: ['0deg', '-45deg']
          })
        }
      ]
    },
    middle: {
      height: jsAnimation.interpolate({
        inputRange: [0, 1],
        outputRange: [10, 0]
      })
    },
    burgerButton: {
      backgroundColor: jsAnimation.interpolate({
        inputRange: [0, 1],
        outputRange: ['green', 'red']
      })
    }
  }
  return (
    <View style={styles.container}>
      <TouchableWithoutFeedback onPress={startAnimation}>
        <Animated.View
          style={[styles.burgerButton, animatedStyles.burgerButton]}
        >
          <Animated.View style={[styles.inner, animatedStyles.upper]} />
          <Animated.View style={[styles.inner, animatedStyles.middle]} />
          <Animated.View style={[styles.inner, animatedStyles.lower]} />
        </Animated.View>
      </TouchableWithoutFeedback>
    </View>
  )
}
[...]

This won’t change our animation, it will simply make it more fluid and consume less memory.

Conclusion

With these few basic concepts, we can get started creating useful animations that will improve our users experience with our apps.

Furthermore, with React Native and Animated, we can also track gestures using the Animated.event method and create dynamic animations, like dragging an element or scrolling. Also, we can use libraries to improve our animations, some that I tend to use are libraries that create SVG animations, like flubber.js and D3-Interpolate.

There is still much to be improved in React Native’s Animated API, but the path ahead for the platform is very exciting and filled with very smart people. This was my take on how to start with animating your React Native apps, any suggestions are more than welcome.

Thank you for reading!

Cheesecake Labs ranked Top #5 React Native Development Company Worldwide by Clutch

 

References

 

https://tobiasahlin.com/blog/meaningful-motion-w-action-driven-animation/

https://facebook.github.io/react-native/docs/animations

 

About the author.

Mateus Bueno
Mateus Bueno

An anti-jokester fullstack developer who loves coffee, sweets and long walks on the beach, but exclusively when the beaches are in videogames.