添加手势
编辑页面
在本教程中,了解如何实现来自 React Native Gesture Handler 和 Reanimated 库的手势。
For the complete documentation index, see llms.txt. Use this Use this file to discover all available pages.
手势是在应用中提供直观用户体验的绝佳方式。React Native Gesture Handler 库提供了内置的原生组件来处理手势。它使用平台的原生触摸处理系统来识别平移、点击、旋转以及其他手势。在本章中,我们将使用这个库添加两种不同的手势:
- 双击以放大 emoji 贴纸的尺寸,并在再次双击时缩小尺寸。
- 平移以在屏幕上移动 emoji 贴纸,这样用户就可以将贴纸放置在图片中的任意位置。
我们还会使用 Reanimated 库在手势状态之间制作动画过渡。

使用 React Native Gesture Handler 和 Reanimated 为 emoji 贴纸添加双击和平移手势。
1
添加 GestureHandlerRootView
为了让手势交互在应用中正常工作,我们将在 Index 组件顶部从 react-native-gesture-handler 渲染 <GestureHandlerRootView>。将 app/(tabs)/index.tsx 中最外层的 <View> 组件替换为 <GestureHandlerRootView>。
// ...其余导入语句保持不变 import { GestureHandlerRootView } from 'react-native-gesture-handler'; export default function Index() { return ( <GestureHandlerRootView style={styles.container}> {/* ...其余代码保持不变 */} </GestureHandlerRootView> ) }
2
使用动画组件
Animated 组件会查看组件的 style 属性,并判断哪些值需要进行动画处理和更新,从而创建动画。Reanimated 导出了诸如 <Animated.View>、<Animated.Text> 或 <Animated.ScrollView> 之类的动画组件。我们将把动画应用到 <Animated.Image> 组件上,以实现双击手势效果。
- 打开 components 目录下的 EmojiSticker.tsx 文件。在其中从
react-native-reanimated库导入Animated,以使用动画组件。 - 将
Image组件替换为<Animated.Image>。
import { ImageSourcePropType, View } from 'react-native'; import Animated from 'react-native-reanimated'; type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; export default function EmojiSticker({ imageSize, stickerSource }: Props) { return ( <View style={{ top: -350 }}> <Animated.Image source={stickerSource} resizeMode="contain" style={{ width: imageSize, height: imageSize }} /> </View> ); }
关于动画组件 API 的完整参考,请查看 React Native Reanimated 文档。
3
添加点击手势
React Native Gesture Handler 允许我们在检测到触摸输入时添加行为,比如双击事件。
在 components/EmojiSticker.tsx 文件中:
- 从
react-native-gesture-handler导入Gesture和GestureDetector。 - 为了识别贴纸上的点击,导入
useAnimatedStyle、useSharedValue和withSpring,以便为<Animated.Image>的样式添加动画。 - 在
EmojiSticker组件内部,使用useSharedValue()钩子创建一个名为scaleImage的引用。它会将imageSize的值作为初始值。
// ...其余导入语句保持不变 import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; export default function EmojiSticker({ imageSize, stickerSource }: Props) { const scaleImage = useSharedValue(imageSize); return ( // ...其余代码保持不变 ) }
使用 useSharedValue() 钩子创建共享值有很多优点。它有助于修改数据,并基于当前值运行动画。我们可以使用 .value 属性来访问和修改共享值。我们将创建一个 doubleTap 对象来缩放初始值,并使用 Gesture.Tap() 在缩放贴纸图片时为过渡添加动画。为了确定所需的点击次数,我们会添加 numberOfTaps()。
在 EmojiSticker 组件中创建以下对象:
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); } });
为了给过渡添加动画,我们来使用基于弹簧的动画。这会让它更有生命力,因为它基于现实世界中弹簧的物理特性。我们将使用 react-native-reanimated 提供的 withSpring() 函数。
在贴纸图片上,我们将使用 useAnimatedStyle() 钩子来创建一个样式对象。这将帮助我们在动画发生时使用共享值更新样式。我们还会通过修改 width 和 height 属性来缩放图片大小。这些属性的初始值设置为 imageSize。
创建一个 imageStyle 变量并将其添加到 EmojiSticker 组件中:
const imageStyle = useAnimatedStyle(() => { return { width: withSpring(scaleImage.value), height: withSpring(scaleImage.value), }; });
接下来,将 <Animated.Image> 组件包裹在 <GestureDetector> 中,并修改 <Animated.Image> 上的 style 属性以传入 imageStyle。
import { ImageSourcePropType, View } from 'react-native'; import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; export default function EmojiSticker({ imageSize, stickerSource }: Props) { 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 }}> <GestureDetector gesture={doubleTap}> <Animated.Image source={stickerSource} resizeMode="contain" style={[imageStyle, { width: imageSize, height: imageSize }]} /> </GestureDetector> </View> ); }
在上面的代码片段中,gesture 属性接收 doubleTap 的值,以便在用户双击贴纸图片时触发手势。
让我们看看我们在 Android、iOS 和 web 上的应用:
关于点击手势 API 的完整参考,请查看 React Native Gesture Handler 文档。
4
添加平移手势
为了识别贴纸上的拖动手势并跟踪其移动,我们将使用平移手势。在 components/EmojiSticker.tsx 中:
- 创建两个新的共享值:
translateX和translateY。 - 将
<View>替换为<Animated.View>组件。
export default function EmojiSticker({ imageSize, stickerSource }: Props) { const scaleImage = useSharedValue(imageSize); const translateX = useSharedValue(0); const translateY = useSharedValue(0); // ...其余代码保持不变 return ( <Animated.View style={{ top: -350 }}> <GestureDetector gesture={doubleTap}> {/* ...其余代码保持不变 */} </GestureDetector> </Animated.View> ); }
让我们了解一下上面的代码做了什么:
- 定义的平移值会让贴纸在屏幕上移动。由于贴纸会沿着两个轴移动,我们需要跟踪 X 和 Y 值。
- 在
useSharedValue()钩子中,我们将两个平移变量的初始位置都设置为0。这是贴纸的初始位置和起点。该值会在手势开始时设置贴纸的初始位置。
在上一步中,我们为通过 Gesture.Tap() 方法链式调用的点击手势触发了 onStart() 回调。对于平移手势,请指定 onChange() 回调,它会在手势处于活动状态并移动时运行。
- 创建一个
drag对象来处理平移手势。onChange()回调接受event作为参数。changeX和changeY属性保存自上一个事件以来位置的变化,并更新存储在translateX和translateY中的值。 - 使用
useAnimatedStyle()钩子定义containerStyle对象。它将返回一个变换数组。对于<Animated.View>组件,我们需要将transform属性设置为translateX和translateY的值。当手势激活时,这将改变贴纸的位置。
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, }, ], }; });
接下来,在 JSX 代码中:
- 更新
<EmojiSticker>组件,使<GestureDetector>组件成为最顶层组件。 - 在
<Animated.View>组件上添加containerStyle,以应用变换样式。
import { Gesture, GestureDetector } from 'react-native-gesture-handler'; import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated'; import { ImageSourcePropType } from 'react-native'; type Props = { imageSize: number; stickerSource: ImageSourcePropType; }; export default function EmojiSticker({ imageSize, stickerSource }: Props) { 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 ( <GestureDetector gesture={drag}> <Animated.View style={[containerStyle, { top: -350 }]}> <GestureDetector gesture={doubleTap}> <Animated.Image source={stickerSource} resizeMode="contain" style={[imageStyle, { width: imageSize, height: imageSize }]} /> </GestureDetector> </Animated.View> </GestureDetector> ); }
让我们看看我们在 Android、iOS 和 web 上的应用:
摘要
Chapter 6: Add gestures
我们已成功实现平移和点击手势。
在下一章中,我们将学习如何对图片和贴纸进行截图,并将其保存到设备的相册中。