React Native School is 40% off when you sign up before December 4!

React Native Animation Using Reanimated 2

Author

Aditya Pahilwani

Love to talk about react native, cross-platform development, and how things work under the hood.

Last Updated: October 29, 2021

Hello everyone 👋, I guess you want to learn how to create some amazing animations in React-Native with Reanimated 2. Don't worry you're in the right place.

Editor's Note: Thanks to Aditya for sharing this article with React Native School. Check out his website aditya01.hashnode.dev and follow him on Twitter.

What we are creating?

We are going to create a bottom navigation bar animation. This will animate the color and view when you switch tabs.😍😍

Tab Bar Demo

The above gif looks amazing, right? we are going to create the same animation.

Prefer Video?

I have given a full talk on this topic in case you wanna check out

Understanding The Basic Animation Needs

02

So in our UI, we got 2 things Icons and selectedContainer. On every tab-press, we need to animate our position of selectedContainer to the selected-tab along with the selected color.

So, it's clear that we want the positions and coordinates of every icon, so we can translate our selectedContainer to the selected tab, the quick solution which might come to your mind is onLayout but that's a kind of expensive operation. We are going to ditch onLayout and instead use some neat tricks and basic maths to calculate the position of the tabs.

We got a basic idea, now we can start with the code and solve all the problems and challenges which will come along.

What we are going to do

There are a lot of things in this tutorial. We are going to break our tutorial into smaller parts, so you can understand it better.

  1. Boiler code
  2. Understanding the logic
  3. Adding animation config to translate out selected tab
  4. Solution for hidden tabs
  5. Decouple behind the scene magic
  6. Bonus: Handling keyboard popups
  7. Adding animation to handle keyboard popups
  8. Celebration

Let's go through each of them one by one. 🚀

Boiler code

Before the Boiler code, let me walk through you the folder structure we are going to follow for this blog. But you can follow any structure as you like.

03

  • constant: This will contain some constants which will be used in our components and styles.
  • style: This will contain styling for our component.
  • bottomNavigation: This will contain our main component.
  • index: This will export our component.

This is our constant file.

// constant.js

import { Dimensions } from 'react-native';
const { width } = Dimensions.get('window');
export const icons = [
  { name: 'send', color: '#28c7fa' },
  { name: 'home', color: '#ff304f' },
  { name: 'search', color: '#22eaaa' },
  { name: 'user', color: '#ffd717' },
];
export const iconContainer = 50;
export const tabBlock = width / icons.length;
export const activeIndex = 0;
export const bottomBarHeight = 70;
export const bottomBarWidth = width;
  • icons is an array that holds our tabs data which are name and color, name indicates the name of Feather icon, and color indicates the color of the icon.
  • iconContainer represents the size of the icon container while will reside inside the tabBlock.
  • tabBlock represents the size of tabs, in the ideal case you just use - justifyContent:'space-evenly' in a container, but we are going to manually set the size of tabs for our secret trick of calculating tab positions.

Let's move to our style file.

// style.js

import { StyleSheet } from 'react-native';
import {
  iconContainer,
  tabBlock,
  bottomBarHeight,
  bottomBarWidth,
} from './constant.js';

const styles = StyleSheet.create({
  bottomTabContainer: {
    elevation: 4,
    height: bottomBarHeight,
    width: bottomBarWidth,
    backgroundColor: 'black',
    flexDirection: 'row',
    alignItems: 'center',
  },
  tabContainer: {
    height: '100%',
    width: tabBlock,
    justifyContent: 'center',
    alignItems: 'center',
  },
  iconContainer: {
    height: iconContainer,
    justifyContent: 'center',
    alignItems: 'center',
    width: iconContainer,
    borderRadius: iconContainer / 2,
  },
  selectedContainer: {
    overflow: 'hidden',
    height: iconContainer,
    justifyContent: 'center',
    alignItems: 'center',
    width: iconContainer,
    borderRadius: iconContainer / 2,
    position: 'absolute',
    bottom: bottomBarHeight / 2 - iconContainer / 2,
    left: 0,
  },
});

export default styles;

The only piece of weird style is in selectedContainer, now our selectedContainer will be positioned absolute, we need to make sure it is properly centered in context of our bottom-navigation so we add bottom: bottomBarHeight / 2 - iconContainer / 2. This will make sure our selectedContainer will be properly centered.

The main component

This is our main component file, where we will add our beautiful animations later.

// bottomNavigation.js

import React from 'react';
import { View, Pressable, Dimensions } from 'react-native';
import { Feather } from '@expo/vector-icons';
import styles from './style';
import Animated, { useSharedValue } from 'react-native-reanimated';
import { activeIndex, icons } from './constant.js';

