添加手势

编辑页面

在本教程中,了解如何实现来自 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 库在手势状态之间制作动画过渡。

观看:为你的通用 Expo 应用添加手势
观看:为你的通用 Expo 应用添加手势

使用 React Native Gesture Handler 和 Reanimated 为 emoji 贴纸添加双击和平移手势。


1

添加 GestureHandlerRootView

为了让手势交互在应用中正常工作,我们将在 Index 组件顶部从 react-native-gesture-handler 渲染 <GestureHandlerRootView>。将 app/(tabs)/index.tsx 中最外层的 <View> 组件替换为 <GestureHandlerRootView>

app/(tabs)/index.tsx
// ...其余导入语句保持不变 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> 组件上,以实现双击手势效果。

  1. 打开 components 目录下的 EmojiSticker.tsx 文件。在其中从 react-native-reanimated 库导入 Animated,以使用动画组件。
  2. Image 组件替换为 <Animated.Image>
components/EmojiSticker.tsx
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 文件中:

  1. react-native-gesture-handler 导入 GestureGestureDetector
  2. 为了识别贴纸上的点击,导入 useAnimatedStyleuseSharedValuewithSpring,以便为 <Animated.Image> 的样式添加动画。
  3. EmojiSticker 组件内部,使用 useSharedValue() 钩子创建一个名为 scaleImage 的引用。它会将 imageSize 的值作为初始值。
components/EmojiSticker.tsx
// ...其余导入语句保持不变 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 组件中创建以下对象:

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); } });

为了给过渡添加动画,我们来使用基于弹簧的动画。这会让它更有生命力,因为它基于现实世界中弹簧的物理特性。我们将使用 react-native-reanimated 提供的 withSpring() 函数。

在贴纸图片上,我们将使用 useAnimatedStyle() 钩子来创建一个样式对象。这将帮助我们在动画发生时使用共享值更新样式。我们还会通过修改 widthheight 属性来缩放图片大小。这些属性的初始值设置为 imageSize

创建一个 imageStyle 变量并将其添加到 EmojiSticker 组件中:

components/EmojiSticker.tsx
const imageStyle = useAnimatedStyle(() => { return { width: withSpring(scaleImage.value), height: withSpring(scaleImage.value), }; });

接下来,将 <Animated.Image> 组件包裹在 <GestureDetector> 中,并修改 <Animated.Image> 上的 style 属性以传入 imageStyle

components/EmojiSticker.tsx
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 中:

  1. 创建两个新的共享值:translateXtranslateY
  2. <View> 替换为 <Animated.View> 组件。
components/EmojiSticker.tsx
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() 回调,它会在手势处于活动状态并移动时运行。

  1. 创建一个 drag 对象来处理平移手势。onChange() 回调接受 event 作为参数。changeXchangeY 属性保存自上一个事件以来位置的变化,并更新存储在 translateXtranslateY 中的值。
  2. 使用 useAnimatedStyle() 钩子定义 containerStyle 对象。它将返回一个变换数组。对于 <Animated.View> 组件,我们需要将 transform 属性设置为 translateXtranslateY 的值。当手势激活时,这将改变贴纸的位置。
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, }, ], }; });

接下来,在 JSX 代码中:

  1. 更新 <EmojiSticker> 组件,使 <GestureDetector> 组件成为最顶层组件。
  2. <Animated.View> 组件上添加 containerStyle,以应用变换样式。
components/EmojiSticker.tsx
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

我们已成功实现平移和点击手势。

在下一章中,我们将学习如何对图片和贴纸进行截图,并将其保存到设备的相册中。

Next: 截取屏幕截图