@[toc]
只要 RN 项目里一旦涉及到图片编辑、画布、地图、白板、卡片拖拽这些复杂交互,手势问题几乎是必踩坑。
常见的吐槽包括:
- 手势一多就开始互相打架
- JS 线程一忙,动画直接掉帧
- Reanimated 写到后面自己都不敢改
这篇文章不讲零散 API,而是从架构设计的角度 ,一步步讲清楚复杂手势在 RN 里应该怎么"组织",并给你一套可直接复用的模式。
为什么 RN 复杂手势容易失控?
先理解为什么问题会集中爆发。
1. 手势不是"事件",而是"状态机"
拖拽、缩放、旋转都不是 click 这种瞬时事件,而是:
- 有开始
- 有持续变化
- 有结束
- 还可能并发发生
如果你把它们当成普通回调来写,代码一定会乱。
2. JS 线程天生不适合做高频动画
如果你还在用:
js
onGestureEvent={() => setState(...)}
那掉帧是必然的。
复杂手势的更新频率非常高(60fps),必须让动画跑在 UI 线程。
3. Reanimated 写法容易"业务 + 动画搅在一起"
很多项目的 Reanimated 代码长这样:
- 手势逻辑
- 动画计算
- 边界判断
- 业务状态
全写在一个 useAnimatedGestureHandler 里,半年后没人敢动。
手势体系的正确打开方式(核心思想)
一句话总结:
Gesture Handler 负责"识别手势",
Reanimated 负责"驱动动画",
JS 负责"业务决策"。
三者职责一定要拆开。
Gesture Handler v2 的设计模式
v2 是一个质的变化,不只是 API 更好看。
推荐组合方式
text
Gesture = 识别
Shared Value = 状态
Animated Style = 表现
核心原则
- 不在 JS 里算位移
- 不在手势回调里写业务
- 手势只改 Shared Value
Shared Values:复杂手势的"唯一数据源"
Shared Value 的最大价值不是性能,而是让状态变得可控。
推荐的状态拆分
js
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
每个手势只负责自己那一块数据。
Demo:可缩放 / 旋转 / 拖拽的图片编辑器
这是一个可以直接跑的 Demo 结构,非常适合图片编辑、贴纸、画布类场景。
1. 初始化 Shared Values
js
const scale = useSharedValue(1);
const rotation = useSharedValue(0);
const translateX = useSharedValue(0);
const translateY = useSharedValue(0);
这些值就是你这个组件的"物理状态"。
2. 拖拽手势(Pan)
js
const panGesture = Gesture.Pan()
.onUpdate(e => {
translateX.value += e.changeX;
translateY.value += e.changeY;
});
这里有两个关键点:
- 用
changeX / changeY,不是translationX - 不依赖 JS,不 setState
3. 缩放手势(Pinch)
js
const pinchGesture = Gesture.Pinch()
.onUpdate(e => {
scale.value = e.scale;
});
这里不做边界判断,只负责"真实缩放"。
4. 旋转手势(Rotation)
js
const rotationGesture = Gesture.Rotation()
.onUpdate(e => {
rotation.value = e.rotation;
});
保持简单,逻辑越少越稳定。
5. 手势组合(关键)
js
const composedGesture = Gesture.Simultaneous(
panGesture,
pinchGesture,
rotationGesture
);
Simultaneous 是复杂交互的核心,不要再手写冲突判断。
6. Animated Style:统一渲染出口
js
const animatedStyle = useAnimatedStyle(() => {
return {
transform: [
{ translateX: translateX.value },
{ translateY: translateY.value },
{ scale: scale.value },
{ rotateZ: `${rotation.value}rad` },
],
};
});
这里是唯一一个关心"怎么画"的地方。
7. 完整组件结构
jsx
<GestureDetector gesture={composedGesture}>
<Animated.Image
source={require('./image.png')}
style={[styles.image, animatedStyle]}
/>
</GestureDetector>
结构非常清晰:
- Gesture 负责交互
- Animated Style 负责展示
- JS 不参与动画
架构拆分:让 Reanimated 不再难维护
推荐拆分方式
text
usePanGesture.ts
usePinchGesture.ts
useRotationGesture.ts
useTransformStyle.ts
每个 Hook 只干一件事。
示例:usePanGesture
js
export function usePanGesture(x, y) {
return Gesture.Pan().onUpdate(e => {
x.value += e.changeX;
y.value += e.changeY;
});
}
这样做的好处:
- 手势逻辑可以复用
- 改一个手势不影响其他
- 新人也能看懂
手势冲突怎么排查?
1. 优先用 Simultaneous / Exclusive
不要自己写 if 判断。
2. 打印 Shared Value,而不是 JS state
js
useDerivedValue(() => {
console.log(scale.value);
});
3. 暂时禁用某个手势快速定位
js
const disabledGesture = Gesture.Pan().enabled(false);
为什么这种架构不卡?
原因很简单:
- 手势识别在 UI 线程
- 动画计算在 UI 线程
- JS 线程完全不参与高频更新
JS 只在你需要的时候(比如点击保存)读取最终状态。
实际场景怎么用?
图片编辑器
- 拖动图片
- 双指缩放
- 旋转角度
地图 / 画布
- 平移视口
- 缩放画布
- 多点协作
卡片编辑 / 贴纸系统
- 拖动排序
- 旋转贴纸
- 缩放元素
总结
RN 复杂手势不是 API 难,而是架构没想清楚。
你只要记住这几条:
- 手势 ≠ 业务
- Shared Value 是唯一真相
- UI 动画不要走 JS
- Gesture 组合优于冲突判断
这套模式一旦搭好,后面加手势、调动画都非常稳。