本节摘要:手势决定界面「听不听话」,动画决定界面「活不活」——而两者的共同命门都是线程。本节把按压、拖拽两类基础手势的实现路径讲透,再用「动画跑在哪条线程」这个判据为 Animated、Reanimated、Lottie 三套方案划清边界,最后用一个可拖拽悬浮球的完整实现把知识串起来。本节回收第 2 章线程模型的伏笔,也是双端交互一致性的试金石。
先回收一条第 2 章的伏笔:手势事件由原生线程捕获,若每一帧位移都要跨线程送到 JavaScript 计算再返回,跟手性就取决于 JS 线程当时的繁忙程度。这套认知决定了本节全部技术选型的底层逻辑——能在线程本地解决的计算,绝不跨线程往返。
按压反馈是手势的第一课,也是最容易被敷衍的一课。Material 设计语言的涟漪与 iOS 的按压高亮是两套方言,直接用 Button 组件会得到两副面孔(3.2 节讲过它的风格差异)。工程上的标准解法是用 Pressable 自绘按压态:它提供按压进度回调,你用透明度或缩放统一两端观感,按下时透明度渐变到六成、松手弹回——三行样式换来双端一致的按压手感,性价比极高。更高阶的诉求是拖拽:气泡可拖、卡片可滑、抽屉可拉,这些都需要连续手势处理。基础版可用内置的手势响应系统,进阶场景(多手势冲突、嵌套滚动)则应上第三方手势库,它把手势识别下沉到原生层,冲突仲裁不再依赖 JS 线程的往返。

把上图压缩成一句判据:动画的每一帧在哪里算完? 在原生线程算完的(原生驱动模式、Reanimated、Lottie),帧率与 JS 线程繁忙度解耦,滚动再忙动画也稳;在 JS 线程逐帧算的(Animated 默认模式),JS 一忙就掉帧。据此得出选型次序:纯进出场、淡入淡出、位移缩放类动画,用 Animated 配原生驱动标志一行搞定;手势联动的复杂动画(拖拽跟手、滑动卡片物理回弹)上 Reanimated,它的动画逻辑可以直接运行在原生线程;设计同学用专业工具产出的复杂动效,走 Lottie 播放,两端渲染一致且开发零成本。有一个历史遗留要注意:原生驱动早期只支持「不可布局」的属性,位移要用变换参数表达而不能直接改位置属性——这个限制随新架构渲染器在逐步放开,但养成用变换做动画的习惯依然更稳。
把知识串成一个完整组件:悬浮球可拖拽跟随手指,松手后带弹簧回弹,全程跟手不掉帧。用第三方手势库与 Reanimated 的组合实现——这正是「手势与动画都在原生线程闭环」的范式:
import React from 'react'; import { StyleSheet } from 'react-native'; import { GestureDetector, Gesture } from 'react-native-gesture-handler'; import Animated, { useSharedValue, useAnimatedStyle, withSpring, } from 'react-native-reanimated'; export function DraggableBubble({ size = 56 }) { // 共享值:原生线程可读写的「线程本地变量」 const tx = useSharedValue(0); const ty = useSharedValue(0); const startX = useSharedValue(0); const startY = useSharedValue(0); const pan = Gesture.Pan() .onBegin(() => { // 记录拖拽起点,供偏移量计算 startX.value = tx.value; startY.value = ty.value; }) .onUpdate(e => { // 关键:位移直接写共享值,不经过 JS 线程,天然跟手 tx.value = startX.value + e.translationX; ty.value = startY.value + e.translationY; }) .onEnd(() => { // 松手弹回:弹簧参数决定「果冻感」强弱 tx.value = withSpring(0, { damping: 12 }); ty.value = withSpring(0, { damping: 12 }); }); // 样式随共享值变化,声明一次,帧帧更新都在原生线程 const bubbleStyle = useAnimatedStyle(() => ({ transform: [{ translateX: tx.value }, { translateY: ty.value }], })); return ( <GestureDetector gesture={pan}> <Animated.View style={[styles.bubble, { width: size, height: size }, bubbleStyle]} /> </GestureDetector> ); } const styles = StyleSheet.create({ bubble: { borderRadius: 28, backgroundColor: '#1a73e8', position: 'absolute', right: 24, bottom: 96 }, });
逐点拆解它的跟手原理:位移量写入共享值——这是原生线程可直接读写的存储;样式与共享值绑定后,每帧更新在原生线程完成;手势识别也在原生层。三层全部线程本地化,JS 线程哪怕被列表更新占满,悬浮球照样丝滑。同样的需求若用默认模式 Animated 实现,快滑时球会「追手指」——那就是跨线程往返的延迟现形。
背景:某短视频应用的点赞动效最初用默认模式 Animated 实现,点赞瞬间要同时缩放心形、飘出粒子、更新计数,真机上偶发掉帧,低端 Android 机尤其明显。操作:先复现归因——性能监视器显示掉帧帧的 JS 线程都在忙(评论列表正在分页渲染),证实是「动画逐帧依赖 JS」的模式问题;然后把心形缩放与粒子位移改写为原生驱动动画,计数文本保留 JS 驱动(文本内容变化本来就要走 JS);最后调整粒子生成时机错开列表分页高峰。结果:双端点赞动效稳定满帧,低端机不再出现粘滞。解读:这次返工没有改任何动画设计,只改了「帧在哪里算」——判据的价值就在于它把玄学卡顿翻译成线程选择题。变式:如果动效进一步复杂到需要物理引擎级表现(碰撞、路径形变),原生驱动的表达能力会不够,届时再评估 Reanimated 工作流或 Lottie 全量接管,升级路径是连续的而不是推倒重来。
组件层到此通关。下一章给界面注入血液:状态、数据流与端上持久化。