Gestures are a great way to provide an intuitive user experience in an app. The React Native Gesture Handler library provides built-in native components that can handle gestures. It uses the platform's native touch handling system to recognize pan, tap, rotation, and other gestures.
In this chapter, we are going to add two different gestures using the React Native Gesture Handler library:
1
The React Native Gesture Handler library provides a way to interact with the native platform's gesture response system. To animate between gesture states, we will use the Reanimated library.
To install them, stop the development server by pressing Ctrl + c and run the following command in the terminal:
-Â
npx expo install react-native-gesture-handler react-native-reanimated
Next, also install @babel/plugin-proposal-export-namespace-from
, which is required to configure the Reanimated library:
-Â
npm install -D @babel/plugin-proposal-export-namespace-from
Then, add Reanimated's Babel plugin to babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: ['babel-preset-expo'],
plugins: [
"@babel/plugin-proposal-export-namespace-from",
"react-native-reanimated/plugin",
],
};
};
Now, start the development server again:
-Â
npx expo start -c
Tip: We are using
-c
option here because we modified the babel.config.js file.
To get gesture interactions to work in the app, we'll render <GestureHandlerRootView>
from react-native-gesture-handler
to wrap the top-level component of our app (also known as the "root component").
To accomplish this, replace the root level <View>
component in the App.js with <GestureHandlerRootView>
.
import { GestureHandlerRootView } from "react-native-gesture-handler";
export default function App() {
return (
<GestureHandlerRootView style={styles.container}>
/* ...rest of the code remains */
</GestureHandlerRootView>
)
}
2
Open the EmojiSticker.js file in the components directory. Inside it, import Animated
from the react-native-reanimated
library to create animated components.
import Animated from 'react-native-reanimated';
To make a double tap gesture work, we will apply animations to the <Image>
component by passing it as an argument to the Animated.createAnimatedComponent()
method.
// after import statements, add the following line
const AnimatedImage = Animated.createAnimatedComponent(Image);
The createAnimatedComponent()
can wrap any component. It looks at the style
prop of the component, determines which value is animated, and then applies updates to create an animation.
Replace the <Image>
component with <AnimatedImage>
.
export default function EmojiSticker({ imageSize, stickerSource }) {
return (
<View style={{ top: -350 }}>
<AnimatedImage
source={stickerSource}
resizeMode="contain"
style={{ width: imageSize, height: imageSize }}
/>
</View>
);
}
3
React Native Gesture Handler allows us to add behavior when it detects touch input, like a double tap event.
In the EmojiSticker.js file, import TapGestureHandler
from react-native-gesture-handler
and the hooks below from react-native-reanimated
.
These hooks will animate the style on the <AnimatedImage>
component for the sticker when the tap gesture is recognized.
import { TapGestureHandler } from 'react-native-gesture-handler';
import Animated, {
useAnimatedStyle,
useSharedValue,
useAnimatedGestureHandler,
withSpring,
} from 'react-native-reanimated';
Inside the <EmojiSticker>
component, create a reference called scaleImage
using the useSharedValue()
hook. It will take the value of imageSize
as its initial value.
const scaleImage = useSharedValue(imageSize);
Creating a shared value using the useSharedValue()
hook has many advantages. It helps to mutate a piece of data and allows running animations based on the current value.
A shared value can be accessed and modified using the .value
property. It will scale the initial value of scaleImage
so that when a user double-taps the sticker,
it scales to twice its original size. To do this, we will create a function and call it onDoubleTap()
, and this function will use the useAnimatedGestureHandler()
hook
to animate the transition while scaling the sticker image.
Create the following function in the <EmojiSticker>
component:
const onDoubleTap = useAnimatedGestureHandler({
onActive: () => {
if (scaleImage.value !== imageSize * 2) {
scaleImage.value = scaleImage.value * 2;
}
},
});
To animate the transition, let's use a spring-based animation. This will make it feel alive because it's based on the real-world physics of a spring.
We will use the withSpring()
hook provided by react-native-reanimated
.
The useAnimatedStyle()
hook from react-native-reanimated
is used to create a style object that will be applied to the sticker image.
It will update styles using the shared values when the animation happens. In this case, we are scaling the size of the image,
which is done by manipulating the width
and height
properties. The initial values of these properties are set to imageSize
.
Create an imageStyle
variable and add it to the EmojiSticker
component:
const imageStyle = useAnimatedStyle(() => {
return {
width: withSpring(scaleImage.value),
height: withSpring(scaleImage.value),
};
});
Next, wrap the <AnimatedImage>
component that displays the sticker on the screen with the <TapGestureHandler>
component.
export default function EmojiSticker({ imageSize, stickerSource }) {
// ...rest of the code remains same
return (
<View style={{ top: -350 }}>
<TapGestureHandler onGestureEvent={onDoubleTap} numberOfTaps={2}>
<AnimatedImage
source={stickerSource}
resizeMode="contain"
style={[imageStyle, { width: imageSize, height: imageSize }]}
/>
</TapGestureHandler>
</View>
);
}
In the above snippet, the onGestureEvent
prop takes the value of the onDoubleTap()
function and triggers it when a user taps the sticker image.
The numberOfTaps
prop determines how many taps are required.
Let's take a look at our app on iOS, Android and the web:
For a complete reference on the tap gesture API, refer to the React Native Gesture Handler documentation.
4
A pan gesture allows recognizing a dragging gesture and tracking its movement. We will use this gesture handler to drag the sticker across the image.
In the EmojiSticker.js, import PanGestureHandler
from the react-native-gesture-handler
library.
import { PanGestureHandler, TapGestureHandler} from "react-native-gesture-handler";
Create an <AnimatedView>
component using the Animated.createAnimatedComponent()
method. Then use it to wrap the <TapGestureHandler>
component by replacing the <View>
component.
// ...rest of the code remains same
const AnimatedView = Animated.createAnimatedComponent(View);
export default function EmojiSticker({ imageSize, stickerSource }) {
// ...rest of the code remains same
return (
<AnimatedView style={{ top: -350 }}>
<TapGestureHandler onGestureEvent={onDoubleTap} numberOfTaps={2}>
{/* ...rest of the code remains same */}
</TapGestureHandler>
</AnimatedView>
);
}
Now, create two new shared values: translateX
and translateY
.
export default function EmojiSticker({ imageSize, stickerSource }) {
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
// ...rest of the code remains same
}
These translation values will move the sticker around the screen. Since the sticker moves along both axes, we need to track the X and Y values separately.
In the useSharedValue()
hooks, we have set both translation variables to have an initial position of 0
.
This means that the position the sticker is initially placed is considered the starting point. This value sets the initial position of the sticker when the gesture starts.
In the previous step, we triggered the onActive()
callback for the tap gesture inside the useAnimatedGestureHandler()
function.
Similarly, for the pan gesture, we have to specify two callbacks:
onStart()
: when the gesture starts or is at its initial positiononActive()
: when the gesture is active and is movingCreate an onDrag()
method to handle the pan gesture.
const onDrag = useAnimatedGestureHandler({
onStart: (event, context) => {
context.translateX = translateX.value;
context.translateY = translateY.value;
},
onActive: (event, context) => {
translateX.value = event.translationX + context.translateX;
translateY.value = event.translationY + context.translateY;
},
});
Both the onStart
and onActive
methods accept event
and context
as parameters. In the onStart
method, we'll use context
to store the initial values of translateX
and translateY
. In the onActive
callback, we'll use the event
to get the current position of the pan gesture and context
to get the previously stored values of translateX
and translateY
.
Next, use the useAnimatedStyle()
hook to return an array of transforms.For the <AnimatedView>
component, we need to set the transform
property to the translateX
and translateY
values. This will change the sticker's position when the gesture is active.
const containerStyle = useAnimatedStyle(() => {
return {
transform: [
{
translateX: translateX.value,
},
{
translateY: translateY.value,
},
],
};
});
Then add the containerStyle
from the above snippet on the <AnimatedView>
component to apply the transform styles.
Also, update the <EmojiSticker>
component so that the <PanGestureHandler>
component becomes the top-level component.
export default function EmojiSticker({ imageSize, stickerSource }) {
// rest of the code
return (
<PanGestureHandler onGestureEvent={onDrag}>
<AnimatedView style={[containerStyle, { top: -350 }]}>
<TapGestureHandler onGestureEvent={onDoubleTap} numberOfTaps={2}>
<AnimatedImage
source={stickerSource}
resizeMode="contain"
style={[imageStyle, { width: imageSize, height: imageSize }]}
/>
</TapGestureHandler>
</AnimatedView>
</PanGestureHandler>
);
}
Let's take a look at our app on iOS, Android and the web:
We successfully implemented pan and tap gestures.
In the next chapter, we'll learn how to take a screenshot of the image and the sticker, and save it on the device's library.