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