Let’s Get Moving: An Introduction to React Native Animations — Part 3
In Part 2 we discussed the Animated API, interpolating values, and looping animations.
How about we wrap things up?
The last thing we’re going to cover in this series is animating an element on the screen based on a user’s gesture input. You may not think of this as an “animation” immediately, but the element isn’t just going to move itself.
To do this we’re going to put to use what React Native calls a PanResponder
. We will use one of these to allow us to drag Jake around the screen.
First, let’s create a new Animated.Value
to hold the position of Jake and update it with the user’s gesture. Since we will want to move Jake along both the X and Y axes, we will create an Animated.ValueXY
in our constructor, instead of just an Animated.Value
. We don’t need to pass it any default, since we don’t need to make any assumptions about the starting value or interpolate it like we did with the rotation value in the last article.
this._gestureValue = new Animated.ValueXY();
Next, we need to create our instance of a PanResponder
. We import PanResponder from 'react-native'
, and create it in right below our new _gestureValue
. It takes a number of configuration options. This is straight out of the React Native documents:
this._panResponder = PanResponder.create({
// Ask to be the responder:
onStartShouldSetPanResponder: (evt, gestureState) => true,
onStartShouldSetPanResponderCapture: (evt, gestureState) => true,
onMoveShouldSetPanResponder: (evt, gestureState) => true,
onMoveShouldSetPanResponderCapture: (evt, gestureState) => true,onPanResponderGrant: (evt, gestureState) => {
// The gesture has started. Show visual feedback so the user knows
// what is happening!
// gestureState.d{x,y} will be set to zero now
},
onPanResponderMove: (evt, gestureState) => {
// The most recent move distance is gestureState.move{X,Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d{x,y}
},
onPanResponderTerminationRequest: (evt, gestureState) => true,
onPanResponderRelease: (evt, gestureState) => {
// The user has released all touches while this view is the
// responder. This typically means a gesture has succeeded
},
onPanResponderTerminate: (evt, gestureState) => {
// Another component has become the responder, so this gesture
// should be cancelled
},
onShouldBlockNativeResponder: (evt, gestureState) => {
// Returns whether this component should block native components from becoming the JS
// responder. Returns true by default. Is currently only supported on android.
return true;
},
});
A lot of these are unnecessary for what we’re going to be doing here, but we’ll just leave them for reference. The main ones we’re interested in here are onPanResponderMove
and onPanResponderRelease
.
onPanResponderMove
gets called as whatever the element it is bound to gets interacted with. We can see that pretty quickly by adding some logging to it:
this._panResponder = PanResponder.create({
{...}
onPanResponderMove: (evt, gestureState) => {
// The most recent move distance is gestureState.move{X,Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d{x,y}
console.log("onPanResponder: ", gestureState);
},
{...}
});
To bind our panResponder
to an element we spread its panHandlers
into it. In this case we will do that into the Animated.Image
that represents our Jake like so:
<Animated.Image
source={jake}
style={{ transform: [this.getRotationAnimation()] }}
{...this._panResponder.panHandlers}
/>
Boot up your app with Remote JS Debugging turned on, then click inside the image of Jake and drag around.
So we can see that onPanResponderMove
gets called a lot, and the gestureState
that it gives us is an object with some movement data. gestureState
contains the following data:
stateID
- ID of the gestureState- persisted as long as there at least one touch on screenmoveX
- the latest screen coordinates of the recently-moved touchmoveY
- the latest screen coordinates of the recently-moved touchx0
- the screen coordinates of the responder granty0
- the screen coordinates of the responder grantdx
- accumulated distance of the gesture since the touch starteddy
- accumulated distance of the gesture since the touch startedvx
- current velocity of the gesturevy
- current velocity of the gesturenumberActiveTouches
- Number of touches currently on screen
Honestly, some of these I’ve never used, and in fact, most of the time dx
and dy
give me all that I need. I believe numberActiveTouches
essentially refers to how many fingers are interacting with the object, so that is probably how you get things like pinch/zoom gestures working.
Let’s get rid of the logging, and take the dx
/dy
values and use them to update the X and Y values of our _gestureValue
using setValue
.
{...}
onPanResponderMove: (evt, gestureState) => {
// The most recent move distance is gestureState.move{X,Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d{x,y}
this._gestureValue.setValue({
x: gestureState.dx,
y: gestureState.dy
});
},
{...}
Then, simply apply that value to the transform rule on our Animated.Image
, remembering that transform takes an array of objects each containing a single transform rule.
<Animated.Image
source={jake}
style={{
transform: [
this.getRotationAnimation(),
{ translateX: this._gestureValue.x },
{ translateY: this._gestureValue.y }
]
}}
{...this._panResponder.panHandlers}
/>
Refresh, and now you can drag our good good Jake around!
There is one clear issue here, right? Similar to the problem we had with our rotation starting back at 0, Jake jumps back to his origin at the beginning of each new gesture. We can fix this with some simple math. We just need to save the distance traveled at the end of each gesture and apply it to the beginning of each subsequent gesture.
To do this we create an object in our constructor to hold the offset: this._gestureOffset = { x: 0, y: 0 };
and then add it to the setValue
method inside of onPanResponderMove:
{...}
onPanResponderMove: (evt, gestureState) => {
// The most recent move distance is gestureState.move{X,Y}
// The accumulated gesture distance since becoming responder is
// gestureState.d{x,y}
this._gestureValue.setValue({
x: this._gestureOffset.x + gestureState.dx,
y: this._gestureOffset.y + gestureState.dy
});
},
{...}
Then at the end of each gesture we just update the offset. This will happen inside onPanResponderRelease
:
onPanResponderRelease: (evt, gestureState) => {
// The user has released all touches while this view is the
// responder. This typically means a gesture has succeeded
this._gestureOffset.x += gestureState.dx;
this._gestureOffset.y += gestureState.dy;
...
}
And we’re done! You should have a nice, consistently draggable Jake! Give yourself a round of applause, you’re now… well you’re not an expert in React Native Animations, but you at least got your feet wet, and that’s sometimes the hardest part.
You can find the final code here.
I hope this has been helpful for you. Let me know if you have any questions or any suggestions on other topics to cover. Thanks!