添加手势

编辑

在本教程中,学习如何实现来自 React Native Gesture Handler 和 Reanimated 库的手势。


手势是为应用程序提供直观用户体验的绝佳方式。React Native Gesture Handler 库提供内置的原生组件,可处理手势。它使用平台的原生触摸处理系统识别平移、轻拍、旋转和其他手势。在本章中,我们将使用此库添加两种不同的手势:

  • 双击放大表情符号贴纸的大小,再次双击时减小比例。
  • 拖动以在屏幕上移动表情符号贴纸,以便用户可以将贴纸放置在图像的任意位置。

我们还将使用 Reanimated 库在手势状态之间进行动画。

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

1

添加 GestureHandlerRootView

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

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 文件。在其中,导入 Animated 来自 react-native-reanimated 库以使用动画组件。
  2. <Animated.Image> 替换 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 允许我们在检测到触摸输入时添加行为,像双击事件。

EmojiSticker.tsx 文件中:

  1. react-native-gesture-handler 导入 GestureGestureDetector
  2. 为了识别贴纸上的轻拍,导入 useAnimatedStyleuseSharedValuewithSpring 来自 react-native-reanimated,以对 <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), }; });

接下来,用 <GestureDetector> 包裹 <Animated.Image> 组件,并在 <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 和网页上查看我们的应用:

有关轻拍手势 API 的完整参考,请参见 React Native Gesture Handler 文档。

4

添加拖动手势

为了识别贴纸上的拖动手势并跟踪其移动,我们将使用拖动手势。在 components/EmojiSticker.tsx 中:

  1. 创建两个新的共享值:translateXtranslateY
  2. <Animated.View> 组件替换 <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. containerStyle 添加到 <Animated.View> 组件的样式中,以应用变换样式。
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 和网页上查看我们的应用:

总结

第六章:添加手势

我们成功实现了平移和轻拍手势。

在下一章中,我们将学习如何截取图像和贴纸的屏幕快照,并将其保存到设备的库中。

Next: 截取屏幕快照