Animated Drag and Drop with React Native

   JavaScript

With the ability to start work and build apps quickly, it’s easy to see why React Native is so awesome! However, when you want to add nice animations and effects it can be tricky, especially because the documentation for animations is limited, and it might be difficult to grasp the concepts.

Recently, we explored the React Native layout system using flex and we’ve discussed the different options and properties to create all kinds of layouts. Once we have the perfect layout, it is time to add some interaction.

In this tutorial, we will learn how to animate an element, drag it around, and drop it. The idea here is to understand how animations in React Native works. Let’s start by creating a new project in your terminal. Type the following command:

$ react-native init DragAndDrop

Once we have our new project created, let’s open the ios/DragAndDrop.xcode file in xCode to build and run the project in the iOS simulator.

Creating the views

Before we start coding, let’s create a folder called app, where we will create all our components and classes for this example. Inside this app folder create a new file called Viewport.js. This is where we will write all our code. This way, we can share this component for both iOS and Android apps.

Open the index.ios.js file in your favorite editor, remove the original code, and add the following.

import React from 'react-native';
import Viewport from './app/Viewport';

React.AppRegistry.registerComponent('DragAndDrop', () => Viewport);

This will import React Native and our Viewport, and then register the app. If we want to create an Android app, we can do exactly the same for the index.android.js file.

Now we can create the UI for this example. We are going to create a simple container at the top of the screen and a circle at the middle. We will allow the user to drag the circle and drop it on the top area. If the user drags the circle somewhere else, then we will animate the circle back to the middle of the screen.

Let’s start coding the Viewport component. For now, we are just going to create the elements and the required styles. Add the following code inside the Viewport.js.

import React,{
    Component,
    StyleSheet,
    View,
    Text,
    PanResponder,
    Animated,
    Dimensions
} from 'react-native';

First, we have to import all the components we are going to need. If you have been following my previous posts about the layout system, you should be familiar with the Component, StyleSheet, View and Text components.

    • ● The

PanResponder

    is necessary to help us with the dragging. We are going to need this component to allow the middle circle to move around.
    • ● The

Animated

    class is the one responsible for the animations. We will use it to animate the circle back to the original position.

Dimensions

    is a utility class and we will use it to get the size of the device. Based on that, we will calculate the center of the screen to set the top and left of the circle.

Next, we will create the Viewport component to display something on the screen.

export default class Viewport extends Component{
    render(){
        return (
            <View style={styles.mainContainer}>
                <View style={styles.dropZone}>
                    <Text style={styles.text}>Drop me here!</Text>
                </View>

                {this.renderDraggable()}
            </View>
        );
    }

    renderDraggable(){
        return (
            <View style={styles.draggableContainer}>
                <Animated.View style={styles.circle}>
                    <Text style={styles.text}>Drag me!</Text>
                </Animated.View>
            </View>
        );
    }
}

Inside the render method we will create the drop zone which is just a view with some text inside. We will also create the circle that we will drag around.

Notice that we are using the Animated.View to display the circle. If we want to animate an element, we need to use the Animated.* class.

React Native give us three animatable components: Animated.View, Animated.Image and Animated.Text. These classes add the needed support to run fluid animations on these three components.

Finally, we need to add the styles to the views — colors, margins, positions, etc.

let CIRCLE_RADIUS = 36;
let Window = Dimensions.get('window');
let styles = StyleSheet.create({
    mainContainer: {
        flex    : 1
    },
    dropZone    : {
        height         : 100,
        backgroundColor:'#2c3e50'
    },
    text        : {
        marginTop   : 25,
        marginLeft  : 5,
        marginRight : 5,
        textAlign   : 'center',
        color       : '#fff'
    },
    draggableContainer: {
        position    : 'absolute',
        top         : Window.height/2 - CIRCLE_RADIUS,
        left        : Window.width/2 - CIRCLE_RADIUS,
    },
    circle      : {
        backgroundColor     : '#1abc9c',
        width               : CIRCLE_RADIUS*2,
        height              : CIRCLE_RADIUS*2,
        borderRadius        : CIRCLE_RADIUS
    }
});

Nothing complicated here, simply adding styles and setting the center of the circle using the size of the device. We also defined a CIRCLE_RADIUS variable to set the size of the circle.

If we run our code in the simulators, we should see something like this.

dd-01
I’m using Genymotion to run the Android emulator as it works great for testing Android apps. As you can see, the same code works great on both devices.

Dragging elements

Now that we have the main elements on screen, let’s add the drag functionality to the green circle. For that, we need to use the PanResponder class.

constructor(props){
    super(props);

    this.state = {
        pan     : new Animated.ValueXY()   //Step 1
    };

    this.panResponder = PanResponder.create({    //Step 2
        onStartShouldSetPanResponder : () => true,
        onPanResponderMove           : Animated.event([null,{ //Step 3
            dx : this.state.pan.x,
            dy : this.state.pan.y
        }]),
        onPanResponderRelease        : (e, gesture) => {} //Step 4
    });
}
    • ● In step one, create an instance of

Animated.ValueXY

    . This component will take care of interpolating X and Y values. We will run the animations by setting these values to the style of the element to animate.
    • ● In step two, create the

