Build Draggable Elements Using the PanResponder API
Published in · 4 min read · Nov 15, 2017
--
While brainstorming my next React Native app, here’s my first, I wanted to see if there’s an API that tracks gesture so I could create a drag and drop component.
Luckily, it wasn’t too hard to find: we have the PanResponder! With this we can track gesture as an abstracted state and update our UI accordingly.
What we will be build
Creating a Draggable Component
I found the documentation for PanResponder
API is a bit hard to follow, but this example and animation guide helped get me going
After importing PanResponder
class, you initiate it with .create
method in componentWillMount()
life cycle method. You could also write it in the constructor()
.
import React, { Component } from "react";
import {
StyleSheet,
View,
PanResponder,
Animated
} from "react-native";export default class Draggable extends Component {
constructor() {
super(); this.state = {
pan: new Animated.ValueXY()
};
}componentWillMount() {
// Add a listener for the delta value change
this._val = { x:0, y:0 }
this.state.pan.addListener((value) => this._val = value); // Initialize PanResponder with move handling
this.panResponder = PanResponder.create({
onStartShouldSetPanResponder: (e, gesture) => true,
onPanResponderMove: Animated.event([
null, { dx: this.state.pan.x, dy: this.state.pan.y }
])
// adjusting delta value
this.state.pan.setValue({ x:0, y:0})
});
}render() {
const panStyle = {
transform: this.state.pan.getTranslateTransform()
}
return (
<Animated.View
{...this.panResponder.panHandlers}
style={[panStyle, styles.circle]}
/>
);
}
}let CIRCLE_RADIUS = 30;
let styles = StyleSheet.create({
circle: {
backgroundColor: "skyblue",
width: CIRCLE_RADIUS * 2,
height: CIRCLE_RADIUS * 2,
borderRadius: CIRCLE_RADIUS
}
});
this.panResponder = PanResponder.create()
initiates the panResponder and creates a reference. We utilize it in our <Animate.View>
component by passing{…this.panResponder.panHandlers}
like we’re passing a group of props.
Inside thePanResponder.create()
we set onStartShouldSetPanResponder
to true so the panResponder will respond to touch feedback. Then we pass an Animated.event
to onPanResponderMove:
to update the location of our Animated.View
circle component that the user is gesturing with.
To get our circle’s position we get the calculated animated value from this.state.pan.getTranslateTransform()
and use it to create a transform style that we pass to our Animated.View
.
Finally, we adjust delta value so the element won’t jump on the second touch.
At this point we have a draggable circle that the user can interact with:
Returning the Circle to its Initial Location
We want the circle to return to its original location when they release the component. To do this we use onPanResponderRelease
to tell the UI how to respond when the user lets go.
componentWillMount() {
...
this.panResponder = PanResponder.create({
...
onPanResponderRelease: (e, gesture) => {
Animated.spring(this.state.pan, {
toValue: { x: 0, y: 0 },
friction: 5
}).start();
}
});
Our circle now returns to its initial location:
Creating the Drop Area
Now that we have the drag, we need the drop. We create another component with our drop area and use our Draggable
component within it:
import React, { Component } from "react";
import { StyleSheet, View, Text } from "react-native";
import Draggable from "./Draggable";export default class Screen extends Component {
render() {
return (
<View style={styles.mainContainer}>
<View style={styles.dropZone}>
<Text style={styles.text}>Drop them here!</Text>
</View>
<View style={styles.ballContainer} />
<View style={styles.row}>
<Draggable />
<Draggable />
<Draggable />
<Draggable />
<Draggable />
</View>
</View>
);
}
}
const styles = StyleSheet.create({
mainContainer: {
flex: 1
},
ballContainer: {
height:200
},
row: {
flexDirection: "row"
},
dropZone: {
height: 200,
backgroundColor: "#00334d"
},
text: {
marginTop: 25,
marginLeft: 5,
marginRight: 5,
textAlign: "center",
color: "#fff",
fontSize: 25,
fontWeight: "bold"
}
});
Then we add a bit of logic to Draggable
, its onPanResponderRelease
handler, and add a function to determine if we’re in the drop area. Note: We make assumptions about the drop zone’s location on the screen here.
...
constructor()
super.props();
this.state = {
showDraggable: true,
dropAreaValues: null,
pan: new Animated.ValueXY(),
opacity: new Animated.Value(1)
};
} componentWillMount() {
...
this.panResponder = PanResponder.create({
...
onPanResponderRelease: (e, gesture) => {
if (this.isDropArea(gesture)) {
Animated.timing(this.state.opacity, {
toValue: 0,
duration: 1000
}).start(() =>
this.setState({
showDraggable: false
})
);
} else {
Animated.spring(this.state.pan, {
toValue: { x: 0, y: 0 },
friction: 5
}).start();
}
} isDropArea(gesture) {
return gesture.moveY < 200;
}
And that’s it. Our Screen
should now look like this:
To see the source code and play with the live demo check out the Expo Snack I created for this demo.
Conclusion
The PanResponder
API was a bit intimidating for me at first, but now I think this is a great API that anyone can use to make the user experience in their apps better.
I hope this article has helped you out, reach out in the comments with questions or concerns. Thanks for reading, and happy coding!
*Korean version of this article hasn’t written yet.