const BottomNavigationTapPress = (props) => {
  const selectedIndex = useSharedValue(activeIndex);
  return (
    <Animated.View
      style={[styles.bottomTabContainer, { position: 'absolute', bottom: 0 }]}
    >
      {icons.map((item, index) => {
        return (
          <Pressable
            key={item.name}
            onPress={() => {
              selectedIndex.value = index;
            }}
          >
            <View style={styles.tabContainer}>
              <Animated.View style={[styles.iconContainer]}>
                <Feather name={item.name} size={24} color={item.color} />
              </Animated.View>
            </View>
          </Pressable>
        );
      })}

      <Animated.View style={[styles.selectedContainer]}></Animated.View>
    </Animated.View>
  );
};

export default BottomNavigationTapPress;

The boilerplate code is done.

Thanks for sticking with me and following along with the boilder code. Now you will see a similar output as attached below.

04

It looks very weird, right? the selectedContainer is aligned in the wrong position and it's overlapping on our icon too. So we need to fix our alignment of selectedContainer and want it to translate to the selected tab respectively. Before coming with a solution let's take a step back and understand our UI first.

Understanding the logic

Now let's understand our UI and how we can translate our selectedContainer to specific tab position without onLayout.

05

  • The blue-colored-tab is our tabContainer.
  • The grey-colored-container is our iconContainer which shares the same size of selectedContainer. (note: This is just for explanation, the background color of iconContainer won't be displayed, it would be transparent.)

To align our selectedContainer and translating to the respective selected tab, we want their positions so we can translate our container, Accordingly.

Now we are going to use a neat trick and do some basic maths. As our every tabContainer and iconContainer are of the same size. That means we can manually calculate the positions based on the index of the tab.

Guess what? All of the constants required to make this calculation is already with us.

selectedIndex.value * tabBlock + tabBlock / 2 - iconContainer / 2;

Let me explain the piece of magic with this simple calculation.

  • selectedIndex.value represents the selected-tab index value.
  • tabBlock and iconContainer were the constant which we declared in our separate constant file giving us the size of the tab and icon-container.
  • selectedIndex.value * tabBlock gives us the starting value of our tabBlock, but we want our container to be centered aligned with the tab.
  • tabBlock / 2 gives us the center point of our tabBlock, with this we got our center point of selected tab.
  • Still, our iconHolder won't be centered aligned because our selectedContainer will paint from the exact center of the tabBlock. As iconContainer and our selectedContainer share the same size, we can use our constant to compute the exact position to start the paint from.
  • Our last bit of magic is iconContainer / 2, which will give us the center point of our iconContainer.
  • Now our simple calculation gives us the exact center point of tabs from where the paint should start.

Adding animation config to translate out selected tab

For Animation config, you can check this amazing tool by mohit.

import React from 'react';
import { View, Pressable, Dimensions } from 'react-native';
import { Feather } from '@expo/vector-icons';
import styles from './style';
import Animated, { useSharedValue } from 'react-native-reanimated';
import { tabBlock, iconContainer, activeIndex, icons } from './constant.js';

const BottomNavigationTapPress = (props) => {
  const selectedIndex = useSharedValue(activeIndex);
  const config = {
    damping: 10,
    mass: 1,
    stiffness: 100,
    velocity: 2,
  };
  const animateSelectedContainer = useAnimatedStyle(() => {
    return {
      backgroundColor: icons[selectedIndex.value].color,
      transform: [
        {
          translateX: withSpring(
            selectedIndex.value * tabBlock + tabBlock / 2 - iconContainer / 2,
            {
              config,
            }
          ),
        },
      ],
    };
  });
  return (
    <Animated.View
      style={[styles.bottomTabContainer, { position: 'absolute', bottom: 0 }]}
    >
      {icons.map((item, index) => {
        return (
          <Pressable
            key={item.name}
            onPress={() => {
              selectedIndex.value = index;
            }}
          >
            <View style={styles.tabContainer}>
              <Animated.View style={[styles.iconContainer]}>
                <Feather name={item.name} size={24} color={item.color} />
              </Animated.View>
            </View>
          </Pressable>
        );
      })}

      <Animated.View
        style={[styles.selectedContainer, animateSelectedContainer]}
      ></Animated.View>
    </Animated.View>
  );
};

export default BottomNavigationTapPress;

Finally as you can see, our selectedContainer is now properly centered aligned and animate to selected-tab. But our icons are getting overlapped by selectedContainer and the user can't see the icons.

Solution for hidden tabs

The solution is actually quite simple and straight. We are going to replicate our bottomNavigationBar just beneath it, but it will be out of the screen and the user will never see it.

I know you might be thinking but why do we need to replicate our bottomNavigationBar? The answer is simple whenever our selectedContainer overlaps our iconContainer, we are going to translate the icon from our duplicate bottomNavigationBar to original bottomNavigationBar. This way we will place the icons on top of our selectedContainer.

import React from 'react';
import { View, Pressable } from 'react-native';
import { Feather } from '@expo/vector-icons';
import styles from './style';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import {
  tabBlock,
  iconContainer,
  activeIndex,
  icons,
  bottomBarHeight,
} from './constant.js';

const BottomNavigationTapPress = (props) => {
  const selectedIndex = useSharedValue(activeIndex);
  const config = {
    damping: 10,
    mass: 1,
    stiffness: 100,
    velocity: 2,
  };
  const animateSelectedContainer = useAnimatedStyle(() => {
    return {
      backgroundColor: icons[selectedIndex.value].color,
      transform: [
        {
          translateX: withSpring(
            selectedIndex.value * tabBlock + tabBlock / 2 - iconContainer / 2,
            {
              config,
            }
          ),
        },
      ],
    };
  });

  const animateIconContainer = ({ index }) => {
    return useAnimatedStyle(() => {
      let offSet = index === selectedIndex.value ? -bottomBarHeight : 0;
      return {
        transform: [
          {
            translateY: withSpring(offSet, config),
          },
        ],
      };
    });
  };

  return (
    <>
      <Animated.View
        style={[styles.bottomTabContainer, { position: 'absolute', bottom: 0 }]}
      >
        {icons.map((item, index) => {
          return (
            <Pressable
              key={item.name}
              onPress={() => {
                selectedIndex.value = index;
              }}
            >
              <View style={[styles.tabContainer, { overflow: 'hidden' }]}>
                <Animated.View
                  style={[
                    styles.iconContainer,
                    animateIconContainer({ index }),
                  ]}
                >
                  <Feather name={item.name} size={24} color={item.color} />
                </Animated.View>
              </View>
            </Pressable>
          );
        })}

        <Animated.View
          style={[styles.selectedContainer, animateSelectedContainer]}
        ></Animated.View>
      </Animated.View>

      <Animated.View
        style={[
          styles.bottomTabContainer,
          { position: 'absolute', bottom: -bottomBarHeight },
        ]}
      >
        {icons.map((item, index) => {
          return (
            <View
              style={styles.tabContainer}
              key={`${item.name} unique unique unique `}
            >
              <Animated.View
                style={[styles.iconContainer, animateIconContainer({ index })]}
              >
                <Feather name={item.name} size={24} color="white" />
              </Animated.View>
            </View>
          );
        })}
      </Animated.View>
    </>
  );
};
export default BottomNavigationTapPress;

We finally did it, our animation is finally ready, and it looks damn beautiful.

Decouple behind the scene magic.

As you can see our duplicate bottomNavigationBar is placed beneath our original. Icons from both bottomNavigationBar are getting animated and translating one position above with bottomBarHeight. This is exactly what we are doing with the animateIconContainer style.

Thanks to overflow: "hidden" we don't see the original translated icons on our screen.

Bonus: Handling keyboard popups.

We got our beautiful bottomNavigationBar with us, but we also need to handle the special textInput case. To understand better let's have a look at how our bottomNavigationBar behaves with textInput now.

As you can see, our bottomNavigationBar is getting stuck with the keyboard and it looks weird. In the ideal case, the bottomNavigationBar should animate and hide.

Understanding the needs

  • We need a listener to know when whether the keyboard is visible or not.
  • When any textInput is touched and the keyboard is visible, we need to animate our bottomNavigationBar out of the screen.
  • When textInput focus is gone and the keyboard is closed, we need to animate back our bottomNavigationBar to the screen.

Adding animation to handle keyboard popups

First let's write one custom hook, which will return whether keyboard is visible or not.

import React, { useState, useEffect } from 'react';
import { Platform, Keyboard } from 'react-native';
const IsKeyBoardShown = () => {
  const [keyboardVisible, setKeyboardVisible] = useState(false);
  const handleKeyboardShow = () => {
    setKeyboardVisible(true);
  };
  const handleKeyboardHide = () => {
    setKeyboardVisible(false);
  };
  useEffect(() => {
    if (Platform.OS === 'ios') {
      Keyboard.addListener('keyboardWillShow', handleKeyboardShow);
      Keyboard.addListener('keyboardWillHide', handleKeyboardHide);
    } else {
      Keyboard.addListener('keyboardDidShow', handleKeyboardShow);
      Keyboard.addListener('keyboardDidHide', handleKeyboardHide);
    }

    return () => {
      if (Platform.OS === 'ios') {
        Keyboard.removeListener('keyboardWillShow', handleKeyboardShow);
        Keyboard.removeListener('keyboardWillHide', handleKeyboardHide);
      } else {
        Keyboard.removeListener('keyboardDidShow', handleKeyboardShow);
        Keyboard.removeListener('keyboardDidHide', handleKeyboardHide);
      }
    };
  }, [handleKeyboardHide, handleKeyboardShow]);
  return { keyboardVisible: keyboardVisible };
};

export default IsKeyBoardShown;

This hook is pretty simple, it just adds listeners to keyboard events and returns whether it's visible or not.

Now let's add the required animations in our main file.

import React from 'react';
import { View, Pressable } from 'react-native';
import { Feather } from '@expo/vector-icons';
import styles from './style';
import Animated, {
  useAnimatedStyle,
  useSharedValue,
  withSpring,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import {
  tabBlock,
  iconContainer,
  activeIndex,
  icons,
  bottomBarHeight,
} from './constant.js';
import IsKeyBoardShown from './isKeyBoardShownHook';

const BottomNavigationTapPress = (props) => {
  const { keyboardVisible } = IsKeyBoardShown();
  const selectedIndex = useSharedValue(activeIndex);
  const config = {
    damping: 10,
    mass: 1,
    stiffness: 100,
    velocity: 2,
  };
  const animateSelectedContainer = useAnimatedStyle(() => {
    return {
      backgroundColor: icons[selectedIndex.value].color,
      transform: [
        {
          translateX: withSpring(
            selectedIndex.value * tabBlock + tabBlock / 2 - iconContainer / 2,
            {
              config,
            }
          ),
        },
      ],
    };
  });

  const animateIconContainer = ({ index }) => {
    return useAnimatedStyle(() => {
      let offSet = index === selectedIndex.value ? -bottomBarHeight : 0;
      return {
        transform: [
          {
            translateY: withSpring(offSet, config),
          },
        ],
      };
    });
  };
  const handleKeyboardAnimation = ({ forActiveBar }) => {
    return useAnimatedStyle(() => {
      let offSet = forActiveBar ? bottomBarHeight * 2 : bottomBarHeight;
      return {
        transform: [
          {
            translateY: withTiming(keyboardVisible ? offSet : 0, {
              duration: 500,
              easing: Easing.linear,
            }),
          },
        ],
      };
    });
  };
  return (
    <>
      <Animated.View
        style={[
          styles.bottomTabContainer,
          { position: 'absolute', bottom: 0 },
          handleKeyboardAnimation({ forActiveBar: true }),
        ]}
      >
        {icons.map((item, index) => {
          return (
            <Pressable
              key={item.name}
              onPress={() => {
                selectedIndex.value = index;
              }}
            >
              <View style={[styles.tabContainer, { overflow: 'hidden' }]}>
                <Animated.View
                  style={[
                    styles.iconContainer,
                    animateIconContainer({ index }),
                  ]}
                >
                  <Feather name={item.name} size={24} color={item.color} />
                </Animated.View>
              </View>
            </Pressable>
          );
        })}

        <Animated.View
          style={[styles.selectedContainer, animateSelectedContainer]}
        ></Animated.View>
      </Animated.View>

      <Animated.View
        style={[
          styles.bottomTabContainer,
          { position: 'absolute', bottom: -bottomBarHeight },
          handleKeyboardAnimation({ forActiveBar: false }),
        ]}
      >
        {icons.map((item, index) => {
          return (
            <View
              style={styles.tabContainer}
              key={`${item.name} unique unique unique `}
            >
              <Animated.View
                style={[styles.iconContainer, animateIconContainer({ index })]}
              >
                <Feather name={item.name} size={24} color="white" />
              </Animated.View>
            </View>
          );
        })}
      </Animated.View>
    </>
  );
};

export default BottomNavigationTapPress;

Now handleKeyboardAnimation does a very simple thing, whenever the keyboard is visible it will animate our bottomNavigationBar out of the screen and when keyboard is not visible it animates back to the screen. With forActiveBar we decide the offset to translate for bottomNavigationBar, because we need to make sure our original and duplicate bottomNavigationBar doesn't overlap each other. So we need to animate our duplicate bottomNavigationBar one position below.

We are finally done!!

As you can see our animation looks neat and our Bottom-Navigation-Bar is complete.

Github repo for the code.

Celebration

React Native School Logo

React Native School

Want to further level up as a React Native developer? Join React Native School! You'll get access to all of our classes and our private Slack community.