Skip to main content

Gestures

Animation is often combined with gesture events to give allow the user to interact with your app. One example of this is swipe-to-delete.

Lets use Reanimated 2 together with react-native-gesture-handler to implement swipe-to-delete UI for the mood history.

Install react-native-gesture-handler#

yarn add react-native-gesture-handler# ornpm install --save react-native-gesture-handler

Open index.js and add the following import as the very first line in the file:

import 'react-native-gesture-handler';

iOS only#

Install native dependencies

cd ios && pod install

Android only#

Update your MainActivity.java file, so that it overrides the method responsible for creating ReactRootView instance and then use the root view wrapper provided by this library. Do not forget to import ReactActivityDelegate, ReactRootView, and RNGestureHandlerEnabledRootView:

src/main/java/com/moodtracker/MainActivity.java
package com.swmansion.gesturehandler.react.example;
import com.facebook.react.ReactActivity;+ import com.facebook.react.ReactActivityDelegate;+ import com.facebook.react.ReactRootView;+ import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView;public class MainActivity extends ReactActivity {
  @Override  protected String getMainComponentName() {    return "MoodTracker";  }+  @Override+  protected ReactActivityDelegate createReactActivityDelegate() {+    return new ReactActivityDelegate(this, getMainComponentName()) {+      @Override+      protected ReactRootView createRootView() {+       return new RNGestureHandlerEnabledRootView(MainActivity.this);+      }+    };+  }}

Add swipe to delete#

The idea behind swipe to delete is this: we wrap the whole row in a GestureHandler, animate the row horizontally when the user "swipes", and trigger the "delete" animation if the user swipes across a certain threshold.

Wrap MoodItemRow in a PanGestureHandler#

First, let's wrap the whole MoodItemRow component in a PanGestureHandler. This allows us to track gesture events within the designated area:

import { PanGestureHandler } from "react-native-gesture-handler";
...
<PanGestureHandler  minDeltaX={1}  minDeltaY={100}>
...
</PanGestureHandler>

Next, let's add a callback for onGestureEvent - this gets called whenever the user interacts with the area within the gesture handler:

import { useAnimatedGestureHandler } from 'react-native-reanimated';
const onGestureEvent = useAnimatedGestureHandler(  {    onActive: event => {      console.warn(event.translationX);    },  },  [],);
<PanGestureHandler  minDeltaX={1}  minDeltaY={100}  onGestureEvent={onGestureEvent}>...

Now we want to use the event.translationX value and animate the row right or left based on how much the user has moved.

To store an animated value, we can use useSharedValue, and to use it in an inline style we can use useAnimatedStyle.

Finally, in order to animate a view using an animated style, replace the View in question with Animated.View:

import Reanimated, {  useAnimatedStyle,  useSharedValue,} from 'react-native-reanimated';...
const offset = useSharedValue(0);
...
const animatedStyle = useAnimatedStyle(() => ({  transform: [{ translateX: offset.value }],}));
const onGestureEvent = useAnimatedGestureHandler(  {    onActive: event => {      const xVal = Math.floor(event.translationX);      offset.value = xVal;    },  },  [],);...
<Reanimated.View style={[styles.moodItem, animatedStyle]}>...

Lastly, we want to snap the row back to its original position if the user finishes dragging. Conveniently enough the animated gesture handler has an onEnd function:

const onGestureEvent = useAnimatedGestureHandler(  {    onActive: event => {      const xVal = Math.floor(event.translationX);      offset.value = xVal;    },+    onEnd: () => {+      offset.value = withTiming(0);+    },  },  [],);

Notice that we set the value back withTiming(0) - this ensures that the card will animate back instead of snapping.

Delete item after swiping over a certain threshold#

Let's do it so that if the user swipes 80pt or more in either direction, the item gets deleted:

import { PanGestureHandlerGestureEvent } from 'react-native-gesture-handler';import { runOnJS } from 'react-native-reanimated';
const removeWithDelay = React.useCallback(() => {  setTimeout(() => {    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);    appContext.handleDeleteMood(item);  }, 250);}, [appContext, item]);
const onGestureEvent = useAnimatedGestureHandler<  PanGestureHandlerGestureEvent,  { shouldRemove: boolean }>(  {    onActive: (event, ctx) => {      const xVal = Math.floor(event.translationX);      offset.value = xVal;
      // use Absolute value so the user could swipe either left or right      if (Math.abs(xVal) <= maxPan) {        ctx.shouldRemove = false;      } else {        ctx.shouldRemove = true;      }    },    onEnd: (_, ctx) => {      if (ctx.shouldRemove) {        // if the item should be removed, animate it off the screen first        offset.value = withTiming(Math.sign(offset.value) * 2000);
        // then trigger the remove mood item with a small delay        runOnJS(removeWithDelay)();      } else {        // otherwise, animate the item back to the start        offset.value = withTiming(0);      }    },  },  []);