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