Animate SVG Path in RN with Reanimated
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}
/>
strokeDashoffset
defines an offset on the rendering of the associated dash array.
For example
<Path
...
strokeDashoffset={300}
/>
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.