Skip to main content
Glama
gestures.md11.6 kB
--- title: Add gestures description: In this tutorial, learn how to implement gestures from React Native Gesture Handler and Reanimated libraries. hasVideoLink: true --- Gestures are a great way to provide an intuitive user experience in an app. The [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/docs/) library provides built-in native components that can handle gestures. It recognizes pan, tap, rotation, and other gestures using the platform's native touch handling system. In this chapter, we'll add two different gestures using this library: - Double tap to scale the size of the emoji sticker and reduce the scale when double tapped again. - Pan to move the emoji sticker around the screen so that the user can place the sticker anywhere on the image. We'll also use the [Reanimated](https://docs.swmansion.com/react-native-reanimated/docs/fundamentals/handling-gestures/) library to animate between gesture states. --- ## Add GestureHandlerRootView To get gesture interactions to work in the app, we'll render `<GestureHandlerRootView>` from `react-native-gesture-handler` at the top of `Index` component. Replace the root level `<View>` component in the **app/(tabs)/index.tsx** with `<GestureHandlerRootView>`. {/* prettier-ignore */} ```tsx app/(tabs)/index.tsx // ... rest of the import statements remain same /* @tutinfo Import <CODE>GestureHandlerRootView</CODE> from <CODE>react-native-gesture-handler-library</CODE>. */import { GestureHandlerRootView } from 'react-native-gesture-handler';/* @end */ return ( /* @tutinfo Replace the root level <CODE>View</CODE> component with <CODE>GestureHandlerRootView</CODE>. */ <GestureHandlerRootView style={styles.container}> /* @end */ {/* ...rest of the code remains */} /* @tutinfo */ </GestureHandlerRootView> /* @end */ ) } ``` ## Use animated components An `Animated` component looks at the `style` prop of the component and determines which values to animate and apply updates to create an animation. Reanimated exports animated components such as `<Animated.View>`, `<Animated.Text>`, or `<Animated.ScrollView>`. We will apply animations to the `<Animated.Image>` component to make a double tap gesture work. 1. Open the **EmojiSticker.tsx** file in the **components** directory. Inside it, import `Animated` from the `react-native-reanimated` library to use animated components. 2. Replace the `Image` component with `<Animated.Image>`. {/* prettier-ignore */} ```tsx components/EmojiSticker.tsx /* @tutinfo Import <CODE>Animated</CODE> from <CODE>react-native-reanimated</CODE>. */import Animated from 'react-native-reanimated';/* @end */ type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; return ( <View style={{ top: -350 }}> /* @tutinfo Replace the <CODE>Image</CODE> component with <CODE>Animated.Image</CODE>. */ /* @end */ </View> ); } ``` > For a complete reference of the animated component API, see [React Native Reanimated](https://docs.swmansion.com/react-native-reanimated/docs/core/createAnimatedComponent) documentation. ## Add a tap gesture React Native Gesture Handler allows us to add behavior when it detects touch input, like a double tap event. In the **EmojiSticker.tsx** file: 1. Import `Gesture` and `GestureDetector` from `react-native-gesture-handler`. 2. To recognize the tap on the sticker, import `useAnimatedStyle`, `useSharedValue`, and `withSpring` from `react-native-reanimated` to animate the style of the `<Animated.Image>`. 3. Inside the `EmojiSticker` component, create a reference called `scaleImage` using the `useSharedValue()` hook. It will take the value of `imageSize` as its initial value. ```tsx components/EmojiSticker.tsx // ...rest of the import statements remain same const scaleImage = useSharedValue(imageSize); return ( // ...rest of the code remains same ) } ``` Creating a shared value using the `useSharedValue()` hook has many advantages. It helps to mutate data and runs animations based on the current value. We can access and modify the shared value using the `.value` property. We'll create a `doubleTap` object to scale the initial value and use `Gesture.Tap()` to animate the transition while scaling the sticker image. To determine the number of taps required, we'll add `numberOfTaps()`. Create the following object in the `EmojiSticker` component: ```tsx components/EmojiSticker.tsx const doubleTap = Gesture.Tap() .numberOfTaps(2) .onStart(() => { if (scaleImage.value !== imageSize * 2) { scaleImage.value = scaleImage.value * 2; } else { scaleImage.value = Math.round(scaleImage.value / 2); } }); ``` To 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()` function provided by `react-native-reanimated`. On the sticker image, we'll use the `useAnimatedStyle()` hook to create a style object. This will help us to update styles using shared values when the animation happens. We'll also scale the size of the image 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: ```tsx components/EmojiSticker.tsx const imageStyle = useAnimatedStyle(() => { return { width: withSpring(scaleImage.value), height: withSpring(scaleImage.value), }; }); ``` Next, wrap the `<Animated.Image>` component with the `<GestureDetector>` and modify the `style` prop on the `<Animated.Image>` to pass the `imageStyle`. {/* prettier-ignore */} ```tsx components/EmojiSticker.tsx type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; const scaleImage = useSharedValue(imageSize); const doubleTap = Gesture.Tap() .numberOfTaps(2) .onStart(() => { if (scaleImage.value !== imageSize * 2) { scaleImage.value = scaleImage.value * 2; } else { scaleImage.value = Math.round(scaleImage.value / 2); } }); const imageStyle = useAnimatedStyle(() => { return { width: withSpring(scaleImage.value), height: withSpring(scaleImage.value), }; }); return ( <View style={{ top: -350 }}> /* @tutinfo Wrap the <CODE>Animated.Image</CODE> component with <CODE>GestureDetector</CODE>.*/ <GestureDetector gesture={doubleTap}>/* @end */ <Animated.Image source={stickerSource} resizeMode="contain" style={/* @tutinfo Modify the <CODE>style</CODE> prop on the <CODE>Animated.Image</CODE> to pass the <CODE>imageStyle</CODE>. */[imageStyle, { width: imageSize, height: imageSize }]/* @end */} /> /* @tutinfo */ </GestureDetector> /* @end */ </View> ); } ``` In the above snippet, the `gesture` prop takes the value of the `doubleTap` to trigger a gesture when a user double-taps the sticker image. Let's take a look at our app on Android, iOS and the web: > For a complete reference of the tap gesture API, see the [React Native Gesture Handler](https://docs.swmansion.com/react-native-gesture-handler/docs/gestures/tap-gesture) documentation. ## Add a pan gesture To recognize a dragging gesture on the sticker and to track its movement, we'll use a pan gesture. In the **components/EmojiSticker.tsx**: 1. Create two new shared values: `translateX` and `translateY`. 2. Replace the `<View>` with the `<Animated.View>` component. {/* prettier-ignore */} ```tsx components/EmojiSticker.tsx const scaleImage = useSharedValue(imageSize); /* @tutinfo Add <CODE>translateX</CODE> and <CODE>translateY</CODE> shared values.*/ const translateX = useSharedValue(0); const translateY = useSharedValue(0); /* @end */ // ...rest of the code remains same return ( /* @tutinfo Replace the <CODE>View</CODE> component with <CODE>Animated.View</CODE>. */<Animated.View style={{ top: -350 }}>/* @end */ <GestureDetector gesture={doubleTap}> {/* ...rest of the code remains same */} </GestureDetector> /* @tutinfo */</Animated.View>/* @end */ ); } ``` Let's learn what the above code does: - The translation values defined will move the sticker around the screen. Since the sticker moves along both axes, we need to track the X and Y values. - In the `useSharedValue()` hooks, we have set both translation variables to have an initial position of `0`. This is the sticker's initial position and a starting point. This value sets the sticker's initial position when the gesture starts. In the previous step, we triggered the `onStart()` callback for the tap gesture chained to the `Gesture.Tap()` method. For the pan gesture, specify an `onChange()` callback, which runs when the gesture is active and moving. 1. Create a `drag` object to handle the pan gesture. The `onChange()` callback accepts `event` as a parameter. `changeX` and `changeY` properties hold the change in position since the last event. and update the values stored in `translateX` and `translateY`. 2. Define the `containerStyle` object using the `useAnimatedStyle()` hook. It will return an array of transforms. For the `<Animated.View>` 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. ```tsx components/EmojiSticker.tsx const drag = Gesture.Pan().onChange(event => { translateX.value += event.changeX; translateY.value += event.changeY; }); const containerStyle = useAnimatedStyle(() => { return { transform: [ { translateX: translateX.value, }, { translateY: translateY.value, }, ], }; }); ``` Next, inside the JSX code: 1. Update the `<EmojiSticker>` component so that the `<GestureDetector>` component becomes the top-level component. 2. Add the `containerStyle` on the `<Animated.View>` component to apply the transform styles. {/* prettier-ignore */} ```tsx components/EmojiSticker.tsx type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; const scaleImage = useSharedValue(imageSize); const translateX = useSharedValue(0); const translateY = useSharedValue(0); const doubleTap = Gesture.Tap() .numberOfTaps(2) .onStart(() => { if (scaleImage.value !== imageSize * 2) { scaleImage.value = scaleImage.value * 2; } else { scaleImage.value = Math.round(scaleImage.value / 2); } }); const imageStyle = useAnimatedStyle(() => { return { width: withSpring(scaleImage.value), height: withSpring(scaleImage.value), }; }); const drag = Gesture.Pan().onChange(event => { translateX.value += event.changeX; translateY.value += event.changeY; }); const containerStyle = useAnimatedStyle(() => { return { transform: [ { translateX: translateX.value, }, { translateY: translateY.value, }, ], }; }); return ( /* @tutinfo Wrap all components inside <CODE>GestureDetector</CODE>. */<GestureDetector gesture={drag}>/* @end */ <Animated.View style={/* @tutinfo Add <CODE>containerStyle</CODE> to the <CODE>Animated.View</CODE> style prop. */[containerStyle, { top: -350 }]/* @end */}> <GestureDetector gesture={doubleTap}> </GestureDetector> </Animated.View> /* @tutinfo */</GestureDetector>/* @end */ ); } ```` Let's take a look at our app on Android, iOS and the web: ## Summary

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jaksm/expo-docs-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server