This page looks best with JavaScript enabled

React Native: Fluid card drag and drop animation

 ·  ☕ 6 min read  ·  ✍️ Iskander Samatov

Recently I’ve been working on a simple React Native todo list app. For adding tasks I wrote a drag and drop animation inspired by Asana’s UI. In this post I’m going to share how I built it.

Here’s what the finished animation looks like:

react native drag drop
end result

So without further ado let’s get to it!

Setup project:

The first thing you need to do is setup the project. I suggest you use React Native CLI to do so. More on that here.

Since the functionality is pretty simple we’re only going to have one App component.

Here’s what the initial component looks like:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import { View, ScrollView, Text, Animated, PanResponder } from 'react-native';
class App extends Component {

    constructor(props) {
        super(props);

        this.state = {
            cards: [],
        }
    }


    render() {
        return (
                        {this.state.cards}
        )
    }
}


const styles = {
    cardContainer: {
        minHeight: 1000,
        flex: 1,
        backgroundColor: '#f3f3f3'
    },
    panel: {
        position: 'absolute',
        backgroundColor: '#bdbdbd',
        flex: 1,
        maxHeight: 200,
        height: 200,
        bottom: 0,
        left: 0,
        right: 0,
        justifyContent: 'center'
    },
    panelCard: {
        backgroundColor: '#fff',
        elevation: 2,
        height: 150,
        maxHeight: 150,
        borderRadius: 10,
        marginLeft: 10,
        marginRight: 10,
        justifyContent: 'center',
        alignItems: 'center'
    },
    dropCard: {
        backgroundColor: '#fff',
        height: 150,
        maxHeight: 150,
        elevation: 2,
        borderRadius: 10,
        position: 'absolute',
        left: 10,
        right: 10,
        alignItems: 'center',
        justifyContent: 'center'
    }
}

export default App;

Setup animation properties

Now let’s define class properties that we will need for this animation in the constructor:

1
2
3
this.panelY = 0;
this.scrollY = 0;
this.mainPosition = new Animated.ValueXY();
  • panelY – stores the initial position of the card. We use it to calculate the position of the card when user releases the touch.
  • scrollY – stores current y coordinates of the ScrollView
  • mainPosition – Animated.ValueXY used to perform animation when the card is dragged

Now let’s figure out a way to calculate the values for panelY and scollY. We will use component listeners to do so:

1
2
3
4
5
6
7
8
handleScroll = (event) => {
    this.scrollY = event.nativeEvent.contentOffset.y
}

handleOnLayout = (event) => {
    let { y } = event.nativeEvent.layout
    this.panelY = y + 25
}
  • handleOnLayout is called when the bottom panel is rendered for the first time
  • handleScroll is called whenever user scrolls the ScrollView

Setup PanResponder

Now comes the fun part. We need to setup PanResponder to react to user’s gestures and activate the drag animation. Add the following code below the constructor properties we defined earlier:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
this.mainPanResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderMove: Animated.event([
        null, { dx: this.mainPosition.x, dy: this.mainPosition.y }
    ]),
    onPanResponderGrant: (event, gesture) => {
        this.mainPosition.setOffset({
            x: this.mainPosition.x._value,
            y: this.mainPosition.y._value
        });
        this.mainPosition.setValue({ x: 0, y: 0 });
    },
    onPanResponderRelease: (e, gesture) => {
        this.mainPosition.flattenOffset();
        if (gesture.dy < -150) {
            const y = gesture.dy + this.panelY + this.scrollY;
            this.addCard(y)
        }
        this.mainPosition.setValue({ x: 0, y: 0 });
    }
});

This PanResponder will move the card along the user’s touch, then once the user releases the card it will call this.addCard(y). this.addCard(y) will render a new card, assign touch’s Y coordinates and add it to the list of the cards.

Some of you who are familiar with animation in React Native might wonder why didn’t I just use data provided by gesture argument, like gesture.moveY. The reason is because I found gesture.moveY to be inconsistent and error prone, at least for Android. Using gesture.dy + this.panelY + this.scrollY; to calculate Y coordinates gives much better results.

Now let’s take a look at the final piece of the puzzle – this.addCard() method. This method takes Y coordinates of new card, renders it and adds it to the list. Pretty simple:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
addCard = (y) => {
    const { cards } = this.state;
    const newStack = [...cards];
    const style = { ...styles.dropCard };
    style.top = y;

    const card = (
        <view key="{y}" style="{style}">
            <text>Sample</text>
        </view>
    )

    newStack.push(card);
    this.setState({ cards: newStack })
}

Finished product

Below is the full App component:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
import React, { Component } from 'react';
import { View, ScrollView, Text, Animated, PanResponder } from 'react-native';



class App extends Component {

    constructor(props) {
        super(props);

        this.state = {
            cards: [],
        }
        this.panelY = 0;
        this.scrollY = 0;
        this.mainPosition = new Animated.ValueXY();
        this.mainPanResponder = PanResponder.create({
            onStartShouldSetPanResponder: () => true,
            onMoveShouldSetPanResponder: () => true,
            onPanResponderMove: Animated.event([
                null, { dx: this.mainPosition.x, dy: this.mainPosition.y }
            ]),
            onPanResponderGrant: (event, gesture) => {
                this.mainPosition.setOffset({
                    x: this.mainPosition.x._value,
                    y: this.mainPosition.y._value
                });
                this.mainPosition.setValue({ x: 0, y: 0 });
            },
            onPanResponderRelease: (e, gesture) => {
                this.mainPosition.flattenOffset();
                if (gesture.dy < -150) {
                    const y = gesture.dy + this.panelY + this.scrollY;
                    this.addCard(y)
                }
                this.mainPosition.setValue({ x: 0, y: 0 });
            }
        });
    }


    addCard = (y) => {
        const { cards } = this.state;
        const newStack = [...cards];
        const style = { ...styles.dropCard };
        style.top = y;

        const card = (
                Sample
        )

        newStack.push(card);
        this.setState({ cards: newStack })
    }

    handleScroll = (event) => {
       this.scrollY = event.nativeEvent.contentOffset.y
    }

    handleOnLayout = (event) => {
        let { y } = event.nativeEvent.layout
        this.panelY = y + 25
    }

    render() {
        return (                
            {this.state.cards}
        )
    }
}


const styles = {
    cardContainer: {
        minHeight: 1000,
        flex: 1,
        backgroundColor: '#f3f3f3'
    },
    panel: {
        position: 'absolute',
        backgroundColor: '#bdbdbd',
        flex: 1,
        maxHeight: 200,
        height: 200,
        bottom: 0,
        left: 0,
        right: 0,
        justifyContent: 'center'
    },
    panelCard: {
        backgroundColor: '#fff',
        elevation: 2,
        height: 150,
        maxHeight: 150,
        borderRadius: 10,
        marginLeft: 10,
        marginRight: 10,
        justifyContent: 'center',
        alignItems: 'center'
    },
    dropCard: {
        backgroundColor: '#fff',
        height: 150,
        maxHeight: 150,
        elevation: 2,
        borderRadius: 10,
        position: 'absolute',
        left: 10,
        right: 10,
        alignItems: 'center',
        justifyContent: 'center'
    }
}

export default App;

And that’s it for this post! This example is simple but you can build on top of it to make your card animation more sophisticated and feature rich.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.