Floating Heart Animation using React-Native-Reanimated

Hey folks, in this article we will be creating floating heart animation as shown in the video. We will be creating the animation using React-Native-Reanimated library.

floatingHeart-part2.gif

You might have probably seen this animation in popular mobile apps like Instagram.

instagram-live-heartAnimation.gif

First things first !!

node --version
16.14.1
npm --version
8.5.0
  • Create a new react native app using the following command -
 npx react-native init Floating_Hearts
  • Remove the template code in App.js and ensure App.js looks like the following -
App.js

import React from 'react';
import {
  SafeAreaView,
  StyleSheet,
  Text,
  View,
} from 'react-native';

const App = () => {

  return (
    <SafeAreaView>

    </SafeAreaView>
  );
};

const styles = StyleSheet.create({

});

export default App;
  • Add react-native-vector-icons library for using the Heart Icon in our project with the following command -
 npm install --save react-native-vector-icons
  • Add the following line in android/app/build.gradle file to complete your android setup for the library.
apply from: "../../node_modules/react-native-vector-icons/fonts.gradle"
  • Add react-native-reanimated library for Animation.
 npm install react-native-reanimated
  • Add the following line for Reanimated's babel plugin in babel.config.js
  plugins: ['react-native-reanimated/plugin']

You can refer to the docs for the React Native libraries added from this section -

  1. React Native Reanimated
  2. React Native Vector Icons

Final package.json file should be similar to -

{
  "name": "Floating_Hearts",
  "version": "0.0.1",
  "private": true,
  "scripts": {
    "android": "react-native run-android",
    "ios": "react-native run-ios",
    "start": "react-native start",
    "test": "jest",
    "lint": "eslint ."
  },
  "dependencies": {
    "react": "17.0.2",
    "react-native": "0.68.2",
    "react-native-reanimated": "^2.8.0",
    "react-native-vector-icons": "^9.1.0"
  },
  "devDependencies": {
    "@babel/core": "7.18.5",
    "@babel/runtime": "7.18.3",
    "@react-native-community/eslint-config": "2.0.0",
    "babel-jest": "26.6.3",
    "eslint": "7.32.0",
    "jest": "26.6.3",
    "metro-react-native-babel-preset": "0.67.0",
    "react-test-renderer": "17.0.2"
  },
  "jest": {
    "preset": "react-native"
  }
}
  • Create a new file AnimatedHeart.js that will contains the Heart icon initially. It imports Icon from 'react-native-vector-icons' and uses AntDesign Heart icon.
  • Change the color of the Icon to red and give it a size of 32.
AnimatedHeart.js

import React from 'react';
import Icon from 'react-native-vector-icons/AntDesign';

const AnimatedHeart = () => {
  return <Icon name="heart" color={'red'} size={32} />;
};

export default AnimatedHeart;
  • Import the AnimatedHeart Component in App.js to render the Icon on screen.
  • Add some styles for the SafeAreaView so that the Icon is visible in the centre of the screen.
App.js

import React from 'react';
import {SafeAreaView, StyleSheet, Text, View} from 'react-native';
import AnimatedHeart from './AnimatedHeart';

const App = () => {
  return (
    <SafeAreaView style={styles.container}>
        <AnimatedHeart />
    </SafeAreaView>
  );
};

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    backgroundColor: 'white',
  },
});

export default App;
  • Import Dimensions from 'react-native' and create a constant window. This will be needed in implementing the animation further.

  • In AnimatedHeart.js file wrap the icon with a TouchableOpacity so that the animation is triggered on pressing the Heart Icon.

AnimatedHeart.js

import React from 'react';
import Icon from 'react-native-vector-icons/AntDesign';
import {TouchableOpacity, Dimensions} from 'react-nativer';

const window = Dimensions.get("window");

const AnimatedHeart = () => {
  return (
    <TouchableOpacity onPress={animate}>
      <Icon name="heart" color={'red'} size={32} />
    </TouchableOpacity>
  );
};

export default AnimatedHeart;

Now lets implement the Animation

  • Import the React-native-reanimated library.
import Animated, {  useAnimatedStyle,
    useSharedValue,
    withTiming,
    withDelay,
    interpolate }
from 'react-native-reanimated';
  • Create a value positionY whose value will change using useSharedValue() hook.

This hook is similar to Animated.Value() of Animated library. It is used to create a variable whose value will change during the animation.

const positionY = useSharedValue(0);
  • Create few constants that will be used for animation.
const maxPositionY = Math.ceil(window.height / 0.5) * 0.1;
  • Create the animate function that is passed to the onPress prop of TouchableOpacity function. This animate function will change the value of positionY using withTiming() hook.
  const animate = () => {
    positionY.value = withTiming(-maxPositionY, {
      duration: 2000,
    });
  };
  • Create Animation styles that are passed to Animated.View which will contain the components which will animate. Wrap the TouchableOpacity with Animated.View and pass animatedStyle to style prop of Animated.View. The animation styles are created using the useAnimatedStyle() hook.
