Practical Animation Examples in React Native

In this tutorial, you're going to learn how to implement animations that are commonly used in mobile apps. Specifically, you're going to learn how to implement animations that:

  • Provide visual feedback: for example, when a user presses a button, you want to use animations to show the user that the button is indeed being pressed.
  • Show the current system status: when performing a process that doesn't finish instantly (e.g. when uploading a photo or sending an email), you want to show an animation so the user has an idea how long the process will take.
  • Visually connect transition states: when a user presses a button to bring something to the front of the screen, this transition should be animated so the user knows where the element originated. 
  • Grab the user's attention: when there's an important notification, you can use animations to grab the user's attention.

This tutorial is a sequel to my Animate Your React Native App post. So if you're new to animations in React Native, be sure to check that out first, because some of the concepts that will be used in this tutorial are explained in more detail there.

Also, if you want to follow along, you can find the full source code used in this tutorial in its GitHub repo.

What We're Building

We're going to build an app which implements each of the different types of animation that I mentioned earlier. Specifically, we're going to create the following pages, each of which will implement animations for a different purpose.

  • News Page: uses gestures to provide visual feedback and show current system status.
  • Buttons Page: uses buttons to provide visual feedback and show current system status.
  • Progress Page: uses a progress bar to show current system status.
  • Expand Page: visually connects transition states using expanding and shrinking motions.
  • AttentionSeeker Page: uses eye-catching movements to grab the user's attention. 

If you want to see a preview of each of the animations, check out this Imgur album.

Setting Up the Project

Start by creating a new React Native project:

Once the project is created, navigate inside the newly created folder, open the package.json file, and add the following to dependencies:

Execute npm install to install those two packages. react-native-animatable is used to easily implement animations, and react-native-vector-icons is used to render icons for the expand page later on. If you don't want to use icons, you can just stick with using the Text component. Otherwise, follow the installation instructions of react-native-vector-icons on their GitHub page.

Building the App

Open the index.android.js file or index.ios.js file and replace the existing contents with the following:

Once that's done, be sure to create the corresponding files so you don't get any errors. All the files that we will be working on are stored under the src directory. Inside that directory are the following folders:

  • components: reusable components that will be used by other components or pages. 
  • img: images that will be used throughout the app. You can get the images from the GitHub repo.
  • pages: the pages of the app.

News Page

Let's start with the News page. 

 

First, add the components that we'll be using:

You should already be familiar with most of these, except for RefreshControl and the custom NewsItem component, which we'll be creating later. RefreshControl is used to add a "pull to refresh" functionality inside a ScrollView or ListView component. So it's actually the one that will handle the swipe down gesture and animation for us. No need to implement our own. As you gain more experience in using React Native, you'll notice that animations are actually built in to some components, and there's no need to use the Animated class to implement your own.

Create the component that will house the whole page: 

Inside the constructor, initialize an animated value for storing the current opacity (opacityValue) of the news items. We want the news items to have less opacity while the news items are being refreshed. This gives the user an idea that they can't interact with the whole page while the news items are being refreshed. is_news_refreshing is used as a switch to indicate whether the news items are currently being refreshed or not. 

The opacity() function is the one that will trigger the animation for changing the opacity.

Inside the render() function, define how the opacity value will change. Here, the outputRange is [1, 0, 1], which means that it will start at full opacity, then go to zero opacity, and then back to full opacity again. As defined inside the opacity() function, this transition will be done over the course of 3,500 milliseconds (3.5 seconds). 

The <RefreshControl> component is added to the <ScrollView>. This calls the refreshNews() function whenever the user swipes down while they're at the top of the list (when scrollY is 0). You can add the colors prop to customize the color of the refresh animation.

Inside the <ScrollView>, use an <Animated.View> component and set the style to that of the opacity value:

The refreshNews() function calls the opacity() function and updates the value of is_news_refreshing to true. This lets the <RefreshControl> component know that the refresh animation should already be shown. After that, use the setTimeout() function to update the value of is_news_refreshing back to false after 3,500 milliseconds (3.5 seconds). This will hide the refresh animation from view. By that time, the opacity animation should also be done since we set the same value for the duration in the opacity function earlier.

renderNewsItems() takes the array of news items that we declared earlier inside the constructor() and renders each of them using the <NewsItem> component.

NewsItem Component

The NewsItem component (src/components/NewsItem.js) renders the title and the website of the news item and wraps them inside the <Button> component so that they can be interacted with.

Button Component

The Button component (src/components/Button.js) uses the TouchableHighlight component to create a button. The underlayColor props is used to specify the color of the underlay when the button is pressed. This is React Native's built-in way of providing visual feedback; later on, in the Buttons Page section, we'll take a look at other ways buttons can provide visual feedback.

Going back to the NewsPage component, add the styling:

Buttons Page

 

