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:
react-native init RNPracticalAnimations
Once the project is created, navigate inside the newly created folder, open the package.json
file, and add the following to dependencies
:
"react-native-animatable": "^0.6.1", "react-native-vector-icons": "^3.0.0"
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:
import React, { Component } from 'react'; import { AppRegistry } from 'react-native'; import NewsPage from './src/pages/NewsPage'; import ButtonsPage from './src/pages/ButtonsPage'; import ProgressPage from './src/pages/ProgressPage'; import ExpandPage from './src/pages/ExpandPage'; import AttentionSeekerPage from './src/pages/AttentionSeekerPage'; class RNPracticalAnimation extends Component { render() { return ( <NewsPage /> ); } } AppRegistry.registerComponent('RNPracticalAnimation', () => RNPracticalAnimation);
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:
import React, { Component } from 'react'; import { StyleSheet, Text, View, Animated, Easing, ScrollView, RefreshControl } from 'react-native'; import NewsItem from '../components/NewsItem';
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:
export default class NewsPage extends Component { ... }
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.
constructor(props) { super(props); this.opacityValue = new Animated.Value(0); this.state = { is_news_refreshing: false, news_items: [ { title: 'CTO Mentor Network – a virtual peer-to-peer network of CTOs', website: 'ctomentor.network', url: 'https://ctomentor.network/' }, { title: 'The No More Ransom Project', website: 'nomoreransom.org', url: 'https://www.nomoreransom.org/' }, { title: 'NASA Scientists Suggest We’ve Been Underestimating Sea Level Rise', website: 'vice.com', url: 'http://motherboard.vice.com/read/nasa-scientists-suggest-weve-been-underestimating-sea-level-rise' }, { title: 'Buttery Smooth Emacs', website: 'facebook.com', url: 'https://www.facebook.com/notes/daniel-colascione/buttery-smooth-emacs/10155313440066102/' }, { title: 'Elementary OS', website: 'taoofmac.com', url: 'http://taoofmac.com/space/blog/2016/10/29/2240' }, { title: 'The Strange Inevitability of Evolution', website: 'nautil.us', url: 'http://nautil.us/issue/41/selection/the-strange-inevitability-of-evolution-rp' }, ] } }
The opacity()
function is the one that will trigger the animation for changing the opacity.
opacity() { this.opacityValue.setValue(0); Animated.timing( this.opacityValue, { toValue: 1, duration: 3500, easing: Easing.linear } ).start(); }
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).
render() { const opacity = this.opacityValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: [1, 0, 1] }); ... }
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.
return ( <View style={styles.container}> <View style={styles.header}> </View> <ScrollView refreshControl={ <RefreshControl colors={['#1e90ff']} refreshing={this.state.is_news_refreshing} onRefresh={this.refreshNews.bind(this)} /> } style={styles.news_container}> ... </ScrollView> </View> );
Inside the <ScrollView>
, use an <Animated.View>
component and set the style
to that of the opacity
value:
<Animated.View style={[{opacity}]}> { this.renderNewsItems() } </Animated.View>
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.
refreshNews() { this.opacity(); this.setState({is_news_refreshing: true}); setTimeout(() => { this.setState({is_news_refreshing: false}); }, 3500); }
renderNewsItems()
takes the array of news items that we declared earlier inside the constructor()
and renders each of them using the <NewsItem>
component.
renderNewsItems() { return this.state.news_items.map((news, index) => { return ( <NewsItem key={index} index={index} news={news} /> ); }); }
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.
import React, { Component } from 'react'; import { StyleSheet, Text, View, } from 'react-native'; import Button from './Button'; const NewsItem = ({ news, index }) => { function onPress(news) { //do anything you want } return ( <Button key={index} noDefaultStyles={true} onPress={onPress.bind(this, news)} > <View style={styles.news_item}> <Text style={styles.title}>{news.title}</Text> <Text>{news.website}</Text> </View> </Button> ); } const styles = StyleSheet.create({ news_item: { flex: 1, flexDirection: 'column', paddingRight: 20, paddingLeft: 20, paddingTop: 30, paddingBottom: 30, borderBottomWidth: 1, borderBottomColor: '#E4E4E4' }, title: { fontSize: 20, fontWeight: 'bold' } }); export default NewsItem;
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.
import React, { Component } from 'react'; import { StyleSheet, Text, TouchableHighlight, } from 'react-native'; const Button = (props) => { function getContent() { if(props.children){ return props.children; } return <Text style={props.styles.label}>{props.label}</Text> } return ( <TouchableHighlight underlayColor="#ccc" onPress={props.onPress} style={[ props.noDefaultStyles ? '' : styles.button, props.styles ? props.styles.button : '']} > { getContent() } </TouchableHighlight> ); } const styles = StyleSheet.create({ button: { alignItems: 'center', justifyContent: 'center', padding: 20, borderWidth: 1, borderColor: '#eee', margin: 20 } }); export default Button;
Going back to the NewsPage
component, add the styling:
const styles = StyleSheet.create({ container: { flex: 1, }, header: { flexDirection: 'row', backgroundColor: '#FFF', padding: 20, justifyContent: 'space-between', borderBottomColor: '#E1E1E1', borderBottomWidth: 1 }, news_container: { flex: 1, } });
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:
import React, { Component } from 'react'; import { StyleSheet, View } from 'react-native'; import Button from '../components/Button'; import ScalingButton from '../components/ScalingButton'; import StatefulButton from '../components/StatefulButton';
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.
import React, { Component } from 'react'; import { StyleSheet, Text, Animated, Easing, TouchableWithoutFeedback } from 'react-native';
Just like the Button
component, this will be a functional type of component since we don't really need to work with the state.
const ScalingButton = (props) => { ... }
Inside the component, create an animated value that will store the current button scale.
var scaleValue = new Animated.Value(0);
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.
function scale() { scaleValue.setValue(0); Animated.timing( scaleValue, { toValue: 1, duration: 300, easing: Easing.easeOutBack } ).start(); }
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.
const buttonScale = scaleValue.interpolate({ inputRange: [0, 0.5, 1], outputRange: [1, 1.1, 1] }); return ( <TouchableWithoutFeedback onPress={onPress}> <Animated.View style={[ props.noDefaultStyles ? styles.default_button : styles.button, props.styles ? props.styles.button : '', { transform: [ {scale: buttonScale} ] } ]} > { getContent() } </Animated.View> </TouchableWithoutFeedback> );
The onPress()
function executes the scale animation first before calling the method passed in by the user through the props.
function onPress() { scale(); props.onPress(); }
The getContent()
function outputs the child components if it's available. If not, a Text
component containing the label
props is rendered.
function getContent() { if(props.children){ return props.children; } return <Text style={props.styles.label}>{ props.label }</Text>; }
Add the styles and export the button:
const styles = StyleSheet.create({ default_button: { alignItems: 'center', justifyContent: 'center' }, button: { alignItems: 'center', justifyContent: 'center', padding: 20, borderWidth: 1, borderColor: '#eee', margin: 20 }, }); export default ScalingButton;
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:
dependencies { //default dependencies here compile 'com.facebook.fresco:animated-gif:0.12.0' }
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.
import React, { Component } from 'react'; import { StyleSheet, View, Image, Text, TouchableWithoutFeedback, Animated } from 'react-native';
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.
export default class StatefulButton extends Component { constructor(props) { super(props); this.colorValue = new Animated.Value(0); this.state = { is_loading: false } } }
Inside the render()
function, specify the different background colors that will be used based on the current value of the animated value.
render() { const colorAnimation = this.colorValue.interpolate({ inputRange: [0, 50, 100], outputRange: ['#2196f3', '#ccc', '#8BC34A'] }); ... }
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.
return ( <TouchableWithoutFeedback onPress={this.onPress.bind(this)}> <Animated.View style={[ styles.button_container, this.props.noDefaultStyles ? '' : styles.button, this.props.styles ? this.props.styles.button : '', { backgroundColor: colorAnimation }, ]}> { this.state.is_loading && <Image style={styles.loader} source={require('../img/ajax-loader.gif')} /> } <Text style={this.props.styles.label}> { this.state.is_loading ? 'loading...' : this.props.label} </Text> </Animated.View> </TouchableWithoutFeedback> );
When the button is pressed, it first executes the function that was passed via the props before doing the animation.
onPress() { this.props.onPress(); this.changeColor(); }
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.
changeColor() { this.setState({ is_loading: true }); this.colorValue.setValue(0); Animated.timing(this.colorValue, { toValue: 100, duration: 3000 }).start(() => { this.setState({ is_loading: false }); }); }
Add the styles:
const styles = StyleSheet.create({ button_container: { flexDirection: 'row', alignItems: 'center', backgroundColor: '#2196f3' }, button: { alignItems: 'center', justifyContent: 'center', padding: 20, borderWidth: 1, borderColor: '#eee', margin: 20 }, loader: { width: 16, height: 16, marginRight: 10 } });
Back on the Buttons page: create the component, render the three kinds of buttons, and add their styles.
export default class ButtonsPage extends Component { press() { //do anything you want } render() { return ( <View style={styles.container}> <Button underlayColor={'#ccc'} label="Ordinary Button" onPress={this.press.bind(this)} styles={{button: styles.ordinary_button, label: styles.button_label}} /> <ScalingButton label="Scaling Button" onPress={this.press.bind(this)} styles={{button: styles.animated_button, label: styles.button_label}} /> <StatefulButton label="Stateful Button" onPress={this.press.bind(this)} styles={{button: styles.stateful_button, label: styles.button_label}} /> </View> ); } } const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'column', padding: 30 }, ordinary_button: { backgroundColor: '#4caf50', }, animated_button: { backgroundColor: '#ff5722' }, button_label: { color: '#fff', fontSize: 20, fontWeight: 'bold' } });
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:
import React, { Component } from 'react'; import { StyleSheet, Text, View, Animated, Dimensions } from 'react-native';
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.
var { width } = Dimensions.get('window'); var available_width = width - 40 - 12;
For the above formula to make sense, let's skip right to the styles:
const styles = StyleSheet.create({ container: { flex: 1, padding: 20, justifyContent: 'center' }, progress_container: { borderWidth: 6, borderColor: '#333', backgroundColor: '#ccc' }, progress_status: { color: '#333', fontSize: 20, fontWeight: 'bold', alignSelf: 'center' } });
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.
export default class ProgressPage extends Component { constructor(props) { super(props); this.progress = new Animated.Value(0); this.state = { progress: 0 }; } }
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.
export default class ProgressPage extends Component { constructor(props) { super(props); this.progress = new Animated.Value(0); this.state = { progress: 0 }; } }
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%).
render() { return ( <View style={styles.container}> <View style={styles.progress_container}> <Animated.View style={[this.getProgressStyles.call(this)]} > </Animated.View> </View> <Text style={styles.progress_status}> { this.state.progress } </Text> </View> ); }
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.
getProgressStyles() { var animated_width = this.progress.interpolate({ inputRange: [0, 50, 100], outputRange: [0, available_width / 2, available_width] }); //red -> orange -> green const color_animation = this.progress.interpolate({ inputRange: [0, 50, 100], outputRange: ['rgb(199, 45, 50)', 'rgb(224, 150, 39)', 'rgb(101, 203, 25)'] }); return { width: animated_width, height: 50, //height of the progress bar backgroundColor: color_animation } }
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!
componentDidMount() { this.progress.setValue(0); this.progress.addListener((progress) => { this.setState({ progress: parseInt(progress.value) + '%' }); }); Animated.timing(this.progress, { duration: 7000, toValue: 100 }).start(() => { this.setState({ progress: '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:
import React, { Component } from 'react'; import { StyleSheet, Text, View, Animated } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import ScalingButton from '../components/ScalingButton';
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.
export default class ExpandPage extends Component { constructor(props) { super(props); this.y_translate = new Animated.Value(0); this.state = { menu_expanded: false }; } ... }
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.
render() { const menu_moveY = this.y_translate.interpolate({ inputRange: [0, 1], outputRange: [0, -300] }); ... }
For this to make more sense, let's skip to the styles:
const styles = StyleSheet.create({ container: { flex: 10, flexDirection: 'column' }, body: { flex: 10, backgroundColor: '#ccc' }, footer_menu: { position: 'absolute', width: 600, height: 350, bottom: -300, backgroundColor: '#1fa67a', alignItems: 'center' }, tip_menu: { flexDirection: 'row' }, button: { backgroundColor: '#fff' }, button_label: { fontSize: 20, fontWeight: 'bold' } });
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.
return ( <View style={styles.container}> <View style={styles.body}></View> <Animated.View style={[ styles.footer_menu, { transform: [ { translateY: menu_moveY } ] } ]} > ... </Animated.View> </View> );
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
.
{ !this.state.menu_expanded && <View style={styles.tip_menu}> <ScalingButton onPress={this.openMenu.bind(this)} noDefaultStyles={true}> <Icon name="ellipsis-h" size={50} color="#fff" /> </ScalingButton> </View> }
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.
{ !this.state.menu_expanded && <View style={styles.tip_menu}> <ScalingButton onPress={this.openMenu.bind(this)} noDefaultStyles={true}> <Icon name="ellipsis-h" size={50} color="#fff" /> </ScalingButton> </View> }
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.
openMenu() { this.setState({ menu_expanded: true }, () => { this.y_translate.setValue(0); Animated.spring( this.y_translate, { toValue: 1, friction: 3 } ).start(); }); }
hideMenu()
does the opposite of showMenu()
, so we simply reverse what it does:
hideMenu() { this.setState({ menu_expanded: false }, () => { this.y_translate.setValue(1); Animated.spring( this.y_translate, { toValue: 0, friction: 4 } ).start(); }); }
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.
import React, { Component } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import * as Animatable from 'react-native-animatable'; import ScalingButton from '../components/ScalingButton';
Create an array containing the type of animation and the background color to be used for each box:
var animations = [ ['bounce', '#62B42C'], ['flash', '#316BA7'], ['jello', '#A0A0A0'], ['pulse', '#FFC600'], ['rotate', '#1A7984'], ['rubberBand', '#435056'], ['shake', '#FF6800'], ['swing', '#B4354F'], ['tada', '#333333'] ];
Create the component:
export default class AttentionSeekerPage extends Component { ... }
The render()
function uses the renderBoxes()
function to create three rows which will render three boxes each.
render() { return ( <View style={styles.container}> <View style={styles.row}> { this.renderBoxes(0) } </View> <View style={styles.row}> { this.renderBoxes(3) } </View> <View style={styles.row}> { this.renderBoxes(6) } </View> </View> ); }
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.
renderBoxes(start) { var selected_animations = animations.slice(start, start + 3); return selected_animations.map((animation, index) => { return ( <ScalingButton key={index} onPress={this.stopAnimation.bind(this, animation[0])} noDefaultStyles={true} > <Animatable.View ref={animation[0]} style={[styles.box, { backgroundColor: animation[1] }]} animation={animation[0]} iterationCount={"infinite"}> <Text style={styles.box_text}>{ animation[0] }</Text> </Animatable.View> </ScalingButton> ); }); }
stopAnimation()
stops the box from being animated. This uses "refs" to uniquely identify each box so that they can be stopped individually.
stopAnimation(animation) { this.refs[animation].stopAnimation(); }
Finally, add the styles:
const styles = StyleSheet.create({ container: { flex: 1, flexDirection: 'column', padding: 20 }, row: { flex: 1, flexDirection: 'row', justifyContent: 'space-between' }, box: { alignItems: 'center', justifyContent: 'center', height: 100, width: 100, backgroundColor: '#ccc' }, box_text: { color: '#FFF' } });
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!
Build a Social App With React Native
Get Started With React Native Layouts
Animate Your React Native App
Creating a Dictionary App Using React Native for Android
Comments