React Native Academy · 课时

捏合缩放与旋转

结合 PinchGestureHandler 和 RotationGestureHandler,并启用同时手势识别,实现可缩放、可旋转的图像组件。

第 4 / 4 课13 个步骤

捏合缩放与旋转 是 CoddyKit 上的免费 React Native Academy 课时。 这是第 4 节课,共 4 节。 你可以在下方免费阅读本课时的完整内容 — 然后在浏览器中使用内置代码编辑器和全天候 AI 导师进行实践。 这是 React Native Academy 学习路径的一部分,你的进度在网页和 CoddyKit 应用中同步。 React Native Academy 课程共包含 4 节课。

捏合与旋转为何重要

捏合缩放和旋转是移动设备上的典型交互,常见于照片查看器、地图和文档应用。要实现良好的效果,需要在原生线程上进行多点触控手势识别,并通过 Reanimated 共享值平滑地实时更新变换。

React Native Gesture Handler 提供了 Gesture.Pinch() 和 Gesture.Rotation() 识别器。将它们与 Reanimated 的 useAnimatedStyle 和 useSharedValue 结合,可以实现极其流畅的交互,使用感受几乎与原生实现无异。

Gesture.Pinch:检测缩放比例

Gesture.Pinch() 可识别双指捏合或张开手势。事件对象中最重要的值是 event.scale:手势开始时为 1.0,随后会根据手指间距离按比例变化。值为 2.0 表示手指间距离是开始时的两倍。

您必须将手势产生的增量缩放值与保存的基础缩放值结合起来,才能在多次捏合手势之间累计缩放;否则,每次新的捏合都会重置为 1.0。

import { Gesture } from 'react-native-gesture-handler';
import { useSharedValue, withSpring } from 'react-native-reanimated';

const scale = useSharedValue(1);
const savedScale = useSharedValue(1);

const pinch = Gesture.Pinch()
  .onUpdate((e) => {
    scale.value = savedScale.value * e.scale;
  })
  .onEnd(() => {
    savedScale.value = scale.value;
  });

限制缩放范围

如果不进行限制,用户可能会放大到极大的比例,或缩小到图片完全消失。请定义最小和最大缩放级别,并在 onUpdate 回调中使用实用工作单元限制缩放值。

限制函数会作为工作单元在 UI 线程上运行,从而保持手势响应迅速。请根据内容选择合适的限制值——照片查看器通常允许 1 到 5 倍缩放,而地图可能允许更宽的范围。

const MIN_SCALE = 1;
const MAX_SCALE = 5;

function clamp(value, min, max) {
  'worklet';
  return Math.min(Math.max(value, min), max);
}

const pinch = Gesture.Pinch()
  .onUpdate((e) => {
    scale.value = clamp(savedScale.value * e.scale, MIN_SCALE, MAX_SCALE);
  })
  .onEnd(() => {
    savedScale.value = scale.value;
    // Snap back if over-zoomed out:
    if (scale.value < MIN_SCALE) {
      scale.value = withSpring(MIN_SCALE);
      savedScale.value = MIN_SCALE;
    }
  });

Gesture.Rotation:跟踪角度

Gesture.Rotation() 可跟踪双指扭转手势的角度变化。event.rotation 的值以弧度表示,即手指从手势开始位置旋转的角度。完整的顺时针旋转约为 6.28 弧度(2π)。

与缩放一样,您必须在多次手势之间累计旋转:在手势结束时保存当前旋转值,并在后续手势中将新的手势旋转量加到保存的偏移值上。

const rotation = useSharedValue(0);
const savedRotation = useSharedValue(0);

const rotationGesture = Gesture.Rotation()
  .onUpdate((e) => {
    rotation.value = savedRotation.value + e.rotation;
  })
  .onEnd(() => {
    savedRotation.value = rotation.value;
  });

在 useAnimatedStyle 中应用缩放与旋转

useAnimatedStyle 中的 transform 数组可以同时应用缩放和旋转。变换的顺序很重要:先应用缩放,再应用旋转(也可以反过来,具体取决于所需效果)。React Native 的变换数组可以直接接受共享值中的数值缩放值和弧度旋转值。

