xref: /expo/docs/pages/tutorial/gestures.mdx (revision dfd15ebd)
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