Fluid animations improve the user experience of any application. React Native is focused on performance to build and deliver great products.
NEW RESEARCH: LEARN HOW DECISION-MAKERS ARE PRIORITIZING DIGITAL INITIATIVES IN 2024.
In this tutorial we will learn the basics of animations. We are going to create a panel component. When the body of the component expands or collapses, we will add a nice animation as shown in the next image.
Letโs start by creating our project. If this is your first time with React Native please follow the Getting Started guide on the official website, then run the following command on your terminal.
$ react-native init Panels
Once the previous command returns, we can open ios/Panels.xcodeproj
in XCode. Now we can run the application on the iOS simulator. We should see something like in the following image.
We have our app running correctly, now we are ready to start coding!
Creating the panel component
Letโs start by creating the panel component. For now we will only have the title and the content. Create a components
folder on the root of the project, we will have all our JavaScript classes in here. Now, letโs create a Panel.js
file and add the following code.
import React,{Component} from 'react'; //Step 1 import {StyleSheet,Text,View,Image,TouchableHighlight,Animated} from 'react-native'; class Panel extends Component{ constructor(props){ super(props); this.icons = { //Step 2 'up' : require('./images/Arrowhead-01-128.png'), 'down' : require('./images/Arrowhead-Down-01-128.png') }; this.state = { //Step 3 title : props.title, expanded : true }; } toggle(){ } render(){ let icon = this.icons['down']; if(this.state.expanded){ icon = this.icons['up']; //Step 4 } //Step 5 return ( <View style={styles.container} > <View style={styles.titleContainer}> <Text style={styles.title}>{this.state.title}</Text> <TouchableHighlight style={styles.button} onPress={this.toggle.bind(this)} underlayColor="#f1f1f1"> <Image style={styles.buttonImage} source={icon} ></Image> </TouchableHighlight> </View> <View style={styles.body}> {this.props.children} </View> </View> ); } } export default Panel;
- We just import all the dependencies that we are using in our Panel class.
- Then we load two images. We will use them in the collapsible/expandible button that we will add in the title bar.
- We set the initial state of our app. Here we are getting the
title
and setting theexpanded
property astrue
. - Based on the
expanded
property, we get the correct image to render on the button. - Finally, we render the components. Basically a
View
with a title and a content. For the body we are usingthis.props.children
, this means that we will render any component on the body.
Now we need to add some styles, something really basic to keep things simple.
var styles = StyleSheet.create({ container : { backgroundColor: '#fff', margin:10, overflow:'hidden' }, titleContainer : { flexDirection: 'row' }, title : { flex : 1, padding : 10, color :'#2a2f43', fontWeight:'bold' }, button : { }, buttonImage : { width : 30, height : 25 }, body : { padding : 10, paddingTop : 0 } });
We are just adding some colors, dimensions and paddings. The code is self explanatory because the properties are very similar to CSS.
Now that we have our Panel
component ready, letโs create some instances in our app. Open the index.ios.js
file and add the following code.
import React, {Component} from 'react'; import {AppRegistry,StyleSheet,Text,ScrollView} from 'react-native'; import Panel from './components/Panel'; // Step 1 class Panels extends Component { render() { return ( //Step 2 <ScrollView style={styles.container}> <Panel title="A Panel with short content text"> <Text>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</Text> </Panel> <Panel title="A Panel with long content text"> <Text>Lorem ipsum...</Text> </Panel> <Panel title="Another Panel"> <Text>Lorem ipsum dolor sit amet...</Text> </Panel> </ScrollView> ); } }; var styles = StyleSheet.create({ container: { flex : 1, backgroundColor : '#f4f7f9', paddingTop : 30 }, }); AppRegistry.registerComponent('Panels', () => Panels);
- First we need to include our new component.
- Next we render our component three times with a title and some content. The content could be anything, but we will use text for the purposes of this demonstration.
Remember to restart the packager in order to get the images working, otherwise you will get an error when loading the images. Once the packager is restarted, we should see something as in the following image.
Running the animations
Now that we have our component ready we can add the animations. When the user taps on the expand/collapse button, we are going to change the height
of the main container with a nice animation.
The first thing we need to do is to create an instance of the Animated.Value
classย and pass the initial value 0. This class is responsible for holding the value of each frame during the animation. In our case, it will hold the value of the height
. Letโs add the following code in the constructor of the Panel
class.
this.state = { title : props.title, expanded : true, animation : new Animated.Value(0), minHeight : 0, maxHeight : 0, maxValueSet : false, minValueSet : false, cardHeight : 'auto' };
Now we have the animation
property in the component state. We can use the same instance in several animations, for example to animate the opacity, the width, scale, almost any style property. However, this instance can only be used by one animation at the time.
Now we need to get the maximum and minimum height of our panel on the first load of application. We need to dynamically calculate this, because as mentioned before, there could be anything in the content. To get those dimensions, we can use the onLayout event of the View component. This event will be fired after the component is rendered and all sizes are calculated based on the styles and the content.
Letโs add those listeners inside of the render method.
render(){ //... return ( <View style={styles.container}> <View style={styles.titleContainer} onLayout={this._setMinHeight.bind(this)}> //Step 1 //... </View> <View style={styles.body} onLayout={this._setMaxHeight.bind(this)}> //Step 2 {this.props.children} </View> </View> ); }
- First we add the
onLayout
listener to the title view. The_setMinHeight
method will be called when the title gets rendered.ย To execute this method only once, we are checking the maxValueSet variable value to be false, after it runs we set its value to be true. So this method shouldnโt run again. Along with minHeight we also set the initial value of animation. - Then we add the
onLayout
listener to the body view. In this case the_setMaxHeight
method will be executed when the body gets rendered. To execute this method only once, we are checking the minValueSet variable value and this should be false, after it runs we set its value to be true. So this method shouldnโt run again.
Now we need to define the callback methods in our Panel
class.
_setMaxHeight(event){ if(!this.state.maxValueSet) { this.setState({ maxHeight : event.nativeEvent.layout.height, maxValueSet : true }); } } _setMinHeight(event){ if(!this.state.minValueSet) { this.state.animation.setValue(event.nativeEvent.layout.height); this.setState({ minHeight : event.nativeEvent.layout.height, minValueSet : true }); } }
All we are doing here is getting the height and creating a new state property. We are going to use these values inside the toggle
method. Basically to set the limits of the animation.
Now that we have the limit values, we can calculate the animation values. Letโs add the following code inside the toggle
method.
toggle(){ //Step 1 let initialValue = this.state.expanded? this.state.maxHeight + this.state.minHeight : this.state.minHeight, finalValue = this.state.expanded? this.state.minHeight : this.state.maxHeight + this.state.minHeight; this.setState((prevState) => { expanded : !prevState.expanded //Step 2 }); this.state.animation.setValue(initialValue); //Step 3 (Remove this line) Animated.spring( //Step 4 this.state.animation, { toValue: finalValue, useNativeDriver: true } ).start(); //Step 5 }
- We set the initial and final value, in here we are using the limits from the previous steps. If the component is expanded we set the
height
to the minimal value, otherwise to the maximum value. - We need to toggle the
expanded
value. - Set useNativeDriver: true. So, JS Native drive runs animation into a separate UI thread in place of JS thread, and JS thread is available to run complex programmatic logic
- Using the
Animated.Value
instance, we set the initial value for this animation. - We use the
Animated.spring
method to run the animation. This method does all the calculations and set the value for each frame to theAnimated.Value
instance we declared in the constructor. We are also setting the end value of the animation, by using an object as a second parameter. - We call the
start
method to run all the calculations.
If we go and try our code in the simulator, we will see that only the image is changing. But nothing else is happening.
Attach listener on height change event and assign its value to cardHeight. Also unregister that listener on unload components.
componentDidMount() { this.animationId = this.state.animation.addListener(({value}) => { this.setState({ cardHeight: value }); }); } componentWillUnmount() { this.state.animation.removeListener(this.animationId); }
The last piece of the puzzle, is to actually set the Animated.Value
instance to the component that we want to animate. Right now we have all the calculations and everything, but we havenโt assigned those values to the height
of the component that we want to animate.
Letโs modify the render method as follows:
render(){ //... return ( <Animated.View style={[styles.container,<span style="font-weight: 400;">{</span><span style="font-weight: 400;">height</span><span style="font-weight: 400;">:</span> <span style="font-weight: 400;">this</span><span style="font-weight: 400;">.</span><span style="font-weight: 400;">state</span><span style="font-weight: 400;">.</span><span style="font-weight: 400;">cardHeight</span><span style="font-weight: 400;">}]}</span>> //... </Animated.View> ); }
The first thing we need to do is to use an Animated.View
instead of a simple View
as the main container. Now we need to select the style property that we want to animate, in this case we want to animate the height
, but we can also animate the opacity
, or the width
, or any other style property.
The height
property receives the Animated.Value
instance, this is the same instance we are using in the Animated.spring
method. This will set all the calculated values to the height
property, and we will be able to see a nice animation when collapsing and expanding the body.
Conclusion
There are only three concepts that we need to know when dealing with animations. First we need the Animated.Value
to hold the value for each frame during the animation. Second, we need to calculate the values for each animation using the Animated.spring
method (There are 2 other methods that I might cover on future tutorials). Third, we need to use an Animated.View
component instead of a regular View
and assign the calculated values to the style property that we want to animate.
Small simple animations will improve the user experience of your application. We can create many other complex animations, but the concepts are the same.ย If you have any questions please leave a comment or ping me on Twitter.
This blog was updated on May 2, 2022 by Akhilesh Jain, Full-Stack Engineer at Modus Create.ย
Crysfel Villa
Related Posts
-
React Navigation and Redux in React Native Applications
In React Native, the question of โhow am I going to navigate from one screen…
-
Using ES2016 Decorators in React Native
*picture courtesy of pixabayDecorators are a popular bit of functionality currently in Stage 1 of…