Adding Pinch-to-Zoom Gestures with React Native
Learn how to add pinch-to-zoom functionality to your mobile app in Crowdbotics with the open source React Native package react-native-gesture-handler.
6 October 2021
The Crowdbotics platform leverages AI and systematic code reuse to help you build better applications faster and with less risk.
Don’t take our word for it, see what our customers have to say.
By Industry
Everything you need to be successful with Crowdbotics.
Use & Learn
We are on a mission to radically transform the software development lifecycle.
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.
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.
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.
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.
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.
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
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.