Adding animations to your React Native app - Part 3: Gesture animations
Tags: reactnative, animations
Published: 2019-07-15 00:27:10

---
title: Adding animations to your React Native app - Part 3: Gesture animations
published: true
description: In this tutorial you'll learn how to implement gesture animations in React Native
tags: [react-native, animations]
canonical_url: https://blog.pusher.com/animation-react-native-part-3/
series: Adding animations to your React Native app
---

Welcome to the final part of a three-part series on adding animations to your React Native app. In this part, we’ll be taking a look at how you can respond to the user’s gestures and animate the components involved in the interaction.

## Prerequisites

To follow this tutorial, you need to know the basics of React and React Native.

Going through the previous parts of this series will be helpful but not required. Though this tutorial assumes that you know how to implement basic animations in React Native. Things like scale, rotation, and sequence animations. We will be applying those same concepts when implementing gesture animations.

## What you’ll be building

In this part of the series, we’ll be looking at how to implement different kinds of gestures and how to animate them.

Here’s what you’ll be building:

![Final output implementing pan, swipe, and pinch gestures (Image credit: pokeapi.co, pokemondb.net)](https://d2mxuefqeaa7sj.cloudfront.net/s_115C812B8243F8C747802527A22CB63F3A3C1BF41FEB8D4721AEFEDEECD662D3_1530241427144_all-gestures.gif)

You can find the full source code for this tutorial on its [GitHub repo](https://github.com/anchetaWern/RNRealworldAnimations).

## Setting up the project

To follow along, you first need to clone the repo:

```javascript
git clone https://github.com/anchetaWern/RNRealworldAnimations.git
```

After that, switch to the `part2` branch and install all the dependencies:

```
cd RNRealworldAnimations
git checkout part2
npm install
```

Next, initialize the `android` and `ios` folders and then link the native modules:

```
react-native upgrade
react-native link
```

Once that’s done, you should be able to run the app on your device or emulator:

```
react-native run-android
react-native run-ios
```

The `part2` branch contains the final output of the second part of this series. That’s where we want to start with, as each part is simply building upon the previous part.

## Drag and drop

The first gesture that we’re going to implement is drag-and-drop, and looks like this:

![Drag and drop gesture](https://d2mxuefqeaa7sj.cloudfront.net/s_115C812B8243F8C747802527A22CB63F3A3C1BF41FEB8D4721AEFEDEECD662D3_1530171143536_drag-and-drop.gif)

### Removing the animated header

Before we start implementing this gesture, we first have to remove the animated header that we did on part two of the series. This is because that animation gets in the way when the user begins to drag the cards around.

To remove the animated header, open `src/screens/Main.js` and remove these lines:

```javascript
import AnimatedHeader from "../components/AnimatedHeader";

<AnimatedHeader
  title={"Poke-Gallery"}
  nativeScrollY={nativeScrollY}
  onPress={this.shuffleData}
/>;
```

You can check the specific commit diff for the change above [here](https://github.com/anchetaWern/RNRealworldAnimations/commit/b0d9ccb7d3cf79feccaa278982d01d12c7399c05#diff-b0a6c7c40d6cbc644f5d286b0a79fa4a). Note that there are other changes in there as well, but mind only the changes that have to do with removing the `AnimatedHeader` component for now. Once that’s done, you should be able to delete the `src/components/AnimatedHeader.js` file as well.

Next, update `src/components/CardList.js` and convert the `Animated.ScrollView` into a regular one. Since we’ll no longer be using the `AnimatedHeader`, there’s no point in keeping the code for animating it on scroll:

```javascript
<ScrollView>
  <View style={[styles.scroll_container]}>
    <FlatList ... />
  </View>
</ScrollView>
```

You can check the specific [commit diff here](https://github.com/anchetaWern/RNRealworldAnimations/commit/b0d9ccb7d3cf79feccaa278982d01d12c7399c05#diff-7c6a1b60deaba243be304c4123215d65). Note that I’ve added a few props as well, but we’ll get to that later.

### Implementing the gesture

The gesture that we’re trying to implement is called “panning”. It is when a user drags their thumb (or any finger) across the screen in order to move things around. In this case, we want the user to be able to drag the cards around so they could drop it inside a specific drop area.

Here’s a break down of what we’ll need to do in order to implement the drag and drop:

1.  Update the `Card` component to use the React Native’s [PanResponder module](https://facebook.github.io/react-native/docs/panresponder.html). This makes it draggable across the screen.
2.  Create a `DropArea` component which will be used as a dropping point for the cards. The styles of this component will be updated based on whether a card is currently being dragged and whether it’s within the drop area.
3.  Update the main screen to include the `DropArea`. Then create the functions to be passed as props to the `CardList`, `Card`, and `DropArea` components. This allows us to control them from the main screen.

Before we proceed, let’s take a moment to break down what we’re trying to accomplish:

1.  When the user touches the card and drags it around, we want it to scale it down and lower its opacity.
2.  While the user is holding down a card, we want to hide all the other cards to keep the focus on the card that they’re holding. The only components that need to show are the `DropArea` and the card.
3.  When the user drags the card over to a target area of the `DropArea`, we want to change the border and text color of the `DropArea` to green. This indicates that the user in on-target and they can drop the card there.
4.  If the user lets go of the card outside of the drop area, we bring it back its original position, hide the `DropArea`, and show all the cards that were hidden.

Now that that’s clear, the first step is to convert the `Card` component from a functional to a class-based component. We won’t really be using state in this component, so this change is only to organize the code better. While you’re at it, also remove the code for animating the card `onPressIn` and `onPressOut`. We don’t really want them to get in the way of the gesture that we’re trying to implement. Here’s what the new code structure will look like after the change. You can check the [diff here](https://github.com/anchetaWern/RNRealworldAnimations/commit/b0d9ccb7d3cf79feccaa278982d01d12c7399c05?diff=split#diff-d99b8305e62aa26a276f475d589ace28):

```javascript
// src/components/Card.js
// imports here
// next: import PanResponder

export default class Card extends Component<Props> {
  constructor() {}

  render() {
    // destructure the props here
    // the return code here
  }
}
```

Next, we need to import `PanResponder` module from `react-native`:

```javascript
import {
  // ..previously imported moodules here..
  PanResponder
} from "react-native";
```

The `PanResponder` module is React Native’s way of responding to gestures such as panning, swiping, long press, and pinching.

Next, update the `constructor` and initialize the animated values that we’re going to use. In this case, we want a scale and opacity animation. We’re also using a new `Animated.ValueXY`, this is the animated value for controlling the card’s position on the screen:

```javascript
// src/components/Card.js
constructor(props) {
  super(props);

  this.pan = new Animated.ValueXY(); // for animating the card's X and Y position
  this.scaleValue = new Animated.Value(0); // for scaling the card while the user drags it
  this.opacityValue = new Animated.Value(2); // for making the card translucent while the user drags it

  this.cardScale = this.scaleValue.interpolate({
    inputRange: [0, 0.5, 1], // animate to 0.5 while user is dragging, then to 1 or 0 once they let go
    outputRange: [1, 0.5, 1]
  });

  this.cardOpacity = this.opacityValue.interpolate({
    inputRange: [0, 1, 2], // default value is 2, so we'll animate backwards
    outputRange: [0, 0.5, 1]
  });
}
```

Next, inside `componentWillMount`, right below the prop destructuring, create a new PanResponder:

```javascript
componentWillMount() {
  const {
    // existing props here..
  } = this.props;

  // add the following:
  this.panResponder = PanResponder.create({
    onStartShouldSetPanResponder: () => true,
    onMoveShouldSetPanResponder: () => true,
    onPanResponderGrant: (e, gestureState) => {
      // next: add code for onPanResponderGrant
    },
    onPanResponderMove: (e, gesture) => {

    },
    onPanResponderRelease: (e, gesture) => {

    }
  });
}
```

Breaking down the code above:

- `onStartShouldSetPanResponder` - used for specifying whether to allow the PanResponder to respond to touch feedback. Note that we’ve simply supplied `true`, but ideally, you’ll want to supply a function which contains a condition for checking whether you want to initiate the gesture or not. For example, if you have some sort of “lock” functionality that the user needs to unlock before they can move things around.
- `onMoveShouldSetPanResponder` - for checking whether to respond to the dragging of the component where the PanResponder will be attached later on. Again, we’ve just specified `true`, but if you need to check whether a specific condition returns `true`, this is where you want to put it.

Oddly though, even if you supply `false` to all of the above or don’t supply it at all, it still works on the iOS simulator. But even if this is the case, it’s a good practice to still specify it because you never know how different devices behave in the real world.

- `onPanResponderGrant` - the function you supply here is executed when the PanResponder starts to recognize the user’s action as a gesture. So when the user has started to drag the card even just a little bit, the function will be executed. This is where we want to animate the card so it becomes smaller and translucent.
- `onPanResponderMove` - executed while the user drags their finger across the screen. This is where you want to execute the animation for changing the card’s position. This is also where you want to check whether the card is within the drop area or not so you can animate the styling of the `DropArea` component.
- `onPanResponderRelease` - executed when the user finishes the gesture. This is when their finger lets go of the screen. Here, you also need to check whether the card is within the drop area or not so you can animate it accordingly.

In my opinion, those are the most common functions that you will want to supply. But there are [others available](https://facebook.github.io/react-native/docs/panresponder.html#create).

Now, let’s add the code for each of the gesture lifecycle functions. For `onPanResponderGrant`, we’ll animate the `scaleValue` and `opacityValue` at the same time. Below it, we have a function called `toggleDropArea` which is used to update the state so it hides all the other cards and shows the `DropArea` component. We’ll be supplying this function later from the Main screen, then down to the CardList, and finally to each of the Card components. For now, don’t forget to destructure it from `this.props`:

```javascript
// src/components/Card.js
onPanResponderGrant: (e, gestureState) => {
  Animated.parallel([
    Animated.timing(this.scaleValue, {
      toValue: 0.5, // scale it down to half its original size
      duration: 250,
      easing: Easing.linear,
      useNativeDriver: true
    }),
    Animated.timing(this.opacityValue, {
      toValue: 1,
      duration: 250,
      easing: Easing.linear,
      useNativeDriver: true
    })
  ]).start();

  toggleDropArea(true, item); // supplied from the src/screens/Main.js down to src/components/CardList and then here. Accepts the drop area's visibility and the item to exclude from being hidden
};
// next: add code for onPanResponderMove
```

Next is `onPanResponderMove`. Here, we do the same thing we did on part two of this series when we implemented the animated header. That is, to use `Animated.event` to map the card’s current position to `this.pan`. This allows us to animate the card’s position as the user drags it around. `dx` and `dy` are the accumulated distance in the X and Y position since the gesture started. We use those as the values for `this.pan.x` and `this.pan.y`:

```javascript
onPanResponderMove: (e, gesture) => {
  Animated.event([null, { dx: this.pan.x, dy: this.pan.y }])(e, gesture);
  if (isDropArea(gesture)) {
    targetDropArea(true);
  } else {
    targetDropArea(false);
  }
},
// next: add code for onPanResponderRelease
```

Note that `Animated.event` returns a function, that’s why we’re supplying `e` (event), and `gesture` as arguments to the function it returns.

On `onPanResponderRelease`, we check whether the card is within the drop area by using the `isDropArea` function. Again, this function will be passed as props from the Main screen just like the `toggleDropArea` function. If the card is within the drop area, we set its opacity down to zero so it’s no longer visible. Then we call the `removePokemon` function (passed as props from the Main screen) to remove it from the state:

```javascript
onPanResponderRelease: (e, gesture) => {
  toggleDropArea(false, item); // hide the drop area and show the hidden cards

  if (isDropArea(gesture)) {
    Animated.timing(this.opacityValue, {
      toValue: 0,
      duration: 500,
      useNativeDriver: true
    }).start(() => {});

    removePokemon(item); // remove the pokemon from the state
  } else {
    // next: add code for animating the card back to its original position
  }
};
```

Next, animate the card to its original position. Here, we simply do the animation we declared on `onPanResponderGrant` in reverse. But the most important part here is animating `this.pan`. Since we’re working with a vector value, we specify an object containing the separate values for the `x` and `y` position. It’s a good thing, we can simply specify `0` as the value for both to bring them back to their original position. No need to calculate the card’s position or anything complex like that:

```javascript
// src/components/Card.js
Animated.parallel([
  Animated.timing(this.scaleValue, {
    toValue: 1,
    duration: 250,
    easing: Easing.linear,
    useNativeDriver: true
  }),
  Animated.timing(this.opacityValue, {
    toValue: 2,
    duration: 250,
    easing: Easing.linear,
    useNativeDriver: true
  }),
  Animated.spring(this.pan, {
    toValue: { x: 0, y: 0 },
    friction: 5,
    useNativeDriver: true
  })
]).start();
```

Next, let’s take a look at the `render` method. Here, everything else is still the same except for the animated styles we supply and the `Animated.View` in place of the `TouchableWithoutFeedback` that we previously had. The most important part that allows the component to be moved is the `transform` styles which correspond to `x` and `y` values of `this.pan`, and the supplying of the `PanResponder` props (`{...this.panResponder.panHandlers}`):

```javascript
// src/components/Card.js
render() {
  const {
    item,
    cardAction,
    viewAction,
    bookmarkAction,
    shareAction
  } = this.props;

  let [translateX, translateY] = [this.pan.x, this.pan.y];

  let transformStyle = {
    ...styles.card,
    opacity: item.isVisible ? this.cardOpacity : 0,
    transform: [{ translateX }, { translateY }, { scale: this.cardScale }]
  };

  return (
    <Animated.View style={transformStyle} {...this.panResponder.panHandlers}>
      ...
    </Animated.View>
  );
}
```

Moreover, we’re also expecting an `isVisible` property for each `item`. It will be used to hide or show the card. This property is what we update when the user starts or stops the gesture.

Next, we can now move on to creating the `DropArea` component. If you’ve seen the demo earlier, this is the little box that shows up in the top-right corner when the user starts the gesture. Here is the code:

```javascript
// src/components/DropArea.js
import React, { Component } from "react";
import { View, Text, Dimensions } from "react-native";

const DropArea = ({ dropAreaIsVisible, setDropAreaLayout, isTargeted }) => {
  const right = dropAreaIsVisible ? 20 : -200;
  const color = isTargeted ? "#5fba7d" : "#333";
  return (
    <View
      style={[styles.dropArea, { right }, { borderColor: color }]}
      onLayout={setDropAreaLayout}
    >
      <Text style={[styles.dropAreaText, { color }]}>Drop here</Text>
    </View>
  );
};

const styles = {
  dropArea: {
    position: "absolute",
    top: 20,
    right: 20,
    width: 100,
    height: 100,
    backgroundColor: "#eaeaea",
    borderWidth: 3,
    borderStyle: "dashed", // note: doesn't work on android: https://github.com/facebook/react-native/issues/17251
    alignItems: "center",
    justifyContent: "center"
  },
  dropAreaText: {
    fontSize: 15
  }
};

export default DropArea;
```

If you inspect the code above, no animation will actually happen. All we’re doing is changing the styles based on the following boolean values coming from the props:

- `dropAreaIsVisible` - whether the component is visible or not.
- `isTargeted` - whether a card is within its area.

`setDropAreaLayout` is a function that allows us to update the Main screen’s state of the position of the drop area in the screen. We’ll be needing that information to determine whether a card is within the drop area or not.

Next, update the Main screen. First, add the `isVisible` property to each of the Pokemon:

```javascript
    // src/screens/Main.js
    const animationConfig = {...}

    // add this:
    let updated_pokemon = pokemon.map(item => {
      item.isVisible = true;
      return item;
    });
```

Inside the component, since we’re no longer using the animated header, you now need to add a value for the `headerTitle`:

```javascript
// src/screens/Main.js inside navigationOptions
headerTitle: "Poke-Gallery",
```

Initialize the state:

```javascript
state = {
  pokemon: updated_pokemon, // change pokemon to updated_pokemon
  isDropAreaVisible: false
};
```

Inside the `render` method, we render the `DropArea` before the `CardList` so that it has lower `z-index` than any of the other cards. This way, the cards are always on top of the card list when the user drags it. Don’t forget to pass the props that we’re expecting from the `DropArea` component. We’ll declare those later:

```javascript
// src/screens/Main.js
import DropArea from "../components/DropArea"; // add this somewhere below the react-native imports

return (
  <View style={styles.container}>
    <DropArea
      dropAreaIsVisible={this.state.isDropAreaVisible}
      setDropAreaLayout={this.setDropAreaLayout}
      isTargeted={this.state.isDropAreaTargeted}
    />
    ..previously rendered components here ..next: update CardList
  </View>
);
```

Update the `CardList` so it has the props that we’re expecting:

```javascript
<CardList
  ...previous props here
  scrollEnabled={!this.state.isDropAreaVisible}
  toggleDropArea={this.toggleDropArea}
  dropAreaIsVisible={this.state.isDropAreaVisible}
  isDropArea={this.isDropArea}
  targetDropArea={this.targetDropArea}
  removePokemon={this.removePokemon}
/>
```

Next, we can now add the code for the functions that we’re passing. First is the `toggleDropArea`. As you’ve seen earlier, this function is called every time the user initiates and finishes the gesture. All it does is flip the `isVisible` property of each item based on the `isVisible` argument passed to it. When the user initiates the gesture, `isVisible` is `true`. This means that the `DropArea` is visible, but the rest of the cards are hidden. The `item` argument passed to this function is the object containing the details of the card being dragged. So we use it to find the index of the item to be excluded from the hiding. We then update the state with the new data:

```javascript
// src/screens/Main.js
toggleDropArea = (isVisible, item) => {
  if (item) {
    let pokemon_data = [...this.state.pokemon];
    let new_pokemon_data = pokemon_data.map(item => {
      item.isVisible = !isVisible;
      return item;
    });
    let index = new_pokemon_data.findIndex(itm => itm.name == item.name);

    if (isVisible) {
      new_pokemon_data[index].isVisible = true;
    }

    this.setState({
      isDropAreaVisible: isVisible,
      pokemon: new_pokemon_data
    });
  }
};
```

`setDropAreaLayout`'s job is to update the `dropAreaLayout` to be used by the `isDropArea` function:

```javascript
// src/screens/Main.js
setDropAreaLayout = event => {
  this.setState({
    dropAreaLayout: event.nativeEvent.layout
  });
};
```

`isDropArea` is where the logic for changing the styles of the drop area is. It’s also used to determine whether the card will be brought back to its original position or dropped in the drop area. Note that the condition below isn’t 100% fool-proof. `gesture.moveX` and `gesture.moveY` aren’t actually the position of the card in the screen’s context. Instead, these are the latest screen coordinates of the recently-moved touch. Which means that the position is based on where the user touched. So if the user held on to the lower portion of the card then they have to raise the card much higher than the drop area in order to target it. The same is true with holding on to the left side of the card. For that, they’ll have to move the card further right of the drop area in order to target it. So the safest place to touch is in the middle, right, or top:

```javascript
// src/screens/Main.js
isDropArea = gesture => {
  let dropbox = this.state.dropAreaLayout;
  return (
    gesture.moveY > dropbox.y + DROPAREA_MARGIN &&
    gesture.moveY < dropbox.y + dropbox.height + DROPAREA_MARGIN &&
    gesture.moveX > dropbox.x + DROPAREA_MARGIN &&
    gesture.moveX < dropbox.x + dropbox.width + DROPAREA_MARGIN
  );
};
```

The function above compares the position of the touch to the position of the drop area. We’re adding a `DROPAREA_MARGIN` to account for the top and right margin added to the `DropArea` component.

`targetDropArea` updates the state so that the text and border color of the drop area is changed:

```javascript
// src/screens/Main.js
targetDropArea = isTargeted => {
  this.setState({
    isDropAreaTargeted: isTargeted
  });
};
```

`removePokemon` removes the dropped item from the state. This also uses LayoutAnimation to animate the rest of the cards with a spring animation after a card has been dropped:

```javascript
// src/screens/Main.js
removePokemon = item => {
  let pokemon_data = [...this.state.pokemon];
  let index = pokemon_data.findIndex(itm => itm.name == item.name);
  pokemon_data.splice(index, 1);

  LayoutAnimation.configureNext(animationConfig);

  this.setState({
    pokemon: pokemon_data
  });
};
```

Next, update the `CardList` component to accept all the props that need to be passed to the `Card` component. We also need to optionally enable the ScrollView’s scrolling (`scrollEnabled` prop) because it gets in the way when the user begins to drag a card. Lastly, the `FlatList` also enables scrolling by default, so we disable that as well, and just rely on the `ScrollView` for the scroll:

```javascript
// src/components/CardList.js
const CardList = ({
  // previous props here..
  scrollEnabled,
  toggleDropArea,
  isDropArea,
  targetDropArea,
  removePokemon
}) => {
  return (
    <ScrollView scrollEnabled={scrollEnabled}>
      <View style={[styles.scroll_container]}>
        <FlatList
          scrollEnabled={false}
          ...previous props here
          renderItem={({ item }) => {
            <Card
              ...previous props here
              toggleDropArea={toggleDropArea}
              isDropArea={isDropArea}
              targetDropArea={targetDropArea}
              removePokemon={removePokemon}
            />
          }}
        />
      </View>
    </ScrollView>
  );
});
```

Also update the `Card` component to accept the props passed to it by the `CardList` component:

```javascript
// src/components/Card.js
componentWillMount() {
    const {
      /* previously accepted props here */
      toggleDropArea,
      isDropArea,
      targetDropArea,
      removePokemon
    } = this.props;
}
```

Next, update the layout settings file to export the `DROPAREA_MARGIN`:

```javascript
// src/settings/layout.js
const DROPAREA_MARGIN = 20;
export { DROPAREA_MARGIN };
```

Lastly, update the Main screen so it uses the `DROPAREA_MARGIN`:

```javascript
// src/screens/Main.js
import { DROPAREA_MARGIN } from "../settings/layout";
```

Once that’s done, you should achieve a similar output to the demo from earlier.

### Solving the FlatList drag-and-drop issue on Android

As you might already know, there are implementation differences between Android and iOS. And if you’ve worked with both platforms before, then you also know that React Native for Android is behind iOS. This means there are more Android-specific issues and gotchas that you might encounter when working with React Native.

This section is specifically for dealing with Android related issue when it comes to the `FlatList` component. The problem with it is that changing the opacity and z-index of the `Card` components while the user is dragging a card doesn’t achieve the same effect as in iOS. On Android, each row has some sort of a wrapper which wraps each of the `Card` components. This wrapper cannot be influenced by declaring a z-index for the `Card` component. And even though the `Card` components are currently hidden while the user is dragging, the card can’t actually be dragged outside its row because it becomes hidden as soon as you do so.

After days of looking for a solution, I gave up and decided to stick with a custom list instead.

Open, `src/components/CardList.js` and convert it into a class-based component:

```javascript
export default class CardList extends Component<Props> {
  render() {
    const { data, scrollEnabled } = this.props;

    return (
      <ScrollView scrollEnabled={scrollEnabled}>
        <View style={[styles.scroll_container]}>{this.renderPairs(data)}</View>
      </ScrollView>
    );
  }

  // next: add renderPairs method
}
```

The `renderPairs` function returns each individual row, and it uses the `renderCards` function to render the cards:

```javascript
renderPairs = data => {
  let pairs = getPairsArray(data);

  return pairs.map((pair, index) => {
    return (
      <View style={styles.pair} key={index}>
        {this.renderCards(pair)}
      </View>
    );
  });
};

// next: add renderCards function
```

Next, add the `renderCards` method. This is responsible for rendering the individual `Card` components:

```javascript
renderCards = pair => {
  const {
    cardAction,
    viewAction,
    bookmarkAction,
    shareAction,
    toggleDropArea,
    isDropArea,
    targetDropArea,
    removePokemon
  } = this.props;

  return pair.map(item => {
    return (
      <Card
        key={item.name}
        item={item}
        cardAction={cardAction}
        viewAction={viewAction}
        bookmarkAction={bookmarkAction}
        shareAction={shareAction}
        toggleDropArea={toggleDropArea}
        isDropArea={isDropArea}
        targetDropArea={targetDropArea}
        removePokemon={removePokemon}
      />
    );
  });
};
```

Outside the class, right before the `styles` declaration, create a function called `getPairsArray`. This function will accept an array of items and restructures it so that each row will have one pair of item:

```javascript
// src/components/CardList.js
const getPairsArray = items => {
  var pairs_r = [];
  var pairs = [];
  var count = 0;
  items.forEach(item => {
    count += 1;
    pairs.push(item);
    if (count == 2) {
      pairs_r.push(pairs);
      count = 0;
      pairs = [];
    }
  });
  return pairs_r;
};

const styles = { ... } // next: update styles
```

Don’t forget to add the styles for each pair:

```javascript
const styles = {
  /* previously added styles here */
  pair: {
    flexDirection: "row"
  }
};
```

Once that’s done, you should now be able to drag and drop the cards in the drop area.

If you’re having problems getting it to work, you can check the specific [commit here](https://github.com/anchetaWern/RNRealworldAnimations/commit/eb387a123b030b2d7d5089be675018f048017f56).

## Swipe

The next gesture that we’re going to implement is swipe, and it looks like this:

![Swipe gesture](https://d2mxuefqeaa7sj.cloudfront.net/s_115C812B8243F8C747802527A22CB63F3A3C1BF41FEB8D4721AEFEDEECD662D3_1530171140999_swipe-animation.gif)

This part builds up from the drag-and-drop gesture. So the list of Pokemon you see above are the one’s that were dropped. Swiping the item to the right removes it entirely.

Let’s see what needs to be done in order to implement this:

1.  Create a `Swiper` component where the swiping gesture will be added. These are the items that you see in the above demo.
2.  Create a Trash screen. This is where the `Swiper` component will be used to display the items that were dropped in the drop area.
3.  Update the Main screen to add a button for navigating to the Trash screen. The `pokemon` state (the array containing the list of Pokemon) also needs to be updated so the dropped Pokemon will be removed and copied into another state (`removed_pokemon`) which contains an array of Pokemon that were dropped. This is then used as the data source for the Trash screen.

### Create a Swiper component

Let’s proceed with creating the `Swiper` component. First, import all the things that we’ll need:

```javascript
// src/components/Swiper.js
import React, { Component } from "react";
import {
  View,
  Image,
  Text,
  Dimensions,
  Animated,
  PanResponder
} from "react-native";

const width = Dimensions.get("window").width; // for calculating the distance swiped
```

Next, create the component. When the user swipes an item to the right, it removes that item from the `removed_pokemon` state so we need to accept the `item` to be removed. We also need to pass the `dismissAction` which will remove the item from the state:

```javascript
const Swiper = ({ item, dismissAction }) => {
  // next: add animated values
};
```

Next, we initialize the animated values that we will be using. We want the items to be moved to the left or to the right (`translateX`), and make it translucent (`opacityValue`) as they move it:

```javascript
let translateX = new Animated.Value(0);
let opacityValue = new Animated.Value(0);

let cardOpacity = opacityValue.interpolate({
  inputRange: [0, 1],
  outputRange: [1, 0.5]
});

// next: add PanResponder code
```

Next, create the PanResponder:

```javascript
let panResponder = PanResponder.create({
  onStartShouldSetPanResponder: () => true,
  onMoveShouldSetPanResponder: () => true,
  onPanResponderMove: (e, gesture) => {
    // next: add code for onPanResponderMove
  },
  onPanResponderRelease: (e, { vx, dx }) => {}
});
```

When the user begins the gesture, we map the value of `dx` to the animated value `translateX`. We did the same thing with the `Card` component earlier. The only difference is that this time, we’re only translating the `X` position. Below that, we also animate the `opacityValue`. We’ve used a really low `duration` value so the component will immediately turn translucent as soon as the gesture is started:

```javascript
onPanResponderMove: (e, gesture) => {
  Animated.event([null, { dx: translateX }])(e, gesture);
  Animated.timing(opacityValue, {
    toValue: 1,
    duration: 50,
    useNativeDriver: true
  }).start();
};
// next: add code for onPanResponderRelease
```

When the user ends the gesture, we determine whether to bring the component back to its original position or animate the remaining distance all the way to the right. Luckily, the way we implement it is pretty straightforward. We compare the absolute value of `dx` (the accumulated distance of the gesture in the `X` axis) to half of the screen’s total width. So if the user managed to drag the component to greater than half of the screen then it means that they want to remove the item. We’re also checking if `dx` is not a negative number. We only consider the gesture as valid if the user is swiping to the right. This works because `dx` will have a negative value if the user swipes to the left.

As for the animated value, we simply use the screen’s width as the final value. If `dx` is greater than `0` then it means they’re swiping to the right so we have a positive `width` value. Otherwise, we use the negative `width`:

```javascript
onPanResponderRelease: (e, { vx, dx }) => {
  if (Math.abs(dx) >= 0.5 * width && Math.sign(dx) != -1) {
    dismissAction(item);
    Animated.timing(translateX, {
      toValue: dx > 0 ? width : -width,
      duration: 200,
      useNativeDriver: true
    }).start();
  } else {
    // next: add code for bringing back the component to its original position
  }
};
```

If the condition failed, we spring back the component to its original position and bring back its original opacity:

```javascript
Animated.parallel([
  Animated.spring(translateX, {
    toValue: 0,
    bounciness: 20,
    useNativeDriver: true
  }),
  Animated.timing(opacityValue, {
    toValue: 0,
    duration: 5,
    useNativeDriver: true
  })
]).start();
```

Next, we render the actual component:

```javascript
// src/components/Swiper.js
return (
  <Animated.View
    style={{
      transform: [{ translateX }],
      opacity: cardOpacity,
      ...styles.bar
    }}
    {...panResponder.panHandlers}
  >
    <Image source={item.pic} style={styles.thumbnail} resizeMode="contain" />
    <Text style={styles.name}>{item.name}</Text>
  </Animated.View>
);
```

Here are the styles:

```javascript
const styles = {
  bar: {
    height: 50,
    flexDirection: "row",
    borderWidth: 1,
    borderColor: "#ccc",
    alignItems: "center",
    marginTop: 2,
    marginBottom: 2,
    paddingLeft: 5
  },
  thumbnail: {
    width: 35,
    height: 35
  }
};

export default Swiper;
```

### Create the Trash screen

The Trash screen is where the `Swiper` component will be displayed. All the items that were dropped in the drop area will be displayed on this page:

```javascript
// src/screens/Trash.js
import React, { Component } from "react";
import { View, FlatList, LayoutAnimation } from "react-native";
import Swiper from "../components/Swiper";

type Props = {};
export default class Trash extends Component<Props> {
  static navigationOptions = ({ navigation }) => {
    return {
      headerTitle: "Trash",
      headerStyle: {
        elevation: 0,
        shadowOpacity: 0,
        backgroundColor: "#B4A608"
      },
      headerTitleStyle: {
        color: "#FFF"
      }
    };
  };

  state = {
    removed_pokemon: [] // array containing the items that were dropped from the Main screen
  };

  // next: add componentDidMount
}
```

When the component is mounted, we call `fillRemovedPokemon`. This will simply copy the contents of the `removed_pokemon` that was passed as a navigation param from the Main screen over to the `removed_pokemon` state of this screen. That way, this screen has its own state that we could manipulate:

```javascript
componentDidMount() {
  this.fillRemovedPokemon(this.props.navigation.state.params.removed_pokemon);
}
```

Here’s the `fillRemovedPokemon` method:

```javascript
fillRemovedPokemon = removed_pokemon => {
  this.setState({
    removed_pokemon
  });
};
```

Next, we render the actual list:

```javascript
render() {
  return (
    <View style={styles.container}>
      {this.state.removed_pokemon && (
        <FlatList
          data={this.state.removed_pokemon}
          renderItem={({ item }) => (
            <Swiper item={item} dismissAction={this.dismissAction} />
          )}
          keyExtractor={item => item.id.toString()}
        />
      )}
    </View>
  );
}
```

The `dismissAction` is the one responsible for removing the items that the user wants to remove. Here, we’re using LayoutAnimation to easily update the UI once an item has been removed. In this case, we’re using it to automatically fill the empty space left by the removed item:

```javascript
// src/components/Swiper.js
dismissAction = item => {
  LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
  let removed_pokemon = [...this.state.removed_pokemon];
  let index = removed_pokemon.findIndex(itm => itm.name == item.name);

  removed_pokemon.splice(index, 1);

  this.setState({
    removed_pokemon
  });
};
```

Here are the styles:

```javascript
const styles = {
  container: {
    flex: 1,
    backgroundColor: "#fff"
  }
};
```

### Update the Main screen

Next, update the `src/screens/Main.js` file. First, include the `IconButton` component. We’ll be using it to display a button inside the header:

```javascript
import IconButton from "../components/IconButton";
```

Update the `navigationOptions` to include `headerRight`. This allows us to render a component on the right side of the header. The `navigateToTrash` method is passed as a navigation param as you’ll see later:

```javascript
static navigationOptions = ({ navigation }) => {
  const { params } = navigation.state; // add this
  return {
    headerTitle: "Poke-Gallery",
    // add this:
    headerRight: (
      <IconButton
        icon="trash"
        onPress={() => {
          params.navigateToTrash();
        }}
      />
    ),
   // previous header config here...
  };
};
```

Next, update the state initialization code to include a default value for `removed_pokemon`:

```javascript
state = {
  pokemon: updated_pokemon,
  removed_pokemon: [], // add this
  isDropAreaVisible: false
};
```

After that, we set the `navigateToTrash` function as a navigation param:

```javascript
constructor(props) {
  /* previously added constructor code here */

  this.props.navigation.setParams({
    navigateToTrash: this.navigateToTrash
  });
}
```

Here’s the `navigateToTrash` method. We can’t really use this screen’s state from inside the `navigationOptions`, that’s why we’re setting this function as a navigation param:

```javascript
navigateToTrash = () => {
  this.props.navigation.navigate("Trash", {
    removed_pokemon: this.state.removed_pokemon
  });
};
```

Next, update the `removePokemon` method to include manipulation of `removed_pokemon`:

```javascript
removePokemon = item => {
  let pokemon_data = [...this.state.pokemon];
  let removed_pokemon_data = [...this.state.removed_pokemon]; // add this
  let index = pokemon_data.findIndex(itm => itm.name == item.name);
  let removed_pokemon = pokemon_data.splice(index, 1); // add 'let removed_pokemon' to this line
  removed_pokemon_data.push(removed_pokemon[0]); // add this
  LayoutAnimation.configureNext(animationConfig);

  this.setState({
    pokemon: pokemon_data,
    removed_pokemon: removed_pokemon_data // add this
  });
};
```

Lastly, update the `Root.js` file to include the Trash screen as one of the screens of the app:

```javascript
import TrashScreen from "./src/screens/Trash";

const transitionConfig = () => {
  // previous transition config here...
};

const MainStack = createStackNavigator(
  {
    // previous screens here..
    Trash: {
      screen: TrashScreen
    }
  }
  // previous config
);
```

Once that’s done, you should be able to test the swiping gesture.

## Pinch

The final gesture that we’re going to implement is pinch gesture, and it looks like this:

![Pinch gesture for zooming object in and out](https://d2mxuefqeaa7sj.cloudfront.net/s_115C812B8243F8C747802527A22CB63F3A3C1BF41FEB8D4721AEFEDEECD662D3_1530241189295_pinch-animation.gif)

In the above demo, the user uses a pinch gesture to zoom the image in and out. In my opinion, this gesture is one of the most complex ones that you can implement. So instead of implementing it with the PanResponder, we’ll use the [React Native Gesture Handler](https://kmagiera.github.io/react-native-gesture-handler/) library to make it easier.

Before we proceed, let’s take a look at the work involved in implementing this:

1.  Install and link React Native Gesture Handler to the project.
2.  Create a `PinchableImage` component. This is where we will use the gesture handler library to implement a pinch-to-zoom gesture.
3.  Update the `BigCard` component to replace the original image with the `PinchableImage` component.

### Installing react-native-gesture-handler

You can install the gesture handler with the following command:

```
npm install --save react-native-gesture-handler
```

At the time of writing this tutorial, `react-native link` doesn’t really link the native modules to the project. So we’ll have to use Cocoapods instead. This also means that you’ll have to do the manual install for Android if you’re planning to test on both iOS and Android.

If you only need to test on Android, you should be able to get away with using `react-native link` to link the native module. We’ll get to the Android installation shortly, first let’s get into iOS.

#### iOS configuration

`cd` into the `ios` directory and initialize a new `Podfile`:

```
cd ios
pod init
```

Next, replace its contents with the following. Here we’re adding React Native modules as pods, then below it (` pod 'RNGestureHandler``' `) is where we include the actual module for the gesture handler:

```
target 'RNRealworldAnimations' do

  rn_path = '../node_modules/react-native'
  pod 'yoga', path: "#{rn_path}/ReactCommon/yoga/yoga.podspec"
  pod 'React', path: rn_path, subspecs: [
    'Core',
    'CxxBridge',
    'DevSupport',
    'RCTActionSheet',
    'RCTAnimation',
    'RCTGeolocation',
    'RCTImage',
    'RCTLinkingIOS',
    'RCTNetwork',
    'RCTSettings',
    'RCTText',
    'RCTVibration',
    'RCTWebSocket',
  ]

  pod 'DoubleConversion', :podspec => "#{rn_path}/third-party-podspecs/DoubleConversion.podspec"
  pod 'glog', :podspec => "#{rn_path}/third-party-podspecs/glog.podspec"
  pod 'Folly', :podspec => "#{rn_path}/third-party-podspecs/Folly.podspec"

  pod 'RNGestureHandler', :path => '../node_modules/react-native-gesture-handler/ios'
end

post_install do |installer|
  installer.pods_project.targets.each do |target|
    if target.name == "React"
      target.remove_from_project
    end
  end
end
```

After that, install all the pods:

```
pod install
```

Once that’s done, you can now run your project:

```
cd ..
react-native run-ios
```

#### Android configuration

To configure it on Android, open the `android/settings.gradle` file and add the following right before the `include ':app'`:

```
include ':react-native-gesture-handler'
project(':react-native-gesture-handler').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-gesture-handler/android')
```

Next, open `android/app/build.gradle` and include `react-native-gesture-handler` right after `react-native-vector-icons`:

```
dependencies {
  compile project(':react-native-vector-icons')
  compile project(':react-native-gesture-handler')
  // ...other dependencies here
}
```

Next, open `android/app/src/main/java/com/rnrealworldanimations/MainApplication.java` and import the corresponding gesture handler library:

```
// other previously imported libraries here
import com.facebook.soloader.SoLoader; // previously imported library
import com.swmansion.gesturehandler.react.RNGestureHandlerPackage; // add this
```

Then use it inside `getPackages`:

```java
protected List<ReactPackage> getPackages() {
  return Arrays.<ReactPackage>asList(
      new MainReactPackage(),
      new VectorIconsPackage(),
      new RNGestureHandlerPackage() // add this
  );
}
```

Lastly, open `android/app/src/main/java/com/rnrealworldanimations/MainActivity.java` and import the gesture handler libraries:

```java
package com.rnrealworldanimations;

import com.facebook.react.ReactActivity;
import com.facebook.react.ReactActivityDelegate; // add this
import com.facebook.react.ReactRootView; // add this
import com.swmansion.gesturehandler.react.RNGestureHandlerEnabledRootView; // add this
```

Inside the class, use `ReactActivityDelegate` to enable the gesture handler on the root view:

```java
public class MainActivity extends ReactActivity {

    @Override
    protected String getMainComponentName() {
        return "RNRealworldAnimations";
    }

    // add everything below this line
    @Override
    protected ReactActivityDelegate createReactActivityDelegate() {
      return new ReactActivityDelegate(this, getMainComponentName()) {
        @Override
        protected ReactRootView createRootView() {
          return new RNGestureHandlerEnabledRootView(MainActivity.this);
        }
      };
    }
}
```

Once that’s done, execute `react-native run-android` from your terminal to rebuild and run the project.

### Creating the PinchableImage component

Create a `src/components/PinchableImage.js` file and add the following. React Native Gesture Handler is only responsible for handling the gesture, you still have to write the code for the animations to be applied when those gestures are performed by the user. That’s why we still need to include the `Animated` module:

```javascript
import React, { Component } from "react";
import { Animated, View, Dimensions } from "react-native";
import { PinchGestureHandler, State } from "react-native-gesture-handler";

const { width, height } = Dimensions.get("window"); // used for dealing with image dimensions

export default class PinchableImage extends Component {
  // next: add constructor
}
```

Most of the time, all you will need to extract from the gesture handler module is the gesture you want to handle, and the `State`. In this case, we want to use the `PinchGestureHandler`. But you can also use handlers like `FlingGestureHandler` to implement the swiping gesture that we did earlier, or the `PanGestureHandler` to implement a drag-and-drop. Best of all, you can combine these gesture handlers together to create more complex interactions.

Next, in the `constructor`, we initialize the animated values that we will be using:

- `baseScale` - the default scale value of the image. Here, we’ve set it to `1` so that if it becomes larger than `1` then the user is making the image larger. If it’s less than `1` then the user is making it smaller.
- `pinchScale` - the scale value for the pinch gesture.
- `scale` - the actual scale value to be used for the transform scale.
- `lastScale` - the scale value to be used for updating the `baseScale`.
- `imageScale` - the current scale value of the image.

Here’s the code:

```javascript
// src/components/PinchableImage.js
constructor(props) {
  super(props);
  this.baseScale = new Animated.Value(1);
  this.pinchScale = new Animated.Value(1);
  this.scale = Animated.multiply(this.baseScale, this.pinchScale);
  this.lastScale = 1;
  this.imageScale = 0;
}
```

Throughout the lifecycle of the whole gesture, the `_onPinchHandlerStateChange` method will be executed. Previously, we’ve used `onPanResponderGrant`, `onPanResponderMove` and `onPanResponderRelease`. But when using the gesture handler, it’s consolidated into a single method. Though this time, there’s the [handler state](https://kmagiera.github.io/react-native-gesture-handler/docs/state.html) which is used to respond to different states of the gesture. Here’s how it maps to the PanResponder methods:

- `onPanResponderGrant` - `State.BEGAN`
- `onPanResponderMove` - `State.ACTIVE`
- `onPanResponderRelease` - `State.END`

Additionally, there’s also `State.UNDETERMINED`, `State.CANCELLED` , and `State.FAILED`.

Since we don’t really need to perform anything when the gesture is started or ended, we wrap everything in `State.ACTIVE` condition. Inside it, we calculate the `newImageScale`. We do that by first determining whether the user’s fingers have traveled away from each other or near each other. If `event.nativeEvent.scale` is negative, then it means the user is trying to zoom it out. Otherwise (larger than `0`) the user is zooming it in. We then use it to determine whether to add or subtract the `currentScale` from the `imageScale`. Here’s the code:

```javascript
// src/components/PinchableImage.js
_onPinchHandlerStateChange = event => {
  if (event.nativeEvent.oldState === State.ACTIVE) {
    let currentScale = event.nativeEvent.scale; // the distance travelled by the user's two fingers

    this.lastScale *= event.nativeEvent.scale; // the value we will animated the baseScale to
    this.pinchScale.setValue(1); // everytime the handler is triggered, it's considered as 1 scale value

    // the image's new scale value
    let newImageScale =
      currentScale > 0
        ? this.imageScale + currentScale
        : this.imageScale - currentScale;

    // next: add code for checking if scale is within range
  }
};
```

Next, we determine if the `newImageScale` is within range. If it is, then we use a spring animation to scale the image:

```javascript
if (this.isScaleWithinRange(newImageScale)) {
  Animated.spring(this.baseScale, {
    toValue: this.lastScale,
    bounciness: 20,
    useNativeDriver: true
  }).start();
  this.imageScale = newImageScale; // don't forget to update the imageScale
} else {
  // next: add code for animating the component if it's not within range
}
```

If it’s not within range, then we scale the image back to its original size:

```javascript
this.lastScale = 1;
this.imageScale = 0;
Animated.spring(this.baseScale, {
  toValue: 1,
  bounciness: 20,
  useNativeDriver: true
}).start();
```

The `isScaleWithinRange` function is used to determine whether the scale is still within a specific limit. In this case, we don’t want the image to become smaller than half its size (`0.5`), and we also don’t want it to become five times (`5`) larger than its original size:

```javascript
// src/components/PinchableImage.js
isScaleWithinRange = newImageScale => {
  if (newImageScale >= 0.5 && newImageScale <= 5) {
    return true;
  }
  return false;
};
```

In order for the component to respond to pinch gesture, we wrap everything inside the `PinchGestureHandler` component. This handler only requires you to pass the `onHandlerStateChange` prop which gets executed throughout the lifecycle of the gesture. Inside it are the components you want to animate as the gesture is being performed by the user. Here’s the `render` method:

```javascript
render() {
  return (
    <PinchGestureHandler
      onHandlerStateChange={this._onPinchHandlerStateChange}
    >
      <View style={styles.container}>
        <View style={styles.imageContainer}>
          <Animated.Image
            resizeMode={"contain"}
            source={this.props.image}
            style={[
              styles.pinchableImage,
              {
                transform: [{ scale: this.scale }]
              }
            ]}
          />
        </View>
      </View>
    </PinchGestureHandler>
  );
}
```

You can also [combine multiple gestures](https://kmagiera.github.io/react-native-gesture-handler/docs/interactions.html) by wrapping the components inside multiple gesture handlers.

Next, add the styles:

```javascript
const styles = {
  container: {
    flex: 1
  },
  imageContainer: {
    width: width,
    overflow: "visible",
    alignItems: "center"
  },
  pinchableImage: {
    width: 250,
    height: height / 2 - 100
  }
};
```

### Update the BigCard component

Next, we update the `BigCard` component to use the `PinchableImage` in place of the `Animated.Image` that we currently have:

```javascript
// src/components/BigCard.js
import PinchableImage from "./PinchableImage";
```

Inside the component, we update the final output range of `titleMoveY` to be equal to half of the screen’s size:

```javascript
import { /*previously imported modules*/ Dimensions } from "react-native";
const { height } = Dimensions.get("window");

export default class BigCard extends Component<Props> {
  const titleMoveY = this.titleTranslateYValue.interpolate({
    inputRange: [0, 1],
    outputRange: [0, height / 2]
  });
}
```

All the other animated values (`titleScale` stays the same). Note that we’ve also removed the animated value for the image opacity. This means that image will no longer be animated when the user views the Details screen. The only animation it’s going to perform is when the user uses the pinch gesture.

Next, replace `Animated.Image` with `PinchableImage`:

```javascript
return (
  <View style={styles.container}>
    <View style={styles.imageContainer}>
      <View style={styles.mainContainer}>
        <PinchableImage image={image} />
        ...
      </View>
    </View>
  </View>
);
```

Once that’s done, you should be able to use a pinch gesture on the image to make it larger or smaller.

## Conclusion

That’s it! In this tutorial, you’ve learned how to implement drag-and-drop, swiping, and pinch gestures. You’ve also learned how implementing gesture animations can be quite code-heavy. Good thing there are libraries like the React Native Gesture Handler which makes it easy to respond to user gestures.

The full source code for this tutorial is available on this [GitHub repo](https://github.com/anchetaWern/RNRealworldAnimations). Be sure to switch to the `part3` branch if you want the final output for this part of the series.

_Originally published on the [Pusher blog](https://blog.pusher.com/animation-react-native-part-3/)_