const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateY: positionY.value,
        },
      ],
    };
  });

At this stage the AnimatedHeart.js file will look like this -

import React from 'react';
import Icon from 'react-native-vector-icons/AntDesign';
import {TouchableOpacity, Dimensions} from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  withDelay,
  interpolate,
} from 'react-native-reanimated';

const window = Dimensions.get('window');

const AnimatedHeart = () => {
  const positionY = useSharedValue(0);
  const maxPositionY = Math.ceil(window.height / 0.5) * 0.1;
  const animate = () => {
    positionY.value = withTiming(-maxPositionY, {
      duration: 2000,
    });
  };

  const animatedStyle = useAnimatedStyle(() => {
    return {
      transform: [
        {
          translateY: positionY.value,
        },
      ],
    };
  });

  return (
    <Animated.View style={animatedStyle}>
      <TouchableOpacity onPress={animate}>
        <Icon name="heart" color={'red'} size={32} />
      </TouchableOpacity>
    </Animated.View>
  );
};

export default AnimatedHeart;

Our Animation at this stage is like this - floatingHeart-part1.gif

To add fading affect and moving the heartIcon in x-direction we will have to add few more animation styles that depend on the positionY value. For this we will use the interpolate() from react-native-reanimated library.

interpolate() takes 3 parameters -

value, inputRange, outputRange.

  • Create constants yAnimation, opacityAnimation and xAnimation and change the animatedStyle constant. The opacityAnimation is used to change the opacity of the Icon which gives the fading affect. The xAnimation is used to change the position of Icon along x axis.

Both xAnimation, opacityAnimation maps the value of yAnimation from inputRange to outputRange specified. yAnimation maps the value of positionY from inputRange to outputRange using interpolate().

 const animatedStyle = useAnimatedStyle(() => {
    const yAnimation = interpolate(
      positionY.value,
      [maxPositionY * -1, 0],
      [maxPositionY, 0],
    );

    const xAnimation = interpolate(
      yAnimation,
      [
        0,
        maxPositionY * 0.25,
        maxPositionY * 0.35,
        maxPositionY * 0.45,
        maxPositionY * 0.55,
        maxPositionY * 0.65,
      ],
      [25, -5, 15, -10, 30, 20],
    );

    const opacityAnimation = interpolate(yAnimation, [0, maxPositionY], [1, 0]);

    return {
      transform: [
        {
          translateY: positionY.value,
        },
        {
          translateX: xAnimation,
        },
      ],
      opacity: opacityAnimation,
    };
  });

At this stage the AnimatedHeart.js file will look like this -

import React from 'react';
import Icon from 'react-native-vector-icons/AntDesign';
import {TouchableOpacity, Dimensions} from 'react-native';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withTiming,
  withDelay,
  interpolate,
} from 'react-native-reanimated';

const window = Dimensions.get('window');

const AnimatedHeart = () => {
  const positionY = useSharedValue(0);
  const maxPositionY = Math.ceil(window.height / 0.5) * 0.1;
  const animate = () => {
    positionY.value = withTiming(-maxPositionY, {
      duration: 2000,
    });
  };

  const animatedStyle = useAnimatedStyle(() => {
    const yAnimation = interpolate(
      positionY.value,
      [maxPositionY * -1, 0],
      [maxPositionY, 0],
    );

    const xAnimation = interpolate(
      yAnimation,
      [
        0,
        maxPositionY * 0.25,
        maxPositionY * 0.35,
        maxPositionY * 0.45,
        maxPositionY * 0.55,
        maxPositionY * 0.65,
      ],
      [25, -5, 15, -10, 30, 20],
    );

    const opacityAnimation = interpolate(yAnimation, [0, maxPositionY], [1, 0]);

    return {
      transform: [
        {
          translateY: positionY.value,
        },
        {
          translateX: xAnimation,
        },
      ],
      opacity: opacityAnimation,
    };
  });

  return (
    <Animated.View style={animatedStyle}>
      <TouchableOpacity onPress={animate}>
        <Icon name="heart" color={'red'} size={32} />
      </TouchableOpacity>
    </Animated.View>
  );
};

export default AnimatedHeart;

Now the animation will be like -

floatingHeart-part2.gif

Important Things to Note -

  • Here is a mapping for you that maps some components from Animated Library to corresponding hooks and animations in Reanimated Library that works similarly.

Mapping.png

  • useSharedValue() Hook is used to create a value that will change during animation.

  • useAnimatedStyle() Hook is used to create styles that are applied to the Animated.View component which contains components which have to animate.

  • withTiming() is used to create a timed animation in Reanimated library. It takes the final value and an optional config object to specify timing and easing.

  • interpolate() is used to map values from the input range to values from the output range. It takes mainly 3 parameters value, inputRange and outputRange.

Voohooooooo!! We have complete the project. Cheers !!

Follow me on Twitter for more tech content on ReactNative and Flutter.

Did you find this article valuable?

Support Parjanya Aditya Shukla by becoming a sponsor. Any amount is appreciated!