The buttons page (src/pages/ButtonsPage.js) shows three kinds of buttons: the commonly used button which gets highlighted, a button which becomes slightly larger, and a button which shows the current state of an operation. Start by adding the necessary components:

Earlier, you saw how the Button component works, so we'll just focus on the other two buttons.

Scaling Button Component

First, let's take a look at the scaling button (src/components/ScalingButton.js). Unlike the button that we used earlier, this uses the built-in TouchableWithoutFeedback component to create a button. Earlier, we used the TouchableHighlight component, which comes with all the bells and whistles for something to be considered a button. You can think of TouchableWithoutFeedback as a bare-bones button in which you have to specify everything that it needs to do when a user taps on it. This is perfect for our use case because we don't have to worry about default button behavior getting in the way of the animation that we want to implement.

Just like the Button component, this will be a functional type of component since we don't really need to work with the state.

Inside the component, create an animated value that will store the current button scale.

Add the function that will start the scale animation. We don't want the app to appear slow, so make the duration as low as possible but also high enough so the user can perceive what's happening. 300 milliseconds is a good starting point, but feel free to play around with the value.

Define how the button will scale (outputRange) depending on the current value (inputRange). We don't want it to become too big so we stick with 1.1 as the highest value. This means it will be 0.1 bigger than its original size halfway through (0.5) the whole animation.

The onPress() function executes the scale animation first before calling the method passed in by the user through the props.

The getContent() function outputs the child components if it's available. If not, a Text component containing the label props is rendered.

Add the styles and export the button:

Stateful Button Component

Next is the stateful button (src/components/StatefulButton.js). When pressed, this button will change its background color and show a loading image until the operation that it's performing is done. 

The loading image that we'll be using is an animated gif. By default, React Native on Android doesn't support animated gifs. To make it work, you have to edit the android/app/build.gradle file and add compile 'com.facebook.fresco:animated-gif:0.12.0' under the dependencies like so:

If you're on iOS, animated gifs should work by default.

Going back to the stateful button component, just like the scaling button, this uses the TouchableWithoutFeedback component to create the button since it will also implement its own animation.

Unlike the scaling button, this component will be a full-fledged class-based component since it manages its own state.

Inside the constructor(), create an animated value for storing the current background color. After that, initialize the state that acts as a switch for storing the current status of the button. By default, this is set to false. Once the user taps on the button, it will be updated to true and will only be set to false again once the imaginary process is done executing.

Inside the render() function, specify the different background colors that will be used based on the current value of the animated value.

Next, wrap everything inside the TouchableWithoutFeedback component, and inside the <Animated.View> is where the animated background color is applied. We also render the loader image if the current value of is_loading is true. The button label also changes based on this value.

When the button is pressed, it first executes the function that was passed via the props before doing the animation.

The changeColor() function is responsible for updating the state and animating the background color of the button. Here we're assuming that the process will take 3,000 milliseconds (3 seconds). But in a real-world scenario, you can't always know how long a process would take. What you can do is to have the animation execute for a shorter period of time and then call the changeColor() function recursively until the process is done.

Add the styles:

Back on the Buttons page: create the component, render the three kinds of buttons, and add their styles.

Progress Page

 

The Progress page (src/pages/ProgressPage.js) shows a progress animation to the user during a long-running process. We will implement our own instead of using the built-in components because React Native doesn't have a unified way of implementing a progress bar animation yet. If you're interested, here are links to the two built-in progress bar components:

To build our Progress page, start by importing the components that we need:

We're using Dimensions to get the device width. From that, we can calculate the width available for the progress bar. We'll do so by subtracting the sum of the left and right paddings that we will add to the container, and also the left and right borders that we will add to the progress bar container. 

For the above formula to make sense, let's skip right to the styles:

The container has a padding of 20 on each side—thus we subtract 40 from the available_width. The progress_container has a border of 6 on each side, so we just double that again and subtract 12 from the progress bar width.

Create the component, and inside the constructor create the animated value for storing the current animation values for the progress bar. 

I said "values" because this time we're going to use this single animated value to animate both the width and the background color of the progress bar. You'll see this in action later on. 

Aside from that, you also need to initialize the current progress in the state.

Inside the render() function, the progress_container acts as the container for the progress bar, and the <Animated.View> inside it is the actual progress bar whose width and background color will change depending on the current progress. Below it, we're also rendering the current progress in text form (0% to 100%). 

The styles for the progress bar are returned by the getProgressStyles() function. Here we're using the animated value from earlier to calculate the width and background color. This is done instead of creating a separate animated value for each animation because we're interpolating the same value anyway. If we used two separate values, we would need to have two animations in parallel, which is less efficient.

The animation is immediately executed once the component is mounted. Start by setting the initial progress value, and then add a listener to the current progress value. This allows us to update the state every time the progress value changes. We're using parseInt(), so the progress value is converted to a whole number. After that, we start the animation with a duration of 7,000 milliseconds (7 seconds). Once it's done, we change the progress text to done!

