添加手势

在本教程中,了解如何从 React Native Gesture Handler 和 Reanimated 库实现手势。


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

¥Gestures are a great way to provide an intuitive user experience in an app. The React Native Gesture Handler 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 library to animate between gesture states.

Watch: Adding gestures to your universal Expo app
Watch: Adding gestures to your universal Expo app

1

添加 GestureHandlerRootView

¥Add GestureHandlerRootView

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

¥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>.

app/(tabs)/index.tsx
// ... rest of the import statements remain same
import { GestureHandlerRootView } from 'react-native-gesture-handler';

export default function Index() {
return (
  <GestureHandlerRootView style={styles.container}>
    {/* ...rest of the code remains */}
  </GestureHandlerRootView>
)
}

2

使用动画组件

¥Use animated components

Animated 组件查看组件的 style prop,并确定要动画化哪些值并应用更新来创建动画。Reanimated 导出动画组件,例如 <Animated.View><Animated.Text><Animated.ScrollView>。我们将向 <Animated.Image> 组件应用动画,以使双击手势发挥作用。

¥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. 打开 components 目录中的 EmojiSticker.tsx 文件。在其中,从 react-native-reanimated 库导入 Animated 以使用动画组件。

    ¥Open the EmojiSticker.tsx file in the components directory. Inside it, import Animated from the react-native-reanimated library to use animated components.

  2. Image 组件替换为 <Animated.Image>

    ¥Replace the Image component with <Animated.Image>.

components/EmojiSticker.tsx
import { View } from 'react-native';
import Animated from 'react-native-reanimated';
import { type ImageSource } from 'expo-image';

type Props = {
imageSize: number;
stickerSource: ImageSource;
};

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 复活 文档。

¥For a complete reference of the animated component API, see React Native Reanimated documentation.

3

添加点击手势

¥Add a tap gesture

React Native Gesture Handler 允许我们在检测到触摸输入时添加行为,例如双击事件。

¥React Native Gesture Handler allows us to add behavior when it detects touch input, like a double tap event.

在 EmojiSticker.tsx 文件中:

¥In the EmojiSticker.tsx file:

  1. react-native-gesture-handler 导入 GestureGestureDetector

    ¥Import Gesture and GestureDetector from react-native-gesture-handler.

  2. 要识别贴纸上的点击,请从 react-native-reanimated 导入 useAnimatedStyleuseSharedValuewithSpring 以动画化 <Animated.Image> 的样式。

    ¥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. EmojiSticker 组件内,使用 useSharedValue() 钩子创建一个名为 scaleImage 的引用。它将把 imageSize 的值作为它的初始值。

    ¥Inside the EmojiSticker component, create a reference called scaleImage using the useSharedValue() hook. It will take the value of imageSize as its initial value.

components/EmojiSticker.tsx
// ...rest of the import statements remain same
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 (
  // ...rest of the code remains same
)
}

使用 useSharedValue() 钩子创建共享值有很多优点。它有助于改变数据并根据当前值运行动画。我们可以使用 .value 属性访问和修改共享值。我们将创建一个 doubleTap 对象来缩放初始值,并使用 Gesture.Tap() 在缩放贴纸图片时为过渡设置动画。要确定所需的点击次数,我们将添加 numberOfTaps()

¥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().

EmojiSticker 组件中创建以下对象:

¥Create the following object in the EmojiSticker component:

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() 功能。

¥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.

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

¥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.

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

¥Create an imageStyle variable and add it to the EmojiSticker component:

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

接下来,用 <GestureDetector> 封装 <Animated.Image> 组件,并修改 <Animated.Image> 上的 style 属性以传递 imageStyle

¥Next, wrap the <Animated.Image> component with the <GestureDetector> and modify the style prop on the <Animated.Image> to pass the imageStyle.

components/EmojiSticker.tsx
import { View } from 'react-native';
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import { type ImageSource } from 'expo-image';

type Props = {
imageSize: number;
stickerSource: ImageSource;
};

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 prop 采用 doubleTap 的值来在用户双击贴纸图片时触发手势。

¥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.

让我们看看我们在 Android、iOS 和 Web 上的应用:

¥Let's take a look at our app on Android, iOS and the web:

有关点击手势 API 的完整参考,请参阅 React Native 手势处理程序 文档。

¥For a complete reference of the tap gesture API, see the React Native Gesture Handler documentation.

4

添加平移手势

¥Add a pan gesture

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

¥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. 创建两个新的共享值:translateXtranslateY

    ¥Create two new shared values: translateX and translateY.

  2. <Animated.View> 组件替换 <View>

    ¥Replace the <View> with the <Animated.View> component.

components/EmojiSticker.tsx
export default function EmojiSticker({ imageSize, stickerSource }: Props) {
const scaleImage = useSharedValue(imageSize);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
// ...rest of the code remains same

return (
  <Animated.View style={{ top: -350 }}>
    <GestureDetector gesture={doubleTap}>
      {/* ...rest of the code remains same */}
    </GestureDetector>
  </Animated.View>
);
}

我们来了解一下上面的代码做了什么:

¥Let's learn what the above code does:

  • 定义的翻译值将使贴纸在屏幕上移动。由于贴纸沿两个轴移动,我们需要跟踪 X 和 Y 值。

    ¥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.

  • useSharedValue() 钩子中,我们将两个翻译变量设置为初始位置 0。这是贴纸的初始位置和起点。此值设置手势开始时贴纸的初始位置。

    ¥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.

在上一步中,我们为链接到 Gesture.Tap() 方法的点击手势触发了 onStart() 回调。对于平移手势,请指定 onChange() 回调,该回调在手势处于活动状态并移动时运行。

¥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. 创建一个 drag 对象来处理平移手势。onChange() 回调接受 event 作为参数。changeXchangeY 属性保存自上次事件以来位置的变化。并更新存储在 translateXtranslateY 中的值。

    ¥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. 使用 useAnimatedStyle() 钩子定义 containerStyle 对象。它将返回一个转换数组。对于 <Animated.View> 组件,我们需要将 transform 属性设置为 translateXtranslateY 值。当手势处于活动状态时,这将更改贴纸的位置。

    ¥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.

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 代码中:

¥Next, inside the JSX code:

  1. 更新 <EmojiSticker> 组件,使 <GestureDetector> 组件成为顶层组件。

    ¥Update the <EmojiSticker> component so that the <GestureDetector> component becomes the top-level component.

  2. <Animated.View> 组件上添加 containerStyle 以应用转换样式。

    ¥Add the containerStyle on the <Animated.View> component to apply the transform styles.

components/EmojiSticker.tsx
import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, { useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
import { type ImageSource } from 'expo-image';

type Props = {
imageSize: number;
stickerSource: ImageSource;
};

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 上的应用:

¥Let's take a look at our app on Android, iOS and the web:

概括

¥Summary

Chapter 6: Add gestures

We've successfully implemented pan and tap gestures.

In 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.

Next: Take a screenshot