PanResponder

    , which is responsible for doing the dragging. We are setting the handlers when the user moves and releases the element.
    ● In step three, the handler will trigger when the element is moving. We need to set the animated values to perform the dragging correctly.
    ● In step four, write the code to execute when the element is released. For now it is empty, but soon we will animate the circle back to the center.

Once we have the configurations ready, we need to set these handlers and the animation values to the element that we want to animate.

renderDraggable(){
    return (
        <View style={styles.draggableContainer}>
            <Animated.View 
                {...this.panResponder.panHandlers}                       //Step 1
                style={[this.state.pan.getLayout(), styles.circle]}>     //Step 2
                <Text style={styles.text}>Drag me!</Text>
            </Animated.View>
        </View>
    );
}
    • ● In the first step, we assign the handlers of the

PanResponder

    • to the

Animated.View

    . We are using the spread syntax to avoid assigning one by one.
    ● In the second step, we set the styles to animate from the animation. The getLayout method returns the left and top properties with the correct values for each frame during the animation.

We are ready to test our code, save the changes, and run the simulator. You should be able to move the circle around the screen.

dd-02
That’s nice! Now we need to the circle to return to the origin when the user releases it. To do this, we need to write that code inside of the PanResponder callback on the constructor.

this.panResponder = PanResponder.create({
    
//...

    onPanResponderRelease           : (e, gesture) => {
        Animated.spring(            //Step 1
            this.state.pan,         //Step 2
            {toValue:{x:0,y:0}}     //Step 3
        ).start();
    }
});
    • ● The first step is to use the

Animated.spring

    • method to run the animation. This method will run the animation at a constant speed and we can control the

friction

    • and

tension

    .
    ● The first parameter accepts the animation values.
    • ● The second parameter is a configuration object. Here, we are defining only the

toValue

    , which is the origin coordinates. This will return the circle to the middle.

If you refresh the simulator, you should see something similar to the following image.

dd-03
Along with the Animated.spring method, we also have Animated.decay and Animated.timing. The decay method runs an animation which starts fast and gradually decrease until it stops. The timing method runs the animation based on the time.

Dropping the element

We’re almost done! All we need now is to set the drop area. When the user drops the green circle inside of the drop zone, we will remove the element from the view.

First, we are going to define a state property to know if we will render the green circle or not. Let’s add the following code to our Viewport class.

constructor(props){
    super(props);

    this.state = {
        showDraggable   : true,     //Step 1
        dropZoneValues  : null,
        pan             : new Animated.ValueXY()
    };

  //...
}

renderDraggable(){
    if(this.state.showDraggable){      //Step 2
        return (
            <View style={styles.draggableContainer}>
                    //...
            </View>
        );
    }
}

We first define two new properties on the state object. showDraggable will hide or show the green circle. By default this is true, which mean it’s going to be visible. We will change the value to false if the user drops the circle inside the drop zone.
In the second step, we render the green circle based on the showDraggable value.

Next, we need to set the value of the dropZoneValues property dynamically and we need to know the width and height (or x and y) from the drop zone container . These values will change based on the device.

setDropZoneValues(event){      //Step 1
    this.setState({
        dropZoneValues : event.nativeEvent.layout
    });
}

render(){
    return (
        <View style={styles.mainContainer}>
            <View 
                onLayout={this.setDropZoneValues.bind(this)}     //Step 2
                style={styles.dropZone}>
                <Text style={styles.text}>Drop me here!</Text>
            </View>

            {this.renderDraggable()}
        </View>
    );
}
    ● First, we first define a callback where we set the values for the property.
    • ● Second, we set the callback to the

onLayout

    event. This event will be fired when the view gets rendered on the screen and contains all the sizing based on the assigned styles.

The last step is to check if the green circle coordinates match the coordinates of the drop zone. For that, we need to modify the release callback from the PanResponder as follows.

constructor(props){
    //...

    this.panResponder = PanResponder.create({
        //...
        onPanResponderRelease           : (e, gesture) => {
            if(this.isDropZone(gesture)){ //Step 1
                this.setState({
                    showDraggable : false //Step 3
                });
            }else{
                Animated.spring(
                    this.state.pan,
                    {toValue:{x:0,y:0}}
                ).start();
            }
        }
    });
}

isDropZone(gesture){     //Step 2
    var dz = this.state.dropZoneValues;
    return gesture.moveY > dz.y && gesture.moveY < dz.y + dz.height;
}
    ● In the first step, we check if the released element is inside the drop zone. For that, we have defined a function. If the element was released on the drop zone, we need to hide the element, otherwise run the animation.
    ● In the second step, we do the actual checking. We are just comparing the Y coordinates.
    • ● In the third step, we hide the green circle by setting the

showDraggable

    property to false.

dd-04
As you can see, our example is working perfectly! We can also create another animation to hide the green circle — maybe a fade out by animating the opacity property.

Conclusion

Animation support in React is great. We have control of every single style property, giving us the opportunity to create very customizable animations and effects. Unfortunately, the React documentation for animations is very limited, but in this tutorial I’ve tried to explain the required steps to use the animation API.

All the code written in this tutorial will work on iOS and Android devices. What I personally like about React Native is the performance. Animations run very smoothly on the actual device, allowing us to deliver awesome experiences to users.

If you have any questions, please leave a comment or catch me on Twitter. You can also find the code for this tutorial on Github. Happy Coding!


Like What You See?

Got any questions?