Create Spotify heart animation with Reanimated 2 - tutorial
Let's make it obvious - even simple animations can take your app to a whole new level. The problem is not whether it is worth adding animations but how to make them. Reanimated 2 is a great and really useful library. Let me show you also how simple it can be!
Before we begin, let me give you a short introduction.
If you are, by any chance (and chances are high I suppose), a Spotify user, you have seen this cool little animation fired up whenever you like a song :) Those are the things that really add to the user experience, so why not try recreating it with React Native and reanimated library in this article! It’s going to be fun, without a doubt!
The main technologies I use in this tutorial are:
React Native
Typescript
Reanimated 2
Styled-components
React Native and Reanimated 2 should be obvious.
Why typescript?
I chose TS over JS for this article as TS is simply fun and very useful, and I couldn’t recommend it more! :) It’s like a guardian angel for your code, and I really love having a type-safe project. For anyone wanting to code-along in plain JS, just loose the TS parts, it shouldn’t be a huge problem. I would also like to encourage every React Native user to try TS.
Why Styled-components? Because the idea, and the library itself, are great! When it’s used in the right way, it can make your code incredibly legible. It’s really super easy. Trust me and give it a try :)
OK! Let’s bite some meat(code) :)
Repository
This article has its own repository. You can clone it from the link below:
But we can give it our own name and hide all styles in the other file.
To create a styled component, we need to import the styled element from styled-components/native. The styled allows us to upgrade any component into a styled component. We use it here to create the Container. All style properties are inside the backticks - in other words, it’s just a template string. We have to export it to use it in the tsx file.
Now we can go to the next step and create the main component:
useSharedValue - this hook creates an object with value, which can be used for animations. This value can be read or changed by other elements or functions. To make things simple, we use it when we need value to change smoothly from one point to another. For example: 0 -> 0.01 -> 0.02 -> 0.03 -> 0.04 -> ... -> 0.97 -> 0.98 -> 0.99 -> 1 To read more, please click HERE
HeartButton / BouncingCircles / FlyingHeartsRenderer - UI components, we will explore them later in this tutorial
Container - our styled component (SafeAreaView)
Coords - type for the startCoords
At the beginning we need to create 3 values:
isBgColored - we will need it to check the current state of our heart (heart button) at the moment (green / black with a white border)
heartAnimation - shared value. We will use it to animate elements. Its fundamental architecture allows it to rise and fall in a way that can be used for animation. In the process, this value is going through every single hundredth number. For example from 0 to 1: (0.01, 0.02, 0.03, 0.04, 0.05 ... 0.96, 0.97, 0.98, 0.99, 1). To read more, please click HERE
startCoords - start coordinates on the X and Y axes. All UI elements used by us will be placed at this point at the beginning
Now we can wrap all UI elements in our Container wrapper and pass the required props.
Dispatch, SetStateAction - necessary to properly type our props - setIsBgColored
Animated - an element from the Reanimated library. We can use it to create different animated components. Here we need it to type the heartAnimation props
useSharedValue - this hook creates an object with value, which can be used for animations. This value can be read or changed by other elements or functions. To make things simple, we use it when we need value to change smoothly from one point to another. For example: 0 -> 0.01 -> 0.02 -> 0.03 -> 0.04 -> ... -> 0.97 -> 0.98 -> 0.99 -> 1 To read more, please click HERE
ScaleViewContainer, ShakeViewContainer, StyledAnimatedPath, StyledHeartButton, StyledSvg - styled-components. We will explore them below
useMainHeartAnimation - custom hook containing animation styles and logic we need to pass to the proper components
useHeartPress - a custom hook that contains everything that must happen when we click on the heart
Let’s take a short break and take a closer look at those styled-components we mentioned a few lines above:
Svg, Path - they’re just svg elements we want to style
PathProps - a type that we use to properly type our Path element
Animated - an element from the Reanimated library. We can use it to create different animated components. Here we need it to create AnimatedPath
AnimateProps - we use it to properly type our Path element. It’s a generic type which accepts the type of the chosen element. In this case Path / PathProps
TouchableOpacity - a well known React Native element. We use it to create touchable elements, for example buttons
At first, we will modify our Path to the AnimatedPath. We need to use Animated to do that. If we take a closer look at the Animated, it can give us 3 animated components by default: Animated.View, Animated.Text, and Animated.Image. However, in some cases you may want to turn another component into an Animated component, so it can take advantage of Animated.Value in its styles or properties. Here we need to animate Path, so we need to convert our Path element to the animated version. To do that, we need to use the function createAnimatedComponent from Animated. AnimatedPath is just a name I have chosen, you can name it however you want. Now that the AnimtedPath exist, we can start creating all the necessary styled-components:
StyledHeartButton
It’s just a styled TouchableOpacity. It will be a wrapper for our svg heart. Its function is to make our heart clickable. When we create a styled component, we can call the attrs method, which takes a function as an argument. In this function, we can return an object with all props that we can normally pass to the element(component) as if we were adding a JSX <View />. For example, we can pass props activeOpacity to the TouchableOpacity. Because I want to cover other elements under my HeartButton, I give it possition: absolute with z-index: 1. I would also like to center all the elements inside a button, so I use flex for that.
ScaleViewContainer
It’s a styled Animated.View. What is an Animated.View? It’s just a View that can be animated by Reanimated. Simple. I chose a name with ‘Scale’ inside, because this View will help me scale my heart.
ShakeViewContainer
The same situation as with ScaleViewContainer, but instead of scaling, this container helps me shake the heart. We should notice one thing - this container is twice as high as the others. Just like I said, we want to use it to shake our heart. If we set the height to 70px, then the center of mass will be placed in the center of the heart (whole heart will be rotating):
But the effect I am after is that the bottom of the heart stays in the same position and only the top is shaking. How to do that? We just need to move the center of the mass to the bottom of our heart. We can do that by making container twice height and placing the heart at the top of the container (I’ve changed the background color to illustrate it better):
After that we can just set bottom: -70px and TADAM!
StyledSvg
This element is a styled wrapper for the path.
StyledAnimatedPath
It’s a path element with an extra prop - animatedProps. Above we can see type for it - AnimatedPathProps. Other props are typical props that we can pass to the Path element. Thanks to the styled-components, we were able to pass the props here (not in the tsx file). Please analyze how easily we can pass external props here.
Let’s come back to the main HeartButton component.
heartTransform - the second shared value that we created. This time - to transform the heart. We did this because now we have to go from 1 to 0 (not like before, from 0 to 1), and the animations will be a bit different from the heartAnimation.
useMainHeartAnimation - this hook takes care of the heart button animation (we will come back to it soon)
heartPress - this hook takes care of the heart button onPress (we will come back to it soon)
Let’s summarize the components we render here:
StyledHeartButton - styled TouchableOpacity
ShakeViewContainer - styled View (we pass an extra animated style here)
ScaleViewContainer - styled View (we pass an extra animated style here)
StyledSvg - styled Svg
StyledAnimatedPath - styled Path (we pass an extra animated props here)
When we summarize it like that, I think it’s super easy to read.
Next in the line is useHeartPress.
useHeartPress
This hook handles everything that has to be done when we click on the main heart. (Please notice that the heartAnimation.value and the heartTransform.value are animated differently. That’s why, as I’ve said, I had to create another shared value) Let’s take a closer look:
Animated, Dispatch, setStateAction - here we need those elements only for type incoming params
withSequence - we can call it the animations ‘helper’. Its role is to combine a few animations. To read more, please click HERE
withSpring - an animation provided by the Reanimated library. We use it when we want to get a spring-based animation with a cool bouncing effect. To read more, please click HERE
withTiming - an animation provided by the Reanimated library. We use it when we want to start a time-based animation. To read more, please click HERE
As we said, the main function of this hook is to prepare a function for the onPress of the main heart. This function is based on the state of our heart. In fact, we have two states:
when the heart is black with a white border
when the heart is green (colored)
If the heart is colored, and we click it, we want to change its color to black, so also the state for false (heart is not colored). Also, we want to change our heartAnimation.value to 0 (default).
If the heart is black (with a white border), we want to change its color to green (state for true). Also, we want to animate heartAnimation.value to 1 with timing. Timing duration is 800ms, with the second argument easing. This argument drives the easing curve for the animation. We can choose one of the pre-configured options. I decided to use bezier. To read more, please click HERE
Regardless of the state, we still need to animate heartTransform.value. This value will be necessary to rotate or scale our heart. Here, we will use the withSequence animation. First, our heartTransform.value will be animated using withTiming to value 0.8, and when the value reaches this point, the animation will change to the withSpring at the end (from the value 0.8 to 1).
As the second argument withSpring takes object with few keys. Two of them are:
damping - as the name says, it’s responsible for damping the spring (bumping) effect. The higher the value, the more damped the effect.
mass - again, as the name says, it’s responsible for the animation mass. The higher the value, the heavier the animation.
I encourage you to try it yourself because it’s really hard to explain it by words :)
That’s all folks! Now we need to return our heartPress function and unpack it in the HeartButton component.
Let’s go to the useMainHeartAnimation hook and take a look what we do with those values and how we animate our main heart.
useMainHeartAnimation
This hook prepares the animated styles and the animated props we would use in our components. First let’s take a look at the whole file:
Animated - an element from the Reanimated library. We can use it to create different animated components. Here we just need it to type the heartTransform value.
Easing - helps us animate switching between values. To read more, please click HERE
interpolate - this function transforms one scope to another. For example, if we have a shared value in the scope [0-1], we can change it to [0-100] or [50-0]**, etc. For example, if we move the square from the point 0 to 1, and interpolate this value to 0-50, then the square will move 50 times on. We can also declare a few points inside, and modify what will happen in different time points. For example, [0, 0.5, 1](input) can be modified to the [0, 0.7, 1](output). For what? It’s a bit crazy, but now that what should happen in point 0.5 will happen in point 0.7. In fact, we extended the first range and shortened the second one. To read more, please click HERE
interpolateColor - Interpolate for the colors. Transforms number values to color values. To read more, please click HERE
useAnimatedStyle - creates animated style values for the Animated Components (for example, View). Returns the same values as classic style values, for example: width, rotate; but customized for the animations. To read more, please click HERE
useAnimatedProps - creates animated props values for other elements, e.g. svg elements. It’s also a style value, but for svg elements. To read more, please click HERE
useDerivedValue - this hook creates a shared value which is updated whenever one of the values used inside is changed. To read more, please click HERE
withTiming - one of the default animations provided by Reanimated library. Creates animation based on time. We can, for example, set a duration of 1000 ms for some animation. To read more, please click HERE
theme - just a theme that I always create in my projects. It contains certain style elements, for example colors.
Let’s take a look at the hook body.
progress value Our first value is progress. I decided to use useDerivedValue (not just useSharedValue) here, because I needed to animate this value and make a condition depending on the state isBgColored. This was the easiest and most comfortable way. I will use this value to interpolateColor in the animatedProps.
animatedProps Here we are creating animated style for the Path element. We need to return fill and stroke values (because we want to animate those two). The first argument that the interpolateColor takes is value. Here, we pass our previously instantiated progress value. Please notice that if the isBgColored is true, then progress.value is equal to 0, and if not, it’s equal to 1. It’s very important because the second argument that interpolateColor takes is range [0-1]. In other words, if progress.value is equal to 0, then the value will go to 1. If the progress.value is equal to 1, then the value will go to 0. The third argument are colors. The first color we pass is equal to 0. The second is equal to 1. Then the range [0-1], means “from green (theme.colors.spotifyGreen), to black (theme.colors.backgroundColor)”. This means that if the progress.value is equal to 0, then the value will go from 0 to 1, in other words, from green to black. If the progress.value is equal to 1, then the value will go from 1 to 0, in other words, from black to green. The fourth value is just some information about the color code in which we pass the value. I’m doing it using RGB, so the fourth argument is just RGB. After that we just need to return fill and stroke.
scaleAnimatedStyle and shakeAnimatedStyle
Depending on the isBgColored boolean value, whether it is true or false, we want to scale or to shake the heart.
In the first case, it’s really simple. We just need to transform the scale just as we would do in css, but instead of putting a ‘hardcoded’ value like 0.5 or 0.7, we want to put a value that will change in time, so we use heartTransform.value (as we have set in hook useHeartPress, its value is going from 0.8 to 1).
When we want to shake our heart, the situation gets a bit more complicated, but still noting we couldn’t handle. As above, we need to transform something, but this time it will be rotate instead of scale. Because we want our heart to go to the left, then to the right, then to the left, etc. to get the shaking effect, we need to interpolate heartTransform.value. At first, we’re changing its range from [0-1] to [0-4], and making a few ‘bullet points’.
In the first point, the heart will rotate -25 deg, then in the second point it has to go back to 0 deg, so that then rotate 25 deg, again to 0 deg, and again to -25 deg… We have 9 points because we need to move it 8 times (the first point is a start point).
FlyingHeartsRenderer
FlyingHeartsRenderer is a container where we render all those small hearts, that show up and vanish behind the main heart:
SingleFlyingHeart - as the name says, it’s a component that renders a single small heart
Animated - an element from the Reanimated library. We can use it to create different animated components. We need it to type our props
Coords - type for the startCoords
Every SingleFlyingHeart needs to get a few props:
startCoords - position X and position Y of the heart at the start of the animation.
heartAnimation - a shared value. We will use it to animate elements. Its fundamental architecture allows it to rise and fall in a way that makes it suitable for the animation. In the process, this value is going through every single hundredth number. For example from 0 to 1: (0.01, 0.02, 0.03, 0.04, 0.05 ... 0.96, 0.97, 0.98, 0.99, 1). To read more, please click HERE
minValueX and maxValueX - I want to make hearts fly to different points randomly (end points). That’s why I’m making a range - min and max value on the X axis. In the end, two hearts have fixed positions X ( 60 and -60 ), but it’s useful for the other 6 hearts. We will discuss it further in the SingleFlyingHeart component.
heartRenderNumber - the number of hearts that should be rendered at once. It’s doing really nothing when the number is 1, but when we have the array, the animation depend on it.
index - optional argument, when we have the array, the animation depend on it.
As we can see, we’re rendering 8 SingleFlyingHeart. Twice we use the array to render SingleFlyingHeart. It’s important when it comes to the animation, because time for a single heart animation is then split halt and half. I will explain it further in the next sections.
SingleFlyingHeart
SingleFlyingHeart renders a single small heart that shows up and vanishes behind the main heart. First let’s take a look at how we styled our components:
Here we have an almost the same (even simpler) structure as in the HeartButton.styled.ts.
First we need to create the AnimatedPath which is, as we said before, just a Path element that can be animated.
We also need to create type (interface) HeartSizeProps to pass the heart size (hearts have different sizes). As before, we need to create styled Animated.View, styled Svg, and styled Animated.Path. Only the width and the height of the Svg and the Path depends on the value we will pass from the SingleFlyingHeart.tsx.
AnimatedViewContainer, StyledAnimatedPath, StyledSvg - styled-components that we created in the file SingleFlyingHeart.styled.ts
useFlyingHeartAnimatedStyle - hook responsible for creating animated styles for the AnimatedViewContainer (Animated.View)
drawRandomNumberInRange - utility function responsible for drawing a random number within the given range
Animated - an element from the Reanimated library. We can use it to create different animated components. We need it here to type the props
Coords - type for the startCoords
As we said before when we were exploring the FlyingHeartsRenderer component, the final X coordinate (in the destination point) must be different for each heart. We need to take minValueX and maxValueX and draw the final X coordinate (randomXCoord) from the drawRandomNumberInRange. The same situation is with the Y coordinate, but with one difference. We want the final Y coordinate to be in the range (-120, -200) for each heart, so we don’t need to pass different ranges for each heart, and can just pass it here (it’s simply fine for us as it is).
After that we can set the final coordinates for each heart (finalCoords).
Also, I want my hearts to have different sizes, so I’m drawing number for height and width for each of them (heartSize).
The last thing that we need to prepare is the heartStyle, which is the animation style for the AnimatedViewContainer (Animated.View). Like we said before, useFlyingHeartAnimatedStyle is responsible for that. We will explore it in a minute.
When we have all those elements, we need to render AnimatedViewContainer (Animated.View) and pass the heartStyle (animated styles) there. Inside AnimatedViewContainer (Animated.View) we render StyledSvg(Svg), and inside the StyledSvg(Svg) the last component - StyledAnimatedPath(Path).
Animated - an element from the Reanimated library. We can use it to create different animated components. Here, we need it to type the startCoords and the heartAnimation (shared values)
Extrapolation - when it’s provided, secures the value from going out of the range. To read more, please click HERE
interpolate - this function transforms one scope to another. For example, if we have a shared value in scope [0-1], we can change it to [0-100] or [50-0], etc. For example, if we move square from point 0 to 1 and we interpolate this value to [0-50], then the square will move 50 times on. We can also declare a few points inside and modify what happens in the different time points. For example [0, 0.5, 1](input) can be modified to the [0, 0.7, 1](output). What’s the purpose of that? It’s a bit crazy, but now that what should happen in point 0.5 will happen in point 0.7. In fact, we extended the first range and shortened the second one. To read more, please click HERE
useAnimatedStyle - creates animated style values for the Animated Components (for example View). Returns the same values as classic style values, for example: width, rotate; but customized for the animations. To read more, please click HERE
Coords - type for the startCoords
Let’s also take a look at the arguments we pass to the useFlyingHeartAnimatedStyle:
finalCoords - coordinates (axis X and Y) where single small heart end its way (different for each small heart)
startCoords - coordinates (axis X and Y) where single small heart start its way (the same for each small heart). These coords are constant and equal {x: 0, y: 0}
heartAnimation - shared value. We will use it to animate elements. Its fundamental architecture allows it to rise and fall in such a way that it can be used for animation. In the process, this value is going through every single hundredth number. For example from 0 to 1: (0.01, 0.02, 0.03, 0.04, 0.05 ... 0.96, 0.97, 0.98, 0.99, 1). To read more, please click HERE
index - index in the array (if the SingleFlyingHeart is rendered from the array)
heartRendersNumber - length of the array (if the SingleFlyingHeart are rendered from the array, in other way equals 1)
Now that we have all the ingredients, we can mix them.
calcBezierPoint
First we need to create the calcBezierPoint function, which allows us to create a path for each small flying heart. We use it because we want from our heart to fly on a curve rather than a straight line. translateX and translateY are calculated to start growing at the right pace and timing relative to each other. You can modify the curve, modifying the values by yourself.
rangeChunk
The rangeChunk variable will help us render a few hearts at once with a time difference (when we render them from the array/when there are more than 1). This variable creates ranges, which will be used as the time segments. Every next heart will be rendered with a multiplied delay.
heartStyle
The time has come and we need to create an animated style for each of our hearts. To simplify code reading at this point, I renamed finalCoords to destination and assigned startCoords.value to the simple variable start.
Another variable is input, which is array of the points in time, when the specific heart should appear. I will say it again - it’s important only when we render a heart from the array (because we want to render them in a sequence).
animatedPosition
Here we interpolate our input array (ranges) to the values we want to receive ([0, 0.5, 0.8] - the final value is 0.8, because I want to cut the second range of heartAnimation.value) a bit, and secure this scope by using Extrapolation. If you are still feeling dizzy about Interpolation and Extrapolation, don’t worry. It comes with the experience. You just need to experiment to get the hang of it better :) Stay strong!
Now we need to create translateX and translateY values using the calcBezierPoint function, and opacity and scale using interpolation. As far as opacity is concerned, we want it to start at 0% visibility and reach 90%, and then disappear completely, which is why the output array is [0, 0.9, 0]. When it comes to the scale, we also want to start from the 0% width and height, go to the 130% (bump!) and vanish completely, that’s why the output array is [0, 1.3, 0].
Now we need to return our style values - transform and opacity. Please remember that the values we want to transform are always placed, as objects in the array (that one thing is different in the css).
At the end, useFlyingHeartAnimatedStyle returns the fully prepared heartStyle for us. And it’s ready for use!
Bouncing Circles
When you look at our final version of the animation, you can see some green diverging circles / circle waves. That’s what I call the ‘Bouncing Circles’. This is the last, and I believe the easiest part of this animation. Let’s first take a look at the styled-components we need to prepare for them:
As you can see, we just need to create one styled component (Animated.View). We will use it twice with a different border-width, that’s why I pass this value in the props. I don’t think there is much more to it, so let’s go to the main component now!
AnimatedCircle - styled component we prepared a minute ago (Animated.View)
useBouncingCirclesAnimatedStyle - hook responsible for preparing animated styles for the animated circles
Animated - an element from the Reanimated library. We can use it to create different animated components. We need it here to type our props
Now incoming props:
heartAnimation - shared value. We will use it to animate elements. Its fundamental architecture allows it to rise and fall in such a way that it can be used for animation. In the process, this value is going through every single hundredth number. For example from 0 to 1: (0.01, 0.02, 0.03, 0.04, 0.05 ... 0.96, 0.97, 0.98, 0.99, 1). To read more, please click HERE
isBgColored - we will need it the check the current state of “our” heart (heart button) at the moment (green / black with a white border)
In the component’s body we just need to unpack styles (animateBigCircle and animateBigCircle) from the useBouncingCirclesAnimatedStyle hook and pass them to the AnimatedCircle components. As you can see, these components have a different borderWidth. The small ones have a wider border, and the big one is much slimmer. They also have different animation styles.
Let’s go to the last part and explore the useBouncingCirclesAnimatedStyle hook.
Animated - an element from the Reanimated library. We can use it to create different animated components. We need it here to type incoming properties
interpolate - this function transforms one scope to another. For example, if we have a shared value in scope [0-1], we can change it to the [0-100] or the [50-0], etc. For example, if we move the square from point 0 to 1 and interpolate this value to 0-50, then the square will move 50 times on. We can also declare a few points inside and modify what happens in different time points. For example [0, 0.5, 1](input) can be modified to the [0, 0.7, 1](output). What’s the purpose of that? It’s a bit crazy, but now that what should happen in point 0.5 will happen in point 0.7. In fact, we extended the first range and shortened the second one. To read more, please click HERE
useAnimatedStyle - creates animated style values for the Animated Components (for example View). Returns the same values as classic style values, for example: width, rotate; but customized for animations. To read more click HERE
And we have two incoming properties:
heartAnimation - shared value. We will use it to animate elements. Its fundamental architecture allows it to rise and fall in such a way that it can be used for the animation. In the process, this value is going through every single hundredth number. For example from 0 to 1: (0.01, 0.02, 0.03, 0.04, 0.05 ... 0.96, 0.97, 0.98, 0.99, 1). To read more, please click HERE
isBgColored - we will need it to check the current state of “our” heart (heart button) at the moment (green / black with a white border)
The first variable is the animateBigCircle animated style.
Because we want from our circle to appear in full opacity and vanish in time, we will animate it from 1 to 0. To do that, we can once again use the well known heartAnimation value. As its input is from 0 to 1, we just need to interpolate it and receive output from 1 to 0. This simple interpolate takes value that is growing from 0 to 1, and turns it to the value which is going from 1 to 0. Our opacity will be decreasing. And the opacity is now **prepared.
The scale is also very simple. We just need to take heartAnimation again and multiply its value 5 times. Because of that we have input [0, 1] and output [0, 4].
If the heart is clicked, and turns green (isBgColored === true) we want to apply those styles. Otherwise (isBgColored === false), we will just return an empty style object and do nothing.
With the animated style for the small circle, only difference is that we want to multiply its size 4 times.
That’s all. Our animated styles are ready for use.
Our Spotify Heart Animation is READY!
Share our work
Reanimated
Typescript
React Native
Styled-components
Written By
Conrad Gauza
See related posts
No items found.
Do you need help with developing react solutions?
Leave your contact info and we’ll be in touch with you shortly
Leave your contact info and we’ll be in touch with you shortly
Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
By clicking “Accept all”, you agree to the storing of cookies on your device to enhance site navigation, analyze site usage, and assist in our marketing efforts. View our Privacy Policy for more information.