Expanding and Collapsing Elements Using Animations in React Native

   JavaScript

Fluid animations improve the user experience of any application. React Native is focused on performance to build and deliver great products.

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.

ec-01

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.

ec-02

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,StyleSheet,Text,View,Image,TouchableHighlight,Animated} from 'react-native'; //Step 1

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;
  1. We just import all the dependencies that we are using in our Panel class.
  2. Then we load two images. We will use them in the collapsible/expandible button that we will add in the title bar.
  3. We set the initial state of our app. Here we are getting the title and setting the expanded property as true.
  4. Based on the expanded property, we get the correct image to render on the button.
  5. Finally, we render the components. Basically a View with a title and a content. For the body we are using this.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,{AppRegistry,StyleSheet,Text,ScrollView} from 'react-native';
import Panel from './components/Panel';  // Step 1

var Panels = React.createClass({
  render: function() {
    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);
  1. First we need to include our new component.
  2. 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.

ec-03

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. 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()
};

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, 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>
    );
}
  1. First we add the onLayout listener to the title view. The _setMinHeight method will be called when the title gets rendered.
  2. Then we add the onLayout listener to the body view. In this case the _setMaxHeight method will be executed when the body gets rendered.

Now we need to define the callback methods in our Panel class.

_setMaxHeight(event){
    this.setState({
        maxHeight   : event.nativeEvent.layout.height
    });
}

_setMinHeight(event){
    this.setState({
        minHeight   : event.nativeEvent.layout.height
    });
}

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({
        expanded : !this.state.expanded  //Step 2
    });

    this.state.animation.setValue(initialValue);  //Step 3
    Animated.spring(     //Step 4
        this.state.animation,
        {
            toValue: finalValue
        }
    ).start();  //Step 5
}
  1. 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.
  2. We need to toggle the expanded value.
  3. Using the Animated.Value instance, we set the initial value for this animation.
  4. We use the Animated.spring method to run the animation. This method does all the calculations and set the value for each frame to the Animated.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.
  5. 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.

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,{height: this.state.animation}]}>
            
//...

        </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.

ec-04

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. You can download the code for this tutorial from Github. If you have any questions please leave a comment or ping me on Twitter.


Like What You See?

Got any questions?