React Native concept art

React Native's built-in touch Gesture Responder system has given us all some performance problems on both iOS and Android platforms. Using the open-source solution react-native-gesture-handler is a great way to overcome this and add gestures in our React Native apps.

Let us add this feature to the Instagram clone Feed screen. If you have been following this series so far, you'll know which screen I am talking about right now, and you'll have already installed the npm package required to use this library as well as other settings to make it work. You can skip the first section below called installing dependencies.

If you are reading about this for the first time, do not worry. There is nothing new, and in the next section, I have added all the necessary steps to install react-native-gesture-handler and make it work with your React Native app. The gesture that I'm going to cover is a feature called "pinch to zoom." It requires two user fingers to use a pinch gesture to initiate a zoom effect.

Installing dependencies

To start, make sure you install the latest version of react-native-gesture-handler that supports React Native 60+ apps. If you are working with lower versions of React Naive, please give the official documentation a read to follow correct methods to set this library.

Note: If you have installed and set up the react-navigation library as part of your app, you do not have to install and set up the Gesture Handler library again.

yarn add react-native-gesture-handler

For the current demo, since you are using the react-native CLI, only Android users have to add the following configuration MainActivity.java file.

package com.swipegesturesdemo;

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 {

 /**
 * Returns the name of the main component registered from JavaScript. This is used to schedule
 * rendering of the component.
 */
 @Override
 protected String getMainComponentName() {
 return "swipeGesturesDemo";
 }

 @Override
 protected ReactActivityDelegate createReactActivityDelegate() {
 return new ReactActivityDelegate(this, getMainComponentName()) {
 @Override
 protected ReactRootView createRootView() {
 return new RNGestureHandlerEnabledRootView(MainActivity.this);
 }
 };
 }
}

For iOS users, navigate inside ios/ directory from the terminal and run pod install.

Everything is set up, so all you have to do is run the build command again, such as for iOS: react-native run-ios and for Android: react-native run-android.

That's all for setup.

Creating a PinchGestureHandler component

A pinch gesture is a continuous gesture that is recognized with the help of PinchGestureHandler from react-native-gesture-handler. This handler tracks the distance between two fingers and uses that information to scale or zoom on the content. It gets activated when the fingers are placed on the screen and when their position changes.

Since the Feed screen right now has many posts, each containing an image with some text, let's create a separate component file called PinchableBox.js inside the components/ directory.

Import the following dependencies that are required to create this component. The Animated library is required to scale and transform the image from its given width and height. Another thing you have to notice is the State object that is also known as the handler state.

import React from 'react'
import { Animated, Dimensions } from 'react-native'
import { PinchGestureHandler, State } from 'react-native-gesture-handler'

Next, define a functional component called PinchableBox and export it.

const PinchableBox = () => {
  // ... rest of the code
}

export default PinchableBox

That's it for setting up the component.

Adding image URI as a prop

The source of the image is going to come from the Feed screen component. In this section, let us replace the Image component in the Feed screen with the PinchableBox component.

The PinchableBox component is going to have an Animated.Image which is going to serve the purpose of displaying images in each post on Feed screen as well as perform scale animations.

In PinchableBox.js file modify the component like the following snippet. The prop imageUri is what this component expects and it is used as the source URI of the image to display.

const PinchableBox = ({ imageUri }) => {
  return (
    <Animated.Image
      source={{ uri: imageUri }}
      style={{
        width: screen.width,
        height: 300,
        transform: [{ scale: 1 }]
      }}
      resizeMode='contain'
    />
  )
}

Setting the value of the scale to one is going to display the image as usual. Also, the width of the image component is calculated according to the screen of the device's width, using Dimensions from react-native. Add the following line in the same snippet above the functional component.

const screen = Dimensions.get('window')

Next, go the screens/Feed.js and import the PinchableBox component.

// ... rest of the import statements
import PinchableBox from '../components/PinchableBox'

Next, replace the existing Image component in the render method with the following snippet:

<PinchableBox imageUri={item.postPhoto.uri} />

It just needs one prop to be passed for now, and that is the image URI.

Handling state event changes

Animated uses declarative relationships between input and output values. For single values, you can use Animated.Value(). It is required since it's going to be a style property initially.

Inside the PinchableBox component, set scale like below to modify it through Animations.

scale = new Animated.Value(1)

Next, in the style property of Animated.Image, change the value of scale to this.scale.

transform: [{ scale: this.scale }]

Now, wrap the Animated.Image with PinchGestureHandler. This wrapper component is going to have to props.

<PinchGestureHandler
  onGestureEvent={this.onPinchEvent}
  onHandlerStateChange={this.onPinchStateChange}>
  <Animated.Image
    source={{ uri: imageUri }}
    style={{
      width: screen.width,
      height: 300,
      transform: [{ scale: this.scale }]
    }}
    resizeMode='contain'
  />
</PinchGestureHandler>

Let us define the onPinchEvent first, before the return statement. This event is going to be an Animated event. This way gestures can directly map to animated values. The animated value to be used here is scale.

Passing useNativeDriver as boolean true allows the animations to happen on the native thread instead of JavaScript thread. This helps with performance.

onPinchEvent = Animated.event(
  [
    {
      nativeEvent: { scale: this.scale }
    }
  ],
  {
    useNativeDriver: true
  }
)

Let us define the handler method onPinchStateChange that handles the state change when the gesture is over. Each gesture handler is assigned a state that changes when a new touch event occurs.

There are different possible states for every handler, but for the current gesture handler, ACTIVE is used to check whether the event is still active or not. To access these states, the object is required to import from the library itself.

The Animated.spring on scale property has toValue set to 1 which is the initial scale value.

onPinchStateChange = event => {
  if (event.nativeEvent.oldState === State.ACTIVE) {
    Animated.spring(this.scale, {
      toValue: 1,
      useNativeDriver: true
    }).start()
  }
}

Here is the output of all code written so far.

GIF of user zooming in via a pinch motion

Here is the code for the complete PinchableBox component.

import React from 'react'
import { Animated, Dimensions } from 'react-native'
import { PinchGestureHandler, State } from 'react-native-gesture-handler'

const screen = Dimensions.get('window')

const PinchableBox = ({ imageUri }) => {
  scale = new Animated.Value(1)

  onPinchEvent = Animated.event(
    [
      {
        nativeEvent: { scale: this.scale }
      }
    ],
    {
      useNativeDriver: true
    }
  )

  onPinchStateChange = event => {
    if (event.nativeEvent.oldState === State.ACTIVE) {
      Animated.spring(this.scale, {
        toValue: 1,
        useNativeDriver: true
      }).start()
    }
  }

  return (
    <PinchGestureHandler
      onGestureEvent={this.onPinchEvent}
      onHandlerStateChange={this.onPinchStateChange}>
      <Animated.Image
        source={{ uri: imageUri }}
        style={{
          width: screen.width,
          height: 300,
          transform: [{ scale: this.scale }]
        }}
        resizeMode='contain'
      />
    </PinchGestureHandler>
  )
}

export default PinchableBox

Conclusion

The Feed screen implemented is from one of the templates from Crowdbotics' react-native collection. We use UI Kitten for our latest template libraries. You can modify the screen further or add another component that takes care of counting likes or comments. Find more about how to create custom screens like this from our open source project here.