Let’s Get Moving: An Introduction to React Native Animations — Part 3

If you want to start completely from scratch, here’s a link to Part 1 where we used LayoutAnimation to animate the repositioning of a single element on screen.

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 screen
  • moveX - the latest screen coordinates of the recently-moved touch
  • moveY - the latest screen coordinates of the recently-moved touch
  • x0 - the screen coordinates of the responder grant
  • y0 - the screen coordinates of the responder grant
  • dx - accumulated distance of the gesture since the touch started
  • dy - accumulated distance of the gesture since the touch started
  • vx - current velocity of the gesture
  • vy - current velocity of the gesture
  • numberActiveTouches - 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!

Sometimes the simulator can run a big laggy, it will look much nicer on a real device.

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!