Inside React Native you can use the PanResponder to recognise multi-touch gestures as well as swipes and other touches that make native apps feel snappy and intuitive. But getting it up and running can feel daunting and borderline black magic. In this post I'll try and guide you through the process, hopefully demystifying it a bit and get you on track to awesomeness.
Obviously we'll be wanting to focus on the PanResponder itself so UI wise this will be pretty barebones. We'll have an image on screen we can drag. When we release it it will bounce back to its original position. As a bonus, while we press down on the image it will scale up.
I'll be assuming you're somewhat familiar with setting up a fresh React Native project. If not, the guys at Facebook have done an excellent job explaining the steps right here.
Let's start with a new project. I'll call it panresponder-demo for simplicity sake and lack of a name that rhymes with unicorns.
$ react-native init panresponder_demo
First up, let's add an image to the project that will act as your drag and drop target.
Create a directory assets
to the panresponder_demo folder and insert the image you want to use in there. If you don't have one, you can use this one.
Let's get our image on the screen so we can continue on to the cool part.
Open up index.ios.js
and add the Image
component at the top:
import React, {
AppRegistry,
Component,
StyleSheet,
Text,
View,
Image, // we want to use an image
} from "react-native";
Now replace the default app content with our Image so alter the render()
method
render() {
return (
<View style={styles.container}>
<Image source={require('./assets/panresponder.png')} />
</View>
);
}
When you run the app now you should see the image in the center of the screen, waiting for you to do something more exciting. So let's get to it.
Let's get to the more interesting part. Adding the PanResponder system.
At the top, import PanResponder
so we can use it. While we're at it, we'll also add Animated
which allows us to use Animated values, which will come in handy for our animation and calculations.
import React, {
AppRegistry,
Component,
StyleSheet,
Text,
View,
Image, // we want to use an image
PanResponder, // we want to bring in the PanResponder system
Animated, // we wil be using animated value
} from "react-native";
PanResponder basically consists of a couple of event-driven methods that you can implement. Once you've defined what you want it to behave like you attach it to a view, which will then propagate all the events (gestures) to the methods you hooked up.
To illustrate it in a simple way, let's implement the componentWillMount()
method and set up a basic PanResponder instance:
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
onPanResponderGrant: (e, gestureState) => {
},
onPanResponderMove: Animated.event([
]),
onPanResponderRelease: (e, {vx, vy}) => {
}
});
}
render() {
return (
<View style={styles.container}>
<Animated.View {...this._panResponder.panHandlers}>
<Image source={require('./assets/panresponder.png')} />
</Animated.View>
</View>
);
}
Whoa, lots going on here. Let's break it down.
onMoveShouldSetResponderCapture
tells the OS we want to allow movement of the view we'll attach this panresponder to. onMoveShouldSetPanResponderCapture
does the same, but for dragging, which we want to be able to do.
Next up we got 3 methods that will be called onPanResponderGrant
gets invoked when we got access to the movement of the element. This is a perfect spot to set some initial values.
onPanResponderMove
gets invoked when we move the element, which we can use to calculate the next value for the object
onPanResponderRelease
is invoked when we release the view. In a minute we'll use this to make the image animated back to its original position
Last up, we add the panresponder to an Animated.View
which we use to wrap the Image
component in so it will obey our panresponding demands.
Let's implement the first 2 methods to be able to drag the image around the screen.
In order to keep track of where the image is on the screen we'll want to keep a record of its position somewhere. This is the perfect job for a components state
, so let's add this:
constructor(props) {
super(props);
this.state = {
pan: new Animated.ValueXY()
};
}
Next, let's update the panHandler
implementation:
componentWillMount() {
this._panResponder = PanResponder.create({
onMoveShouldSetResponderCapture: () => true,
onMoveShouldSetPanResponderCapture: () => true,
// Initially, set the value of x and y to 0 (the center of the screen)
onPanResponderGrant: (e, gestureState) => {
this.state.pan.setValue({x: 0, y: 0});
},
// When we drag/pan the object, set the delate to the states pan position
onPanResponderMove: Animated.event([
null, {dx: this.state.pan.x, dy: this.state.pan.y},
]),
onPanResponderRelease: (e, {vx, vy}) => {
}
});
}
Basically, upon dragging we updat the states pan value, and when we move, we set the dx/dy to the value from the pan.
Now that we have our values, we can use this in our render()
method, which gets called constantly as we're dragging, so we can calculate the position of our image in there:
render() {
// Destructure the value of pan from the state
let { pan } = this.state;
// Calculate the x and y transform from the pan value
let [translateX, translateY] = [pan.x, pan.y];
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let imageStyle = {transform: [{translateX}, {translateY}]};
return (
<View style={styles.container}>
<Animated.View style={imageStyle} {...this._panResponder.panHandlers}>
<Image source={require('./assets/panresponder.png')} />
</Animated.View>
</View>
);
}
We're getting somewhere. When you run the app now you will be able to drag the image around the screen! However, when you do this for a second time you will notice it starts from the middle of the screen again instead of following up where you left it.
Let's fix that.
Fortunately, it's quite simple. We need to alter the initial value in onPanResponderGrant
to take in account the correct offset (we dragged it off center):
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
},
If you were to run the code again you will notice a second drag and drop works perfectly, but every time after that the image will jump erratically. This has to do with the way the offset is calculated. We actually need to flatten this once you let go of the image. This can be done in our 3rd and last method:
onPanResponderRelease: (e, { vx, vy }) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
};
Last but not least, lets make the image change in size while we're dragging. First we'll add a scale
property to our state so we can use this in our style and influence its value in the PanResponder
this.state = {
pan: new Animated.ValueXY(),
scale: new Animated.Value(1),
};
We'll use the value of this in our style inside the render method
...
let rotate = '0deg';
// Calculate the transform property and set it as a value for our style which we add below to the Animated.View component
let imageStyle = {transform: [{translateX}, {translateY}, {rotate}, {scale}]};
...
With this in place all that's left to do is influence the value of scale
in the PanResponder implementation. When we start dragging the onPanResponderGrant
method is invoked, so we can animate the value
onPanResponderGrant: (e, gestureState) => {
// Set the initial value to the current state
this.state.pan.setOffset({x: this.state.pan.x._value, y: this.state.pan.y._value});
this.state.pan.setValue({x: 0, y: 0});
Animated.spring(
this.state.scale,
{ toValue: 1.1, friction: 3 }
).start();
},
and when we release it we'll animate it back
onPanResponderRelease: (e, { vx, vy }) => {
// Flatten the offset to avoid erratic behavior
this.state.pan.flattenOffset();
Animated.spring(this.state.scale, { toValue: 1, friction: 3 }).start();
};
And that's it! We got an image we can drag around, and it will give a visual indication we're doing so (besides following our finger).
The resulting code can be found here on Github, in case you didn't follow along or want to review it.
As always, should you have any questions you can find me on Twitter.
Happy coding!