How to create performant gesture-based animations

Sure, there are existing swipe-to-delete packages out there, but sometimes you just gotta roll your own. I was recently asked how to add a swipe-to-delete feature to the drag-and-drop FlatList component I wrote and was curious to see how big of a job this would be.

My component already uses a lot of nested gesture handlers and I wasn’t sure if I could add another one without making major alterations to the existing code (It turns out, I didn’t have to make any changes—yes!). In the process, I realized that implementing swipe-to-delete from scratch would be a fairly simple and practical intro to gesture-based animations in React Native. So here goes!

An extended version of the swipe-enabled component we build in this tutorial is now an NPM package that you can use in your app! Find it here: https://github.com/computerjazz/react-native-swipeable-item

What we’ll do:

Wrap each FlatList item in a wrapper that handles swipe/pan gestures and slide animations

Remove the swiped item when row is swiped past threshold

Animate rows to fill the gap left by the removed item

In most apps, swiping a row would reveal a hidden overflow menu where the delete button would live, but for simplicity’s sake ours will be more aggressive. As soon as the user swipes past our threshold, the item will be deleted.

Animations here are crucial. It would be a jarring experience if our list instantaneously updated. I might not even realize anything was deleted in the example below:

Instantaneous list update makes it hard to understand what’s going on

Instead, we want to see the deleted item disappear and the other list items animate in to fill the space, giving the user a clear understanding about what just took place. Thankfully, React Native’s LayoutAnimation API will handle this for us. More on that later, but the end result looks much nicer:

Layout animation makes it clear that a row is being removed

Wrap each row in a component that handles swipe gestures and animations

This is the most complex piece of the puzzle, so let’s think about what exactly needs to happen:

When the user touches a row and “pans” (moves their finger on the screen) to the left, the row should move with their finger

Once the user pans to the left past some threshold, the delete method should be triggered

method should be triggered If the user lifts their finger before the threshold, the row should spring back into its original place

Gesture Handling

Let’s pause for a minute to review gesture handlers. A gesture handler is a component that wraps a view and reports any touch data that happens on that view as a stream of events (tap, swipe, pinch, pan, etc.). I like to think of gesture handlers as a “window” that sits on top of the wrapped view and extends its functionality, kind of like the old GameBoy peripherals did:

We’re going to use the PanGestureHandler from React Native Gesture Handler, which has a friendly API and enables some performance benefits we’ll explore below.

Our gesture handler can be in different states:

BEGAN — Gesture just began

ACTIVE — Gesture is currently in progress

END — User has completed the gesture

CANCELLED/FAILED— Gesture has ended for some external reason

UNKNOWN — User has not yet interacted with handler

We can register callbacks with the handler that fire when the user interacts with the wrapped view:

onHandlerStateChange: Fires when the state of the gesture handler changes, like when the user begins or ends a pan. onGestureEvent: Fires in a constant stream as the user is performing the gesture. Provides updated data about where on the screen the gesture is taking place, the velocity of a moving finger, etc.

In order to run JavaScript code that responds to gestures (like moving a view to follow your finger) the touch events all have to cross the bridge, which can be a major performance bottleneck and result in janky animations. This is where React Native Gesture Handler and React Native Reanimated come together beautifully.

Reanimated

Reanimated is an alternative to React Native’s own Animated API. It has a bit of a learning curve but it opens the door to much more performant animations by enabling gesture-based animations without crossing the bridge.

Reanimated maintains a tree of “nodes”, which are special data types that can store simple numeric values, but can also represent the result of operations on multiple values, like interpolations or mathematical operations like multiply(a, b) and sin(n) . Animated components can use these nodes to change certain style properties, like opacity, color, and transforms*.

Think of Reanimated as a tiny programming language that describes these operations on nodes. Instead of if/else blocks, Reanimated has cond() , instead of && it has and() , and instead of using = for assignment it uses set() .

Here’s how equivalent JS and Reanimated logic might compare:

We build up blocks of logic using Reanimated that get sent over the bridge once, then may never have to cross the bridge again (unless we explicitly want to call back to JS, which we’ll see later). This allows our animations to be driven by the stream of gesture data without a trip over the bridge, resulting in much smoother animations.

Using Gestures To Drive Animated Values

Instead of a traditional callback, we pass our gesture handler a Reanimated event . An event describes how we want the gesture data to interact with our Reanimated nodes.

On each gesture event, we set the animState.position value to translationX , which is the distance the finger has traveled from the beginning of the gesture (positive numbers mean the user is swiping right, negative to the left). We’ll set this value as our row’s translateX transform , which will cause it to slide slide left/right as the value changes, following the finger.

// Update our animated translation value to follow finger onPanEvent = event([{

nativeEvent: ({ translationX }) => block([

set(this.animState.position, translationX)

])

}]) ... // Attach animated value to our View <Animated.View

style={{

transform: [{ translateX: this.animState.position }]

}}

/>

If this translation amount exceeds our threshold, we make a call to JS to delete the swiped row item (we’re deleting using a left swipe, so our translation distance value will be negative, and we’ll check whether it is less than our threshold). Since our animations run on the native side, we use call() to invoke a JS function from within our Reanimated logic:

// If translationX is less than our swipeThreshold

// call our onSwipe callback. cond(

lessThan(translationX, swipeThreshold),

call([position], () => this.props.onSwipe(this.props.item)

)

)

We want the row to spring back into place when when the user lifts their finger before reaching the threshold. To run a timing or spring animation, we create a clock which “ticks” on every frame. Since we’re using a spring animation, we create an animation config with properties like mass and damping . We’ll also create an object that will store data about the state of the animation, like the current position and whether or not it has finished. While the clock is running, we’ll call spring() , which increments our position value each frame to match our animation config:

When the finger is released before the threshold is reached, the gesture handler goes into state END , and we start our clock. This resets our row translation back to 0 using our spring animation:

// If the user has ended the gesture AND the clock is not running,

// start clock. cond(

and(

eq(state, GestureState.END),

not(clockRunning(this.clock)

)

), startClock(this.clock))

In order for Reanimated to run our spring animation, the spring node must be included in its update tree, which is updated each frame. Reanimated provides Animated.View , which is a special kind of View component that can use Animated nodes in its style prop. Nodes that are used in an Animated.View are automatically included in the update tree, like our animState.position value that slides our row. But since our spring isn’t directly attached to a View , we manually include it in the tree by putting it in an Animated.Code block within render() :

Putting it all together

We now can package all of our gesture and animation logic into a component we’ll call SwipeRow . We’ll render children inside of our gesture handler and our translation-enabled Animated.View so that we can pass arbitrary child components from the parent. All we need to do now is handle item deletion and we’re done!

Delete the swiped item when row is swiped past threshold

Our delete method is super simple. It filters out the passed-in item from the current items in state, sets that updated array as our new state, and configures the next LayoutAnimation .

LayoutAnimation makes animating layout changes a breeze. We call LayoutAnimation.configureNext() when we make a change that triggers a re-render and the transition animation will be handled for us:

I hope you now feel more confident in using Reanimated and React Native Gesture Handler to build performant gesture-based animations!

Finished product here: https://snack.expo.io/@computerjazz/swipetodelete-rngh-reanimated