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