React Native Animated Svg

by Alberto Osio - 01/09/2021
ReactNative

One of the major quality indexes for a mobile application is the presence of smooth transition and user engaging animations. This does not mean everything should be animated, but the right animation in the right place can really make the difference in the user experience.

React Native itself exposes the Animated API to address this task, but it is a very simple, low level and cumbersome set of animation primitives. Much better results can be achived with React Native Reanimated, which has a much more high level and expressive API.

In this tutorial we will use React Native Reanimated in order to create an animated SVG figure. Please note that all the dependencies we are going to use are included in the Expo SDK, so this also applies to Expo Managed applications.

Let's start by creating a brand new React Native application and installing the required dependencies

$ npx create-react-native-app animated-svg
$ cd animated-svg
$ yarn add react-native-svg react-native-reanimated

or, if you use expo

$ expo init --name animated-svg -y
$ cd animated-svg
$ expo install react-native-svg react-native-reanimated

We strongly suggest to follow the installation instructions for react-native-reanimated from their website, since the installation requires some special steps, like setting up a babel plugin.

You can even follow the tutorial on Expo Snack, just make sure to add dependencies to the package.json file

{
  "dependencies": {
    "react-native-paper": "3.6.0",
    "expo-constants": "~11.0.1",
    "react-native-svg": "12.1.1",
    "react-native-reanimated": "~2.2.0",
    "react-native-gesture-handler": "~1.10.2"
  }
}

So far so good, we can start creating our component. What we are tring to do is to animate three circles from zero radius to max radius, fading out in the last moments of the animation.

import React from "react"
import Svg, { Circle } from "react-native-svg"

export default function PulseCircle() {
  return (
    <Svg width={212} height={212}>
      <Circle cx={106} cy={106} r={35} stroke="red" />
      <Circle cx={106} cy={106} r={70} stroke="red" />
      <Circle cx={106} cy={106} r={105} stroke="red" />
    </Svg>
  )
}

If you are coding with me, you should see something like in the following picture. This is the starting point for building up our animation

blog image

Now, we are going to animate the innermost circle radius from 0 to 35, the middle one from 35 to 70 and the outermost from 70 to 105. The outermost circle will also animate its opacity from 100% to 0% while increasing its radius.

In order to do this, we need a SharedValue from react-native-reanimated. Shared values are called this way because they are shared between the Reanimated Worker that computes the animations and the JS thread that schedules them. A shared value can then be interpolated into a set of props or styles to pass to our components.

In our example, since we want to synchronize the animation of the circles, we use just one shared value and perform three interpolations on top of it. Complex animations may need more than one shared value.

import React from "react";
import Svg, { Circle } from 'react-native-svg';
import { useSharedValue, useAnimatedProps } from "react-native-reanimated";

function PulseCircle() {
  // This is the shared value, the base of our interpolation
  // In the example, it ranges from 0 to 1, but you can choose the range as you wish
  // You'll see later on how to do it
  const pulse = useSharedValue(0); 

  // These are the props that we will pass to the innermost circle in order to animate it
  const innerStyle = useAnimatedProps(() => {
    return {
      r: pulse.value * 35
    }
  })

  // These are the props for the middle circle
  const middleStyle = useAnimatedProps(() => {
    return {
      r: 35 + pulse.value * 35
    }
  })

  // These are the props of the outermost circle
  const outerStyle = useAnimatedProps(() => {
    return {
      r: 70 + pulse.value * 35,
      opacity: 1 - pulse.value
    }
  })

  // No change here (not yet!)
  return (
    <Svg width={212} height={212}>
      <Circle cx={106} cy={106} r={35} stroke="red" />
      <Circle cx={106} cy={106} r={70} stroke="red" />
      <Circle cx={106} cy={106} r={105} stroke="red" />
    </Svg>
  )
}

In this case, since the radius is a property of our Circle component, we need to use the useAnimatedProps hook. When dealing with style, the useAnimatedStyle hook should be used instead, but the usage is pretty similar.

Right now nothing is animated yet, we first need to

  • make our circles animatable
  • connect our animated props to the components
  • start the animation

Circles from react-native-svg do not support animations out of the box, thus we need to create an animated version of them

import React from "react";
import Svg, { Circle } from 'react-native-svg';
import Animated, { useSharedValue, useAnimatedProps } from "react-native-reanimated"

// Create an animatable variant of <Circle />
// Ensure to put this OUTSIDE your component (it is another component in the end)
const AnimatedCircle = Animated.createAnimatedComponent(Circle)

