Tinder Swipe in React Native

September 6, 2020 - 5 min read

Tinder is a dating app that shows you profiles as a card stack. You drag and swipe the card left in order to dismiss (nope) or swipe right in order to agree (like) the other person. I will describe how to implement these animations in a React Native app.

The source code can be found here.

In Short

  • react-native-fast-image for better performing images
  • Render 2 images, where the front image is actively animated, and the one behind (next on the stack) static
  • PanGestureHandler for tracking drag movements
  • Change of position (dragging) of the image via translationX and translationY transform property (on active pan gesture)
  • Rotation of the image via rotationZ transform property. Gets closer to 20° the further the image is to an edge, tracked via translationX
  • Swipe end animations via spring where the destination can get back to initial, or far over the screen to the left/right

#1 The Basic App

Since the app is based on React Native, we can quite easily specify the user interface. The most critical component is the image view, here showcasing random (static, not fetching anything) picture from Unsplash's flower category. In the end, these pictures will be hold in an array, and an index will be used to track the current (and next) image shown. I call it "Card Stack", since it will render two images behind another.

In order for the image stack to render correctly, you may later need to adjust the zIndex, elevation and key props of the animated component. The latter prop may be especially critical here, if you run into render performance issues. That key prop is used by React to tell whether or not to re-render a specific component, which means it must be unique. You can read more about the key prop in React's docs.

#2 The Animation Setup

We need some kind of tracker that allows us to decide whether the card should be moved or swiped in a certain direction. Also, the "like" and "nope" label boxes and rotation of the image card should be rotated and displayed according to the swipe direction.

For the animation in React Native we will use react-native-reanimated (v2). It provides a PanGestureHandler for dragging movement gestures. It has an "onStart" callback, that is triggered when the finger (or multiple) has been placed on the screen. From there on, we know that the animation (movement of the finger / dragging) is running. Also, we can keep the initial touch coordinates. These are useful for tracking and calculating the distance that the image has moved, via the "onActive" callback.

When the finger has been released, the "onEnd" callback will be fired. From there on we can decide whether to swipe/snap to the right, left or "jump back" to the initial positions. For the left/right decision we specify a threshold of 40 percent of the screen's width. In relation to the initial x touch coordinate, we can calculate the absolute distance (in px) we have swiped. For example if you swipe to the left, the distance will be negative (since 0 is in the screen's center).

The Spring animation performs a movement from a to b with the option to specify physical properties, like velocity or mass. This is pretty useful for the three swipe/snap movements described before.

onEnd: (event, ctx) => {
// dragged 40 percent of the screen's width
const thresh = width * 0.4;
// how much the user moved the image horizontally
const diff = ctx.startX + event.translationX;
if (diff > thresh) {
// swiped right
onSnap({ right: true });
} else if (diff < -1 * thresh) {
// swiped left
onSnap({ right: false });
} else {
// no left or right swipe, so 'jump' back to the initial position
// withSpring is from 'react-native-reanimated'
x.value = withSpring(0);
y.value = withSpring(0);
}
}

The Interpolation animation allows us to map a specific value from a range (input range) to an output range. For example, in relation to the distance in x (translationX) from 0 to half the screen width, we can rotate the image by up to 10 degrees (z axis).

By extending the interpolation, we can use the same call for both directions, meaning left and right swipe. This also means, that the z rotation would extend over 10 degrees. That is okay, since the image card would also be out of screen.

Input (X) Output (Z Rotation Degree)
-500 -20
-250 -10
-125 -5
0 0
125 5
250 10
500 20

When putting it into code, it would look like the following. Note, that the -1 relates to a different angle, depending on where the user dragged the image from. If they touch the upper part, the rotation would be the opposite to touching the lower part.

// the further we are to the left (-) or right (+), we rotate by up to 10deg
// interpolate is from 'react-native-reanimated'
const rotateZ = interpolate(x.value,
[0, -1 * (screenWidth / 2)],
[0, 10]);

Styling the actual Animated.View plus using useSharedValue requires using useAnimatedStyle. This can be easy to mismatch.

const imageStyle = useAnimatedStyle(() => {
// ...
// x/y are based on "useSharedValue"
// and assigned in PanGestureHandler
return {
transform: [
{ translateX: x.value },
{ translateY: y.value },
{ rotateZ: `${rotateZ}deg` },
],
};
});

#3 The Final Swipe Part

As soon as the user swiped a certain amount to the left or right, a specific animation will be triggered. This will lead to a chain of different actions. The card image must be swiped out of the screen, to the respective direction, via spring. After that, the next image(s) must be toggled. Finally, the position must be reset. While the spring animation to move the card out of the screen is running, we don't want to toggle the next image right away. Instead, we wait for 300ms, using setTimeout.