Animate SVG Path in RN with Reanimated

House Gif

Start a new Expo project (Assuming you already installed expo globally)

expo init animatedHouse

install react-native-svg and react-native-reanimated

yarn install react-native-svg react-native-reanimated

Add Reanimated's babel plugin to your babel.config.js

  module.exports = {
      ...
      plugins: [
          ...
          'react-native-reanimated/plugin',
      ],
  };

I created the house svg using figma (check it out here)

Create a new component and render the SVG using react-native-svg

import React, { Dimensions } from 'react'
import Svg, { Path } from 'react-native-svg'

const { height, width } = Dimensions.get("screen")
const HEIGHT = width * 0.47

export default () => {
  return (
    <Svg width={width} height={HEIGHT} viewBox={[0, 0, width, HEIGHT].join(" ")}>
        <Path
            d="M0 120.5H119V66H106C134.118 41.7875 149.882 28.2125 178 4L194 17.5L199.5 11.5H211.5V31.5L247 61V66H237.5V120.5H194V75.5H179H165V120.5H121.5"
            stroke="black"
            strokeWidth="5"
        />
    </Svg>
  )
}

This should be enough to render the svg.

Animating the path

To animate the Path we'll need two properties strokeDasharray and strokeDashoffset. The strokeDasharray defines the pattern of dashes and gaps used to paint the path.

For example (10 is the length of each dash)

<Path
  ...
  strokeDasharray={10}
/>

Dashed

strokeDashoffset defines an offset on the rendering of the associated dash array. For example

<Path
  ...
  strokeDashoffset={300}
/>

Offset

Instead of using random values for the strokeDashoffset we need to find a way to view all lengths of the offset, so that we can easily animate it from 0% - 100%. The easiest way to implement that is to know of the whole path length and multiply it to values between 0 - 1.

To get the path length we'll need to invoke the getTotalLength() function as soon as the element renders. We can leverage the onLayout View prop to set the Path length.

export default () => {
  const [houseLength, setHouseLength] = useState(0)
  const houseRef = useRef(null)

  return (
    <Svg width={width} height={HEIGHT} viewBox={[0, 0, width, HEIGHT].join(" ")}>
        <Path
            d="M0 120.5H119V66H106C134.118 41.7875 149.882 28.2125 178 4L194 17.5L199.5 11.5H211.5V31.5L247 61V66H237.5V120.5H194V75.5H179H165V120.5H121.5"
            ref={houseRef}
            onLayout={() => setHouseLength(houseRef.current.getTotalLength())}
            stroke="black"
            strokeWidth="5"
            strokeDashoffset={houseLength}
            strokeDasharray={houseLength}            
        />
    </Svg>
  )
}

If you test strokeDashoffset with values like houseLength x 0.2, houseLength x 0.5, houseLength x 0.8 and houseLength x 1. You get a hint of how it should work.

So to animate out Path we need to convert it into an animated component, with the help of the animated shared value and animated props we connect all the dots.

import React, { useRef, useState, useEffect } from 'react'
import Svg, { Path } from 'react-native-svg'
import { Dimensions } from 'react-native'
import Animated, { useAnimatedProps, useSharedValue, withTiming, withRepeat } from 'react-native-reanimated'

const { height, width } = Dimensions.get("screen")
const HEIGHT = width * 0.47

export default () => {
  const [houseLength, setHouseLength] = useState(0)
  const houseRef = useRef(null)

  const progress = useSharedValue(0)

  const AnimatedPath = Animated.createAnimatedComponent(Path)

  const houseAnimatedProps = useAnimatedProps(() => ({
    strokeDashoffset: houseLength * progress.value
  }))

  useEffect(() => {
    progress.value = withRepeat(withTiming(1, { duration: 1000 }), 50, true)
  }, [])

  return (
    <Svg width={width} height={HEIGHT} viewBox={[0, 0, width, HEIGHT].join(" ")}>
        <AnimatedPath
            d="M0 120.5H119V66H106C134.118 41.7875 149.882 28.2125 178 4L194 17.5L199.5 11.5H211.5V31.5L247 61V66H237.5V120.5H194V75.5H179H165V120.5H121.5"
            stroke="black"
            strokeWidth="5"
            ref={houseRef}
            onLayout={() => setHouseLength(houseRef.current.getTotalLength())}
            strokeDasharray={houseLength}
            animatedProps={houseAnimatedProps}
        />
    </Svg>    
  )
}

The above code should run the animation, however, you'll notice that the path starts at full length and reduces to 0 as opposed to how it should be i.e 0 to fullLength. This happens because we are using strokeDashoffset property which renders the offset of the Path, hence giving us opposite results. To fix this lets make an adjustment to the animated strokeDashoffset prop

  const houseAnimatedProps = useAnimatedProps(() => ({
    strokeDashoffset: houseLength * (1 - progress.value)
  }))

That's it, I hope you learnt something.