Expand Page

 

The expand page (src/pages/ExpandPage.js) shows how to visually connect transition states using expanding and shrinking motions. It's important to show the user how a specific element came to be. It answers the questions of where the element came from and what its role is in the current context. As always, start by importing the things that we need:

Inside the constructor(), create an animated value that will store the current y-position of the menu. The idea is to have a big box that's enough to contain all the menu items. 

Initially, the box will have a negative value for the bottom position. This means that only the tip of the whole box will be shown by default. Once the user taps on the menu, the whole box will look as if it's expanded, when in reality we're only changing the bottom position so that everything is shown. 

You might be wondering why we use this approach instead of just scaling the box to accommodate all its children. That's because we only need to scale the height attribute. Think what happens to images when you just adjust their height or width alone—they look stretched. The same thing would happen to the elements inside the box. 

Going back to the constructor(), we also add a state flag that indicates whether the menu is currently expanded or not. We need this because we need to hide the button for expanding the menu if the menu is already expanded. 

Inside the render() function, specify how the bottom position will be translated. The inputRange is 0 and 1, and the outputRange is 0 and -300. So if the y_translate has a value of 0, nothing will happen because the outputRange equivalent is 0. But if the value becomes 1, the menu's bottom position is translated to -300 from its original position. 

Take note of the negative sign, because if it's just 300, the box will go down even further. If it's a negative number, the opposite will happen.

For this to make more sense, let's skip to the styles:

Notice the footer_menu style. Its total height is set to 350, and the bottom position is -300, which means that only the top 50 is shown by default. When the translate animation is executed to expand the menu, the bottom position ends up with a value of 0. Why? Because if you still remember the rules when subtracting negative numbers, two minus signs become a positive. So (-300) - (-300) becomes (-300) + 300.

We all know what happens when adding positive and negative numbers: they cancel each other out. So the bottom position ends up becoming 0, and the whole of the menu is displayed.

Going back to the render() function, we have the main content (body) and the footer menu, which is the one that will be expanded and shrunk. The translateY transform is used to translate its position in the Y-axis. Because the whole container has flex: 10 and the body is also flex: 10, the starting point is actually at the very bottom of the screen.

Inside the <Animated.View> are the tip_menu and the full menu. If the menu is expanded, we don't want the tip menu to be shown, so we only render it if menu_expanded is set to false

On the other hand, we only want to display the full menu if menu_expanded is set to true. Each of the buttons will shrink the menu back to its original position.

When opening the menu, the state needs to be updated first so that the hidden menus will be rendered. Only once that's done can the translate animation be executed. This uses Animated.spring as opposed to Animated.timing to add a bit of playfulness to the animation. The higher the value you supply to the friction, the less bounce there will be. Remember not to overdo your animations because instead of helping the user, they can end up being an annoyance.

hideMenu() does the opposite of showMenu(), so we simply reverse what it does:

AttentionSeeker Page

 

Last but not the least is the attentionseeker page (src/pages/AttentionSeekerPage.js). I know that this tutorial is already getting quite long, so to make things shorter, let's use the react-native-animatable package to implement the animations for this page.

Create an array containing the type of animation and the background color to be used for each box:

Create the component:

The render() function uses the renderBoxes() function to create three rows which will render three boxes each. 

The renderBoxes() function renders the animated boxes. This uses the starting index supplied as an argument to extract a specific part of the array and render them individually. 

Here we're using the <Animatable.View> component instead of <Animated.View>. This accepts the animation and iterationCount as props. The animation specifies the type of animation you want to perform, and the iterationCount specifies how many times you want to execute the animation. In this case, we just want to bug the user until they press on the box.

stopAnimation() stops the box from being animated. This uses "refs" to uniquely identify each box so that they can be stopped individually.

Finally, add the styles:

Conclusion

In this tutorial, you've learned how to implement some animations commonly used in mobile apps. Specifically, you've learned how to implement animations that provide visual feedback, show the current system status, visually connect transition states, and grab the user's attention. 

As always, there's still a lot more to learn when it comes to animations. For example, we still haven't touched the following areas:

  • How to perform animations on specific user gestures such as dragging, flicking, pinching, and spreading. For example, when the user uses the spread gesture, you should use a scale animation to show how the element involved becomes bigger.
  • How to animate the transition of multiple elements from one state to another. For example, when showing a list of photos, you may want to perform a stagger animation to delay the showing of all the photos.
  • Intro animation for first-time users of the app. A video could be used as an alternative, but this is also a good place to implement animations.

Maybe I'll cover some of those topics in a future tutorial. In the meantime, check out some of our other courses and tutorials on React Native!


Tags:

Comments

Related Articles