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.
react-native-gesture-handler
#
Install 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 onlyInstall native dependencies
cd ios && pod install
#
Android onlyUpdate 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:
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 deleteThe 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 PanGestureHandlerFirst, 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.
Checkpoint ๐
#
Delete item after swiping over a certain thresholdLet'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); } }, }, []);