function PulseCircle() {
  const pulse = useSharedValue(0);

  const innerStyle = useAnimatedProps(() => {
    return {
      r: pulse.value * 35
    }
  })

  const middleStyle = useAnimatedProps(() => {
    return {
      r: 35 + pulse.value * 35
    }
  })

  const outerStyle = useAnimatedProps(() => {
    return {
      r: 70 + pulse.value * 35,
      opacity: 1 - pulse.value
    }
  })


  // Please note that we are no more using <Circle />, but <AnimatedCircle />
  return (
    <Svg width={212} height={212}>
      <AnimatedCircle cx={106} cy={106} r={35} stroke="red" />
      <AnimatedCircle cx={106} cy={106} r={70} stroke="red" />
      <AnimatedCircle cx={106} cy={106} r={105} stroke="red" />
    </Svg>
  )
}

Then, we need to pass them the animated props

import React from "react";
import Svg, { Circle } from 'react-native-svg';
import Animated, { useSharedValue, useAnimatedProps } from "react-native-reanimated"

const AnimatedCircle = Animated.createAnimatedComponent(Circle)

function PulseCircle() {
  const pulse = useSharedValue(0);

  const innerStyle = useAnimatedProps(() => {
    return {
      r: pulse.value * 35
    }
  })

  const middleStyle = useAnimatedProps(() => {
    return {
      r: 35 + pulse.value * 35
    }
  })

  const outerStyle = useAnimatedProps(() => {
    return {
      r: 70 + pulse.value * 35,
      opacity: 1 - pulse.value
    }
  })

  return (
    <Svg width={212} height={212}>
      <AnimatedCircle cx={106} cy={106} r={35} stroke="red" animatedProps={innerStyle} />
      <AnimatedCircle cx={106} cy={106} r={70} stroke="red" animatedProps={middleStyle} />
      <AnimatedCircle cx={106} cy={106} r={105} stroke="red" animatedProps={outerStyle} />
    </Svg>
  )
}

Tip: if instead you use useAnimatedStyle, just pass it to the style property!

Finally, start the animation by setting the value of the shared value

import React, { useEffect } from 'react';
import Svg, { Circle } from 'react-native-svg';
import Animated, { useSharedValue, useAnimatedProps, withRepeat, withTiming, Easing } from "react-native-reanimated";

const AnimatedCircle = Animated.createAnimatedComponent(Circle)

export default function App() {
  return (
    <View style={styles.container}>
      <PulseCircle />
    </View>
  );
}

function PulseCircle() {
  const pulse = useSharedValue(0);

  const innerStyle = useAnimatedProps(() => {
    return {
      r: pulse.value * 35
    }
  })

  const middleStyle = useAnimatedProps(() => {
    return {
      r: 35 + pulse.value * 35
    }
  })

  const outerStyle = useAnimatedProps(() => {
    return {
      r: 70 + pulse.value * 35,
      opacity: 1 - pulse.value
    }
  })

  // This will start the animation automatically when the component mounts
  // withRepeat is needed in order to repeat the animation forever
  //   it takes three arguments: the animation, the number of repetitions (-1 means infinite),
  //   and whether to perform the backward animation between loops
  // withTiming is the interpolation behaviour (the alternative is to use a spring)
  //   it takes the final value of the animation (do you remember that the we said the shared value
  //   ranges from 0 to 1?) and a configuration object to set the duration and the easing function
  useEffect(() => {
    pulse.value = withRepeat(withTiming(1, { duration: 1000, easing: Easing.linear }), -1, false)
  }, [pulse])

  return (
    <Svg width={212} height={212}>
      <AnimatedCircle cx={106} cy={106} r={35} stroke="red" animatedProps={innerStyle} />
      <AnimatedCircle cx={106} cy={106} r={70} stroke="red" animatedProps={middleStyle} />
      <AnimatedCircle cx={106} cy={106} r={105} stroke="red" animatedProps={outerStyle} />
    </Svg>
  )
}

And here is our final result

The full source code of the example is available on our Snack account at https://snack.expo.dev/@inmagik/pulsecircle.

This is a very simple use case, but enough to display a general way you can use to animate several SVG components, from circle to rects, ellipsis, and so on. Beware that not all svg elements can be animated that easily. For instance, paths are difficult to animated because the interpolation function for their d attribute can become quite complex even for simple shapes. Other elements like gradient stops are not animatable.

Keep in mind that the effectiveness of an animation greatly depends on its smoothness, i.e. on the number of FPS your implementation can achieve. In other words, the simpler the interpolators, the higher the FPS of the animation, and the other way round as interpolators grow in complexity, animations slow down and become laggy. So, when writing your interpolators, you should precompute as much as possible and avoid any unnecessary computation.

Hope this article can help you, not much documentation is available on the internet about animated SVGs in ReactNative. If you liked it, or if you want to suggest improvements or ideas for future articles, just drop us a line at at info(at)inmagik.com or tweet to @inmagiklabs.