rotate 变换需要一个带有“rad”后缀的字符串,而 scale 可以直接接收数字。请使用字符串拼接(不要使用带反引号的模板字面量)在工作单元中构造旋转字符串。

const animatedStyle = useAnimatedStyle(() => ({
  transform: [
    { scale: scale.value },
    { rotate: rotation.value + 'rad' }, // concatenate string, no backticks
  ],
}));

return (
  <GestureDetector gesture={composedGesture}>
    <Animated.Image
      source={{ uri: imageUrl }}
      style={[styles.image, animatedStyle]}
    />
  </GestureDetector>
);

同时组合捏合与旋转

照片查看器需要让捏合和旋转同时进行。请将两个手势包装在 Gesture.Simultaneous 中,并将组合后的手势传递给单个 GestureDetector。这样,用户就能通过双指手势在一次流畅的操作中同时缩放和旋转。

再同时添加平移功能,就能实现完整的照片查看器体验:用双指同时缩放、旋转并重新定位图片。

const composedGesture = Gesture.Simultaneous(
  pinch,
  rotationGesture,
  pan
);

return (
  <GestureDetector gesture={composedGesture}>
    <Animated.Image
      source={{ uri: imageUrl }}
      style={[styles.image, animatedStyle]}
      resizeMode='contain'
    />
  </GestureDetector>
);

双击重置缩放

照片查看器的标准功能之一是双击在原始大小和放大视图之间切换,再次双击则重置。使用 Gesture.Tap().numberOfTaps(2) 创建双击识别器,并在其处理程序中使用弹簧动画将缩放和旋转恢复到初始值。

将 Gesture.Exclusive 与双击手势结合,并让双击优先于单击,以防止单击在双击序列期间触发。

const doubleTap = Gesture.Tap()
  .numberOfTaps(2)
  .onEnd(() => {
    if (scale.value > 1) {
      // Reset to natural size:
      scale.value = withSpring(1);
      savedScale.value = 1;
      rotation.value = withSpring(0);
      savedRotation.value = 0;
      offsetX.value = withSpring(0);
      offsetY.value = withSpring(0);
    } else {
      // Zoom to 2x:
      scale.value = withSpring(2);
      savedScale.value = 2;
    }
  });

捏合缩放的中心点

默认情况下,缩放变换会以图片中心为中心缩放图片。为了获得更自然的效果,图片应围绕焦点缩放——也就是两根手指之间的中点。手势事件会提供 event.focalX 和 event.focalY 来实现这一点。

实现围绕焦点缩放,需要在缩放的同时调整平移偏移量,使手指下方的点在缩放过程中保持固定。这涉及计算给定焦点处因缩放变化而产生的偏移变化。

const pinch = Gesture.Pinch()
  .onUpdate((e) => {
    const newScale = clamp(savedScale.value * e.scale, MIN_SCALE, MAX_SCALE);
    const scaleDelta = newScale / scale.value;
    // Adjust offset to keep focal point fixed:
    offsetX.value = e.focalX - scaleDelta * (e.focalX - offsetX.value);
    offsetY.value = e.focalY - scaleDelta * (e.focalY - offsetY.value);
    scale.value = newScale;
  });

平移与缩放后的边界限制

放大后,用户可以平移来查看图片的不同部分。但您应限制平移范围,避免图片被拖动到边缘之外而露出空白画布。请根据当前缩放比例和图片尺寸计算最大平移偏移量,并在平移手势的 onUpdate 中限制偏移量。

这种细节处理能将专业的照片查看器与粗糙的原型区分开来。限制逻辑只需几行数学计算,并会作为工作单元在 UI 线程上运行。

const IMAGE_WIDTH = 300;
const CONTAINER_WIDTH = 375;

const pan = Gesture.Pan()
  .onUpdate((e) => {
    const maxOffsetX = Math.max(0, (scale.value * IMAGE_WIDTH - CONTAINER_WIDTH) / 2);
    offsetX.value = clamp(
      savedOffsetX.value + e.translationX,
      -maxOffsetX,
      maxOffsetX
    );
  });

缩放过度时弹回

当用户在缩放过度(低于最小值或高于最大值)后松开手指时,应使用弹簧动画将缩放比例恢复到限制值,而不是让图片停留在异常大小。请在捏合手势的 onEnd 回调中执行此操作。

