1--- 2title: Add gestures 3--- 4 5import { SnackInline, Terminal } from '~/ui/components/Snippet'; 6import Video from '~/components/plugins/Video'; 7import { LinkBase } from '~/ui/components/Text'; 8import ImageSpotlight from '~/components/plugins/ImageSpotlight'; 9 10Gestures are a great way to provide an intuitive user experience in an app. The <LinkBase href="https://docs.swmansion.com/react-native-gesture-handler/docs/" openInNewTab>React Native Gesture Handler library</LinkBase> 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. 11 12In this chapter, we are going to add two different gestures using the React Native Gesture Handler library: 13 14- Double tap to scale the size of the emoji sticker. 15- Pan to move the emoji sticker around the screen so that the user can place the sticker anywhere on the image. 16 17## Step 1: Install and configure libraries 18 19The 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 <LinkBase href="https://docs.swmansion.com/react-native-reanimated/docs/" openInNewTab>Reanimated library</LinkBase>. 20 21To install them, stop the development server by pressing <kbd>Ctrl</kbd> + <kbd>c</kbd> and run the following command in the terminal: 22 23<Terminal cmd={['$ npx expo install react-native-gesture-handler react-native-reanimated']} /> 24 25Next, also install `@babel/plugin-proposal-export-namespace-from`, which is required to configure the Reanimated library: 26 27<Terminal cmd={['$ npm install -D @babel/plugin-proposal-export-namespace-from ']} /> 28 29Then, add Reanimated's Babel plugin to **babel.config.js**: 30 31{/* prettier-ignore */} 32```jsx babel.config.js 33module.exports = function (api) { 34 api.cache(true); 35 return { 36 presets: ['babel-preset-expo'], 37 /* @info Add the plugins array and inside it, add the plugins.*/ 38 plugins: [ 39 "@babel/plugin-proposal-export-namespace-from", 40 "react-native-reanimated/plugin", 41 ], 42 /* @end */ 43 }; 44}; 45``` 46 47Now, start the development server again: 48 49<Terminal cmd={['$ npx expo start -c']} /> 50 51> **Tip**: We are using `-c` option here because we modified the **babel.config.js** file. 52 53To 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"). 54 55To accomplish this, replace the root level `<View>` component in the **App.js** with `<GestureHandlerRootView>`. 56 57{/* prettier-ignore */} 58```jsx App.js 59/* @info Import GestureHandlerRootView from react-native-gesture-handler-library. */import { GestureHandlerRootView } from "react-native-gesture-handler"; /* @end */ 60 61export default function App() { 62 return ( 63 /* @info Replace the root level View component with GestureHandlerRootView. */<GestureHandlerRootView style={styles.container}> /* @end */ 64 /* ...rest of the code remains */ 65 /* @info */</GestureHandlerRootView>/* @end */ 66 ) 67} 68``` 69 70## Step 2: Create animated components 71 72Open the **EmojiSticker.js** file in the **components** directory. Inside it, import `Animated` from the `react-native-reanimated` library to create animated components. 73 74```jsx EmojiSticker.js 75import Animated from 'react-native-reanimated'; 76``` 77 78To 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. 79 80```jsx EmojiSticker.js 81// after import statements, add the following line 82 83const AnimatedImage = Animated.createAnimatedComponent(Image); 84``` 85 86The `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. 87 88Replace the `<Image>` component with `<AnimatedImage>`. 89 90{/* prettier-ignore */} 91```jsx EmojiSticker.js 92export default function EmojiSticker({ imageSize, stickerSource }) { 93 return ( 94 <View style={{ top: -350 }}> 95 /* @info Replace the Image component with AnimatedImage. */<AnimatedImage /* @end */ 96 source={stickerSource} 97 resizeMode="contain" 98 style={{ width: imageSize, height: imageSize }} 99 /> 100 </View> 101 ); 102} 103``` 104 105## Step 3: Add a tap gesture 106 107React Native Gesture Handler allows us to add behavior when it detects touch input, like a double tap event. 108 109In 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. 110 111```jsx EmojiSticker.js 112import { TapGestureHandler } from 'react-native-gesture-handler'; 113import Animated, { 114 useAnimatedStyle, 115 useSharedValue, 116 useAnimatedGestureHandler, 117 withSpring, 118} from 'react-native-reanimated'; 119``` 120 121Inside the `<EmojiSticker>` component, create a reference called `scaleImage` using the `useSharedValue()` hook. It will take the value of `imageSize` as its initial value. 122 123```jsx EmojiSticker.js 124const scaleImage = useSharedValue(imageSize); 125``` 126 127Creating 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. 128 129Create the following function in the `<EmojiSticker>` component: 130 131```jsx EmojiSticker.js 132const onDoubleTap = useAnimatedGestureHandler({ 133 onActive: () => { 134 if (scaleImage.value) { 135 scaleImage.value = scaleImage.value * 2; 136 } 137 }, 138}); 139``` 140 141To 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`. 142 143The `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: 144 145```jsx EmojiSticker.js 146const imageStyle = useAnimatedStyle(() => { 147 return { 148 width: withSpring(scaleImage.value), 149 height: withSpring(scaleImage.value), 150 }; 151}); 152``` 153 154Next, wrap the `<AnimatedImage>` component that displays the sticker on the screen with the `<TapGestureHandler>` component. 155 156<SnackInline 157label="Handling tap gesture" 158templateId="tutorial/06-gestures/App" 159dependencies={['expo-image-picker', '@expo/vector-icons/FontAwesome', '@expo/vector-icons', 'expo-status-bar', '@expo/vector-icons/MaterialIcons', 'react-native-gesture-handler', 'react-native-reanimated']} 160files={{ 161 'assets/images/background-image.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/503001f14bb7b8fe48a4e318ad07e910', 162 'assets/images/emoji1.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/be9751678c0b3f9c6bf55f60de815d30', 163 'assets/images/emoji2.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/7c0d14b79e134d528c5e0801699d6ccf', 164 'assets/images/emoji3.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/d713e2de164764c2ab3db0ab4e40c577', 165 'assets/images/emoji4.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/ac2163b98a973cb50bfb716cc4438f9a', 166 'assets/images/emoji5.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/9cc0e2ff664bae3af766b9750331c3ad', 167 'assets/images/emoji6.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/ce614cf0928157b3f7daa3cb8e7bd486', 168 'components/ImageViewer.js': 'tutorial/02-image-picker/ImageViewer.js', 169 'components/Button.js': 'tutorial/03-button-options/Button.js', 170 'components/CircleButton.js': 'tutorial/03-button-options/CircleButton.js', 171 'components/IconButton.js': 'tutorial/03-button-options/IconButton.js', 172 'components/EmojiPicker.js': 'tutorial/04-modal/EmojiPicker.js', 173 'components/EmojiList.js': 'tutorial/05-emoji-list/EmojiList.js', 174 'components/EmojiSticker.js': 'tutorial/06-gestures/EmojiSticker.js', 175}}> 176 177{/* prettier-ignore */} 178```jsx 179export default function EmojiSticker({ imageSize, stickerSource }) { 180 // ...rest of the code remains same 181 return ( 182 <View style={{ top: -350 }}> 183 /* @info Wrap the AnimatedImage component with TapGestureHandler*/ <TapGestureHandler onGestureEvent={onDoubleTap} numberOfTaps={2}>/* @end */ 184 <AnimatedImage 185 source={stickerSource} 186 resizeMode="contain" 187 /* @info Modify the style prop on the AnimatedImage to pass the imageStyle.*/ style={[imageStyle, { width: imageSize, height: imageSize }]} /* @end */ 188 /> 189 /* @info */</TapGestureHandler>/* @end */ 190 </View> 191 ); 192} 193``` 194 195</SnackInline> 196 197In 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. 198 199Let's take a look at our app on iOS, Android and the web: 200 201<Video file="tutorial/tap-gesture.mp4" /> 202 203> For a complete reference on the tap gesture API, refer to the <LinkBase href="https://docs.swmansion.com/react-native-gesture-handler/docs/api/gestures/tap-gesture" openInNewTab>React Native Gesture Handler</LinkBase> documentation. 204 205## Step 4: Add a pan gesture 206 207A pan gesture allows recognizing a dragging gesture and tracking its movement. We will use this gesture handler to drag the sticker across the image. 208 209In the **EmojiSticker.js**, import `PanGestureHandler` from the `react-native-gesture-handler` library. 210 211{/* prettier-ignore */} 212```jsx EmojiSticker.js 213import { /* @info */ PanGestureHandler,/* @end */ TapGestureHandler} from "react-native-gesture-handler"; 214``` 215 216Create an `<AnimatedView>` component using the `Animated.createAnimatedComponent()` method. Then use it to wrap the `<TapGestureHandler>` component by replacing the `<View>` component. 217 218{/* prettier-ignore */} 219```jsx EmojiSticker.js 220// ...rest of the code remains same 221/* @info */ const AnimatedView = Animated.createAnimatedComponent(View); /* @end */ 222 223export default function EmojiSticker({ imageSize, stickerSource }) { 224 // ...rest of the code remains same 225 226 return ( 227 /* @info Replace the View component with AnimatedView */<AnimatedView style={{ top: -350 }}>/* @end */ 228 <TapGestureHandler onGestureEvent={onDoubleTap} numberOfTaps={2}> 229 {/* ...rest of the code remains same */} 230 </TapGestureHandler> 231 /* @info */</AnimatedView>/* @end */ 232 ); 233} 234``` 235 236Now, create two new shared values: `translateX` and `translateY`. 237 238```jsx EmojiSticker.js 239export default function EmojiSticker({ imageSize, stickerSource }) { 240 const translateX = useSharedValue(0); 241 const translateY = useSharedValue(0); 242 243 // ...rest of the code remains same 244} 245``` 246 247These 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. 248 249In 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. 250 251In 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: 252 253- `onStart()`: when the gesture starts or is at its initial position 254- `onActive()`: when the gesture is active and is moving 255 256Create an `onDrag()` method to handle the pan gesture. 257 258{/* prettier-ignore */} 259```jsx EmojiSticker.js 260const onDrag = useAnimatedGestureHandler({ 261 onStart: (event, context) => { 262 context.translateX = translateX.value; 263 context.translateY = translateY.value; 264 }, 265 onActive: (event, context) => { 266 translateX.value = event.translationX + context.translateX; 267 translateY.value = event.translationY + context.translateY; 268 }, 269}); 270``` 271 272Both 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`. 273 274Next, 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. 275 276{/* prettier-ignore */} 277```jsx EmojiSticker.js 278const containerStyle = useAnimatedStyle(() => { 279 return { 280 transform: [ 281 { 282 translateX: translateX.value, 283 }, 284 { 285 translateY: translateY.value, 286 }, 287 ], 288 }; 289}); 290``` 291 292Then 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. 293 294<SnackInline 295label="Handle pan gesture" 296templateId="tutorial/06-gestures/App" 297dependencies={['expo-image-picker', '@expo/vector-icons/FontAwesome', '@expo/vector-icons', 'expo-status-bar', '@expo/vector-icons/MaterialIcons', 'react-native-gesture-handler', 'react-native-reanimated']} 298files={{ 299 'assets/images/background-image.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/503001f14bb7b8fe48a4e318ad07e910', 300 'assets/images/emoji1.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/be9751678c0b3f9c6bf55f60de815d30', 301 'assets/images/emoji2.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/7c0d14b79e134d528c5e0801699d6ccf', 302 'assets/images/emoji3.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/d713e2de164764c2ab3db0ab4e40c577', 303 'assets/images/emoji4.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/ac2163b98a973cb50bfb716cc4438f9a', 304 'assets/images/emoji5.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/9cc0e2ff664bae3af766b9750331c3ad', 305 'assets/images/emoji6.png': 'https://snack-code-uploads.s3.us-west-1.amazonaws.com/~asset/ce614cf0928157b3f7daa3cb8e7bd486', 306 'components/ImageViewer.js': 'tutorial/02-image-picker/ImageViewer.js', 307 'components/Button.js': 'tutorial/03-button-options/Button.js', 308 'components/CircleButton.js': 'tutorial/03-button-options/CircleButton.js', 309 'components/IconButton.js': 'tutorial/03-button-options/IconButton.js', 310 'components/EmojiPicker.js': 'tutorial/04-modal/EmojiPicker.js', 311 'components/EmojiList.js': 'tutorial/05-emoji-list/EmojiList.js', 312 'components/EmojiSticker.js': 'tutorial/06-gestures/CompleteEmojiSticker.js', 313}}> 314 315{/* prettier-ignore */} 316```jsx 317export default function EmojiSticker({ imageSize, stickerSource }) { 318 // rest of the code 319 320 return ( 321 /* @info Wrap all components inside PanGestureHandler. */<PanGestureHandler onGestureEvent={onDrag}>/* @end */ 322 /* @info Add containerStyle to the AnimatedView style prop. */<AnimatedView style={[containerStyle, { top: -350 }]}>/* @end */ 323 <TapGestureHandler onGestureEvent={onDoubleTap} numberOfTaps={2}> 324 <AnimatedImage 325 source={stickerSource} 326 resizeMode="contain" 327 style={[imageStyle, { width: imageSize, height: imageSize }]} 328 /> 329 </TapGestureHandler> 330 </AnimatedView> 331 /* @info */</PanGestureHandler>/* @end */ 332 ); 333} 334``` 335 336</SnackInline> 337 338Let's take a look at our app on iOS, Android and the web: 339 340<Video file="tutorial/pan-gesture.mp4" /> 341 342## Up next 343 344We successfully implemented pan and tap gestures. 345 346In 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](/tutorial/screenshot). 347