同样地,当用户缩小回 1 倍后,请将平移偏移量重置为零,使图片回到中心。在 onEnd 中结合这些边界检查,可以让组件使用起来更精致、更可预测。

const pinch = Gesture.Pinch()
  .onEnd(() => {
    if (scale.value < MIN_SCALE) {
      scale.value = withSpring(MIN_SCALE);
      savedScale.value = MIN_SCALE;
      // Reset pan to center:
      offsetX.value = withSpring(0);
      offsetY.value = withSpring(0);
    } else if (scale.value > MAX_SCALE) {
      scale.value = withSpring(MAX_SCALE);
      savedScale.value = MAX_SCALE;
    } else {
      savedScale.value = scale.value;
    }
  });

整合所有功能

一个完整的可缩放、可旋转、可平移照片查看器包括:用于保存缩放、旋转和偏移的共享值;通过 useAnimatedStyle 应用所有变换;使用 Gesture.Simultaneous 组合捏合、旋转、平移和双击;以及在 onUpdate 中进行边界限制,并在 onEnd 中进行弹簧修正。

这是您可以在 React Native 中构建的最令人印象深刻的交互之一,并且整个过程都在 UI 线程上运行,手势期间无需使用 JavaScript 线程,因此其表现几乎与原生实现无异。

const composed = Gesture.Simultaneous(
  Gesture.Exclusive(
    doubleTap,
    singleTap
  ),
  pan,
  pinch,
  rotationGesture
);

const style = useAnimatedStyle(() => ({
  transform: [
    { translateX: offsetX.value },
    { translateY: offsetY.value },
    { scale: scale.value },
    { rotate: rotation.value + 'rad' },
  ],
}));

快速检查

测试您对本课 React Native 移动开发概念的理解。

课程回顾

在本课中,您学习了:Gesture.Pinch() 提供 event.scale,用于跟踪相对于手势起始位置的双指缩放;Gesture.Rotation() 提供以弧度表示的 event.rotation,用于跟踪双指扭转;以及Gesture.Simultaneous 将捏合、旋转和平移组合起来,使三者同时工作,从而实现完整的照片查看器体验。接下来,我们将配置自定义 URL 方案,以便深层链接到应用中的特定屏幕。

免费开始

用 AI 导师学习 JavaScript — 免费

在浏览器中编写并运行真实代码,获得全天候 AI 导师的即时帮助,并在网页或应用中继续学习。

课程
30
课程
120

常见问题解答

「捏合缩放与旋转」课时是免费的吗?

是的 — 「捏合缩放与旋转」的完整文本可在网页上免费阅读。要进行交互式练习(内置代码编辑器和全天候 AI 导师)并解锁 React Native Academy 课程的其余内容,请升级到 CoddyKit PRO。 React Native Academy 课程共包含 4 节课。

「捏合缩放与旋转」这节课中我会学到什么?

结合 PinchGestureHandler 和 RotationGestureHandler,并启用同时手势识别,实现可缩放、可旋转的图像组件。 你通过在浏览器中直接运行的动手代码来练习 React Native Academy,全天候 AI 导师会在你学习这节课的过程中回答你的问题。

学习 React Native Academy 需要有经验吗?

无需任何先前经验。CoddyKit 上的 React Native Academy 课程适合初学者到高级学习者,你可以从这里开始或从头开始,按照自己的节奏学习。 这是第 4 节课,共 4 节。

「捏合缩放与旋转」课时需要多长时间?

大多数 CoddyKit 课程大约需要 5–10 分钟。每节课都很精短且互动,所以你能稳步进步,并在网页和应用中从离开的地方继续。

我能在这节 React Native Academy 课中编写并运行代码吗?

能。每节 React Native Academy 课都包含内置代码编辑器,你可以在浏览器中直接编写并运行真实代码,并获得即时 AI 反馈 — 无需本地设置。

此课程中的所有课时

  1. 安装 Reanimated 与共享值
  2. Worklet 与在 UI 线程上运行代码
  3. 使用 Gesture Handler 实现平移与滑动手势
  4. 捏合缩放与旋转
← 返回 React Native Academy