十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Reanimated 3 + Gesture Handler:实现丝滑手势动画的架构与实战

Reanimated 3 + Gesture Handler:实现丝滑手势动画的架构与实战 每次在技术社区看到“丝滑”这个词我脑子里蹦出来的第一个画面就是Reanimated 3配合Gesture Handler跑起来的那种跟手度。这种丝滑跟传统方案有本质区别——它不是靠优化CPU帧率挤出来的而是从架构层把动画和手势任务移到了专用UI线程JS线程根本插不上手动画自然就不会因为业务卡顿掉链子。这篇文章就围绕Reanimated 3动画引擎和Gesture Handler手势处理这套组合拆解它丝滑的根源、核心API的玩法、实战中踩过的坑以及到底怎么调参数才能把跟手感调到“刚体碰撞”级别。适合想把手势交互做扎实、或者正被RN动画卡顿折磨的朋友读完之后你至少能明白为什么有些动效你做出来就卡别人做出来就像原生。1. 内容整体设计与思路拆解1.1 为什么说卡顿问题出在“线程归属”上先讲一个特别容易被忽略的底层事实React Native的应用默认跑在JavaScript线程上而UI渲染跑在原生线程上。这两条线程之间传递数据是有成本的频繁的异步通信必然造成帧率抖动。很多开发者第一次遇到“事件卡顿”第一反应是“优化一下JS代码”其实方向就错了——如果动画每一帧都要从原生侧发事件交给JS处理再让JS返回新的状态就算JS写得再高效通信开销也摆在那里。真正靠谱的做法是把动画放在原生UI线程直接计算、直接刷新。Reanimated 3正是围绕这个思路做的架构。它的核心机制是worklet——把开发者写的动画逻辑函数自动提取出来序列化后运行在UI线程这样动画本身就不依赖JS线程了。而Gesture Handler负责做手势识别它同样在原生侧完成监听和状态分类。这两个库“一识别、一驱动”的配合恰好覆盖了用户在屏幕上操作的完整闭环。以前做拖拽、缩放、滑动这类跟手交互总感觉隔着一层纱布根源就在线程搬运上现在用这套组合手势数据直接喂给UI线程的动画逻辑中间没有跨界跳转丝滑就变成了物理上的必然。1.2 这套方案的设计意图和选型权衡我在选择方案时会考虑这样一个问题为什么一定要Reanimated Gesture Handler组合而不是只用一个或者用Animated PanResponder单独用Animated的确能在原生UI线程驱动部分属性但它的手势监听往往还要依赖PanResponder或原生事件。PanResponder本身是JS侧的响应系统响应时机天然受JS线程调度影响而且它的手势判定逻辑——比如拖拽中坐标系转换、多点触控的冲突处理——在复杂场景下非常容易出现不跟手的情况。Gesture Handler完全不一样它把触摸事件的生命周期从原生层开始管理支持原生手势状态的回调并且能和React Native的默认滚动、点击手势精确协调优先级。选型的时候也要考虑包大小和维护成本。Reanimated 3比第二版在API上简化了不少采用了更加明确的useSharedValue和useAnimatedStyle配合worklet机制几乎可以用写普通函数的方式完成动画逻辑。Gesture Handler的API也逐渐收敛到gesture对象描述式的写法。这样的折中让团队上手成本降低了很多而且内置的babel插件会帮你在编译期把worklet转换好不用自己操心序列化细节。如果有团队成员问“把这套放进现有项目值不值”我的回答是只要你的界面有一处需要跟手拖拽、手势缩放或列表轻扫就值得上。2. 核心细节解析与实操要点2.1 worklet机制让代码“跨线程”跑起来的关键Reanimated 3里最核心的概念就是worklet。简单说worklet是一个可以运行在UI线程上的JavaScript函数。你在开发时还是写JS语法但babel插件会在编译阶段把这类函数标记并打包成UI线程可执行的代码。React Native运行时会在后台准备好一份精简的JS引擎放在UI线程这个引擎专门执行worklet函数里的逻辑。写worklet的时候要注意不要直接捕获JS线程里的普通变量因为UI线程上的运行时和你JS线程的运行时是不共享作用域的。正确做法是把需要用到的数据放进sharedValue。sharedValue属于共享值JS线程和UI线程都能读写而且读写时会自动同步。比如拖拽场景里我们需要记录手指当前的位置、视图初始位置这些就应该用sharedValue管理。如果不小心在worklet里引用了外部的普通变量运行时会提示“Tried to synchronously call a non-worklet function on the UI thread”之类的错误这种情况多半是漏了添加worklet指令或者变量传递方式不对。在调试UI线程代码时控制台打印也和普通JS不一样。虽然worklet里可以调用console.log但输出内容和调用时机都可能和你想的不同数据在跨线程传输后对象序列化会造成一些信息缺失。我的习惯是尽量少在worklet里做复杂调试改为把关键值写入sharedValue再在JS侧监听。这样能利用React DevTools查看状态调试体验顺畅很多。2.2 Shared Value与动画驱动的关系Shared Value是Reanimated 3的“共享数据中枢”。它本质上是一个持有可变值的对象任何线程读它的value属性都能拿到最新值任何地方改value就会触发关联的动画或UI刷新。对于需要反复修改并实时驱动视图的手势场景它比React state高效得多——React state的每一次变化都要经过组件重渲染而sharedValue的变化只触发绑定UI属性的更新不走重渲染流程。何时使用sharedValue只要某个数值会连续变化并且变化需要立即反映到UI上就适合把它放进sharedValue。例如进度条、拖拽坐标、当前缩放倍数。如果是间歇性变化且变化频次很低比如组件挂载后只改变一次那用React state就够了不必引入额外开销。还有一点需要记住任何从worklet里修改sharedValue的代码都会立即触发依赖它的useAnimatedStyle重新计算所以不要在一个手势回调里反复给同一个sharedValue赋值同样的值这毫无意义只会带来无谓的计算。2.3 动画API的选择withTiming、withSpring与withSequence的适用场景Reanimated 3提供的动画函数不算多但每个都对应一种动效需求。新手容易把弹簧和补间混着用其实它们设计初衷完全不同withTiming适合明确的线性或曲线过渡比如透明度从0到1、宽度展开、位移到固定坐标。特点是可控性强可以指定duration和easing曲线动画结果确定不会“过冲”。withSpring适合需要模拟物理手感的场景比如拖拽松手后的回弹、点赞图标缩放、列表项的弹性进入。它的表现由质量、刚度、阻尼决定参数调得好就会非常跟手调不好容易一直“抖”。withSequence把多个动画串起来比如先上移再淡出。注意它中间可以插入withDelay来做时间间隔。我给新手最大的建议是能用withTiming解决的问题不要上withSpring尤其是有明确结束位置而且结束位置不依赖速度的场景。例如用户拖拽到某个阈值后释放卡片应该吸附到边界这种情况用withSpring虽然也能到目标位置但它会因为有初始速度而产生过冲视觉上可能让人觉得很“肉”。正确做法是用withTiming或者对速度做判断后用spring参数要对齐需求而不是一味追求弹性。3. 实操过程与核心环节实现3.1 环境准备与工程接入要把这套组合接入项目首先得装好几个包。以React Native 0.73以上的项目为例执行npm install react-native-reanimated react-native-gesture-handler如果是Expo项目则用npx expo install react-native-reanimated react-native-gesture-handler装完Reanimated之后需要修改babel配置在babel.config.js里添加插件module.exports { presets: [module:react-native/babel-preset], plugins: [ react-native-reanimated/plugin, // 注意该插件必须放在最后一条其他babel插件放在它前面 ], };这里有一个非常大的坑reanimated插件必须放在插件列表的最后。因为它在编译阶段要做AST转换如果后续还有其他插件转换结果可能被二次处理导致worklet标记失效。过去我见过很多“刚装好就崩溃”的问题十有八九就是插件顺序不对。另外Gesture Handler要求应用的入口必须包裹GestureHandlerRootView。我用一个最简单的例子来说明import React from react; import { GestureHandlerRootView } from react-native-gesture-handler; export default function App() { return ( GestureHandlerRootView style{{ flex: 1 }} {/* 这里放你的应用内容 */} /GestureHandlerRootView ); }如果在后续开发中发现手势事件偶尔不触发多半是漏了这一步或者某个页面的根节点没有被GestureHandlerRootView包住。别忘了Android上还要处理一些原生配置但现代版本的RN脚手架基本已经集成好了不需要额外处理。3.2 从零实现一个跟手拖拽卡片这一节咱们直接动手做一个最经典的拖拽卡片。这个功能覆盖了手势识别、sharedValue更新、动画样式绑定和松手回弹四个核心环节。完整代码如下import React from react; import { StyleSheet, View } from react-native; import { Gesture, GestureDetector } from react-native-gesture-handler; import Animated, { useSharedValue, useAnimatedStyle, withSpring } from react-native-reanimated; export default function DragCard() { const translateX useSharedValue(0); const translateY useSharedValue(0); const startX useSharedValue(0); const startY useSharedValue(0); const pan Gesture.Pan() .onStart(() { startX.value translateX.value; startY.value translateY.value; }) .onUpdate((event) { translateX.value startX.value event.translationX; translateY.value startY.value event.translationY; }) .onEnd(() { // 松手后回弹到初始位置 translateX.value withSpring(0); translateY.value withSpring(0); }); const animatedStyle useAnimatedStyle(() { return { transform: [ { translateX: translateX.value }, { translateY: translateY.value }, ], }; }); return ( View style{styles.container} GestureDetector gesture{pan} Animated.View style{[styles.card, animatedStyle]} / /GestureDetector /View ); } const styles StyleSheet.create({ container: { flex: 1, justifyContent: center, alignItems: center, }, card: { width: 120, height: 120, borderRadius: 16, backgroundColor: #6366f1, }, });这段代码里最核心的逻辑在于onUpdate里不是直接用event.translationX赋值而是先记录初始位置再加上偏移量。这个写法很关键因为Pan手势的translationX是相对于手势开始时的累计偏移如果直接用位移值覆盖坐标第二次拖拽就会跳动。记录初始位置的方案能保证视图的绝对坐标始终跟着手指走。onEnd里使用withSpring(0)表示松手回弹参数0代表目标坐标是初始位置。如果希望不同拖拽方向回弹手感不同可以换成withSpring(0, { damping: 15, stiffness: 200 })来调整阻尼和刚度。这里给一个参考值阻尼越高回弹越快停止刚度越大回弹过程越硬。3.3 进阶手势与动画的交互联动——可缩放和旋转卡片拖拽只是入门真正体现Reanimated 3 Gesture Handler优势的场景是多手势组合。咱们扩展一下让卡片不仅能拖拽还能用双指缩放和旋转。const scale useSharedValue(1); const savedScale useSharedValue(1); const rotation useSharedValue(0); const savedRotation useSharedValue(0); const pinch Gesture.Pinch() .onUpdate((event) { scale.value savedScale.value * event.scale; }) .onEnd(() { savedScale.value scale.value; }); const rotationGesture Gesture.Rotation() .onUpdate((event) { rotation.value savedRotation.value event.rotation; }) .onEnd(() { savedRotation.value rotation.value; });组合手势的时候要用.simultaneousWithExternalGesture()或.simultaneousWithInternalGesture()。比如const composed Gesture.Simultaneous(pan, pinch, rotationGesture);然后GestureDetector的gesture属性用composed对象。这里的细节是Pan手势和Pinch手势默认不是同时响应的因为系统会试图判定用户到底是想滑动还是捏合。如果你希望拖拽缩放并行就必须显式声明simultaneous关系。不加这行的话你会发现双指放大的时候卡片纹丝不动或者拖拽时缩放偶尔失灵。另一个细节是savedScale和savedRotation这两个中间变量。它们的用途和拖拽里的startX一样都是为了在手势开始时保存一个基准值。如果直接在onUpdate里用scale.value * event.scale做累乘累积误差会让缩放倍数和实际手指间距严重不成比例。3.4 实战附加轻扫删除列表项的阻尼判断再举一个常见场景左滑删除。这个场景有一点值得展开——如何判断滑多远才算“删除”。生产环境里不能只靠“松手就回弹”而是要做一个阈值判断滑过一半松手自动滑出并触发删除没滑过一半松手回弹。const offsetX useSharedValue(0); const itemHeight 80; const pan Gesture.Pan() .onUpdate((event) { offsetX.value Math.max(-120, Math.min(0, event.translationX)); }) .onEnd((event) { if (event.translationX -60) { offsetX.value withTiming(-120, { duration: 150 }); } else { offsetX.value withSpring(0); } });这里有几个细节可以琢磨。第一onUpdate里用Math.max/Math.min把offsetX限制在[-120, 0]区间这样用户往左滑最多滑出120像素不会无限滑动往右滑最多回到0不会被拉出右边界。第二阈值判断用的是event.translationX而不是offsetX.value因为onEnd事件里translationX代表手势结束时的总平移量直接拿它判断最直观。第三删除操作不要放在onEnd里同步执行建议用runOnJS触发一个JS函数否则UI线程无法安全调用React组件方法。3.5 配置手势冲突何时用Native何时用JSGesture Handler最让人头疼的地方就是手势冲突尤其涉及ScrollView列表的时候。比如你有一个垂直滚动的列表同时列表项支持水平拖拽这时系统必须决定用户的操作到底属于滚动还是拖拽。此时需要给手势设置requireExternalGestureToFail或blocksExternalGesture。以“垂直滚动列表水平滑动删除”为例const pan Gesture.Pan() .activeOffsetX([-10, 10]) .failOffsetY([-15, 15]);activeOffsetX的意思是当横向位移超过10像素时才让这个手势激活failOffsetY的意思是如果纵向位移超过15像素就放弃这个手势。这样用户横向滑动时触发删除纵向滑动时滚动容器接管二者互不干扰。这个配置比手动来管理手势状态优雅得多也避免了很多“列表滚动不流畅”的抱怨。如果你集成的是TabView或类似有横向滚动的容器手势优先级会变成另一个问题。此时可以在Pan手势上添加.simultaneousWithExternalGesture()或者用.requireExternalGestureToFail()来控制优先级。总体原则是先想清楚用户手指的意图再配置对应的偏移阈值和失败条件不要一上来就禁用滚动容器的原生手势。4. 常见问题与排查技巧实录4.1 手势失效、卡顿或动画闪烁的排查思路我把自己排查这类问题的经验整理成一个速查表按出现频率排序症状可能原因排查方法手势完全无响应没有用GestureHandlerRootView包裹根节点或目标区域检查入口文件根节点是否被GestureHandlerRootView包裹只有部分手势生效组合手势未声明同时执行或依赖关系检查是否使用了Gesture.Simultaneous或requireExternalGestureToFail动画偶发抖动在worklet之外读取或写入sharedValue检查所有sharedValue的读写是否发生在worklet函数内拖拽时视图跳动onUpdate里直接用translationX覆盖坐标改用startX event.translationX的模式松手回弹过度抖withSpring刚度太低、阻尼太高调高stiffness调低damping运行时抛worklet错误babel插件顺序不对或变量被外部捕获检查babel.config.js插件顺序把reanimated插件放最后启动时红屏Reanimated版本和RN版本不兼容检查react-native-reanimated的升级文档必要时用npx react-native run-android重编译4.2 为什么用runOnJS有时会丢数据worklet不能直接调用JS线程的函数所以需要runOnJS。但注意runOnJS并不保证函数参数在你调用时还保持原始引用——尤其当参数包含复杂对象时跨线程通信可能被序列化。我在项目里遇到过一个很诡异的bug从UI线程调用runOnJS触发一个更新函数函数接收一个对象但对象在到达JS线程时某些字段变成了undefined。排查后发现原来这个对象在UI线程被修改过多次最终捕获的引用已经不可靠。解决办法是只传递可序列化的原始数据比如数字和字符串。如果非要传对象用结构化克隆能安全支持的对象并且尽量避免大对象。更好的方式是让JS线程侧维护一份最新状态UI线程只负责传关键事件IDJS线程根据ID查表取出最新数据。4.3 性能监控与调优实战Reanimated 3提供的性能体验最终还是要落到数字上。我建议在调试阶段开启FPS监控import { PerformanceObserver } from react-native-performance;或者直接在DevTools的Performance标签页观察帧率。更简单粗暴的方式是打开开发者菜单里的Perf Monitor上面会显示实时FPS。判断丝滑与否我的经验标准是在手势拖拽过程中FPS始终高于55掉帧率不超过5%。如果达不到优先检查动画依赖的属性——比如transform里是否同时驱动了layout相关的width或height这类变化会触发原生布局计算远比重绘transform昂贵。另外一个调优思路是减少过度更新。假设有多个Animated组件都监听了同一个sharedValue只要这个值一变所有组件都会在UI线程重新计算样式。如果这些组件只是部分样式相关可以把共享值拆分更细。比如用一个对象sharedValue存所有参数看似方便实际任何一个字段变化都会触发所有依赖它的useAnimatedStyle。更好的做法是拆成独立的translateX、translateY、scale分别管理让无关组件不会被垃圾刷新。5. 从工程视角看Reanimated 3与Gesture Handler的协作5.1 架构设计动画状态与手势状态如何解耦上面讲了不少具体API现在把视野拉高一点从工程角度看这两个库怎么配合才能让项目长期维护下去。我见过太多团队把动画逻辑写在组件文件里两百行的组件同时承载了UI结构、网络请求、手势逻辑、动画状态最后改一个手势拖动范围能引发一堆样式问题。好的做法是把手势、动画和UI组件做一个清晰的层次划分。首先在组件外部定义独立的“手势-动画”hook。比如useDraggableCard它接受初始位置和目标回调返回一个gesture对象和animatedStyle。这个hook内部封装了sharedValue、手势配置和动画函数对外暴露的API极少。UI组件只需要拿到gesture和animatedStyle直接挂到GestureDetector和Animated.View上。这样如果后续要改手势行为不影响组件树要复用同一套拖拽逻辑直接把hook拿过去用。其次复杂交互建议用状态机管理手势阶段。Gesture Handler的事件有明确的生命周期BEGAN、ACTIVE、END、FAILED等。Reanimated 3的worklet里可以通过event.state判断当前阶段。如果你在做一个可交互的编辑器画面不同阶段需要显示不同辅助线用状态机会让代码直观很多。我习惯在手势onUpdate里用一个type字段记录当前阶段然后在useAnimatedStyle里根据阶段决定返回哪些辅助样式。5.2 动效设计中的“物理正确”原则很多人调弹簧动画调半天还是觉得“手感不对”是因为只改了damping和stiffness两个参数却没有从物理层面理解它们的作用。可以这样类比stiffness相当于弹簧的硬度和弹性系数。数值越大弹簧越难被拉伸回弹时也会更快到达目标位置。damping相当于空气阻力。数值越小物体回弹次数越多像失重环境里的弹跳数值越大回弹越慢接近黏稠液体里的感觉。手感细腻的拖拽卡片不要求每次都用同一个弹簧配置。我习惯在用户快速甩动时给一个更高的stiffness让动画“追”上手指的速度在慢速拖动时给一个中等stiffness保持柔顺感。判断“快速”可以用onEnd事件里velocityX或velocityY的值。示例.onEnd((event) { const speed Math.hypot(event.velocityX, event.velocityY); if (speed 1200) { offsetX.value withSpring(0, { damping: 20, stiffness: 300 }); } else { offsetX.value withSpring(0, { damping: 15, stiffness: 150 }); } });实测中这个细节带来的感受差异比调半天easing曲线明显得多。动画要服务的是人的直觉人甩动越快物体就应该越干脆地停下来。5.3 实战项目里的取舍与扩展思路如果项目里已经有一个运行了很久的RN应用里面的旧动画很多是用Animated API实现的迁移到Reanimated 3要不要一步到位我的建议是不必全量迁移优先把手势相关的交互迁移过来。理由很实际——Animated API已经能胜任transition类的动画但如果涉及手势跟手性和物理反馈它先天架构有短板。做迁移的时候可以从最核心的“拖拽卡片”“轻扫删除”“手势开关”这些交互开始一旦体验提升再逐步扩大到其他动画样式。扩展性方面Reanimated 3配合react-native-redash这类工具库会更顺手。redash提供很多内置函数比如夹取clamp、映射map、转换为cubicBezier能让worklet代码简洁不少。但要注意redash的版本和Reanimated版本匹配很重要否则可能出现函数在UI线程不可用的报错。平时写worklet的时候也尽量用纯函数不要依赖外部库这样未来升级库版本时工作量大减。6. 一个完整的综合示例可拖拽、可缩放、带删除功能的卡片组件我把上面的知识点揉在一起写一个稍微综合的示例。这个组件能拖拽、双指缩放旋转并且左滑超过阈值后触发删除。代码较长但每一段都有注释可以直接照着敲。import React from react; import { StyleSheet, Text, View } from react-native; import { Gesture, GestureDetector } from react-native-gesture-handler; import Animated, { useSharedValue, useAnimatedStyle, withSpring, withTiming, runOnJS, } from react-native-reanimated; function SwipeableCard({ onDelete }) { const translateX useSharedValue(0); const translateY useSharedValue(0); const startX useSharedValue(0); const startY useSharedValue(0); const scale useSharedValue(1); const savedScale useSharedValue(1); const rotation useSharedValue(0); const savedRotation useSharedValue(0); const emitDelete () onDelete onDelete(); const pan Gesture.Pan() .onStart(() { startX.value translateX.value; startY.value translateY.value; }) .onUpdate((event) { // 支持水平左滑阈值判断和垂直自由拖拽并存 translateX.value startX.value event.translationX; translateY.value startY.value event.translationY; }) .onEnd((event) { const shouldDelete event.translationX -80; if (shouldDelete) { translateX.value withTiming(-500, { duration: 200 }); runOnJS(emitDelete)(); } else if (event.translationX 80) { translateX.value withSpring(0); } else { translateX.value withSpring(0); translateY.value withSpring(0); } }); const pinch Gesture.Pinch() .onUpdate((event) { scale.value savedScale.value * event.scale; }) .onEnd(() { savedScale.value scale.value; }); const rotate Gesture.Rotation() .onUpdate((event) { rotation.value savedRotation.value event.rotation; }) .onEnd(() { savedRotation.value rotation.value; }); const composed Gesture.Simultaneous(pan, pinch, rotate); const animatedStyle useAnimatedStyle(() { return { transform: [ { translateX: translateX.value }, { translateY: translateY.value }, { scale: scale.value }, { rotate: ${rotation.value}rad }, ], opacity: translateX.value -50 ? 1 translateX.value / 500 : 1, }; }); return ( View style{styles.cardWrapper} GestureDetector gesture{composed} Animated.View style{[styles.card, animatedStyle]} Text style{styles.cardText}拖我、捏我、划我/Text /Animated.View /GestureDetector /View ); } const styles StyleSheet.create({ cardWrapper: { flex: 1, justifyContent: center, alignItems: center, }, card: { width: 180, height: 180, borderRadius: 24, backgroundColor: #8b5cf6, justifyContent: center, alignItems: center, }, cardText: { color: #fff, fontSize: 16, fontWeight: 600, }, }); export default SwipeableCard;这个示例的亮点在opacity那行滑到-50之后透明度开始变化让用户预览到删除的动作。借助表达式1 translateX.value / 500当translateX为-500时opacity变为0动画自动渐进不需要额外的useAnimatedStyle监听。实际运行时你会感受到几个关键点拖拽阶段完全跟手没有延迟捏合缩放和拖拽可以同时进行不互相打断左滑超过阈值后卡片会快速滑出并回调删除未超过阈值时卡片以弹簧回弹的方式回到原位回弹过程非常自然7. 我在生产中反复验证的几个经验这套组合用了两年多有些经验很想分享给正要上手的同学。第一动画相关变量尽量都放到sharedValue里不要“混合双写”。我在早期项目里试过一部分属性用Reanimated驱动一部分还用StyleSheet的state控制结果导致界面状态无法同步一个动画结束后另一个属性还在旧位置。一旦决定用Reanimated就把该组件所有动态属性都纳入它的体系。第二不要用setState更新频繁变化的属性哪怕setState写在runOnJS里也不行。setState会触发整个组件重渲染即使渲染被React优化过开销也远大于sharedValue的精准更新。遇到需要反映手势位置的文本内容比如“当前进度50%”建议直接把文本组件做成Animated分量在useAnimatedStyle里更新它。第三真机调试和模拟器表现差异很大。Gesture Handler在模拟器里表现的流畅度会偏高因为模拟器触摸事件是鼠标模拟的采样率低很多卡顿问题不会被暴露。Android模拟器尤其明显。所以涉及手势流畅度的验证一定要在真机上做最好还分别用低端和中端机型测一遍。我见过团队在模拟器上觉得效果完美上线后用户普遍抱怨卡顿最后定位下来是Reanimated版本在低端Android机的JS引擎初始化开销偏大冷启动时白屏时间明显增加。第四手势冲突配置一定要提前规划。不要在已经出现“列表滚动卡死”后再补规则。做列表项滑动操作时先想清楚整条手势链最内层是什么手势外层是否可滚动滚动容器是否同时支持横向和纵向。配置主动作偏移量比事后修要省心得多。第五worklet代码要考虑生产环境压缩问题。如果配置了字节码压缩worklet序列化可能被破坏。简单说RN 0.72之前的字节码格式和Reanimated 3配合偶尔有兼容问题。如果你出包后遇到UI线程执行异常排查一下是否开了字节码Hermes配置。新版本这块已经好很多但升级依赖时还是要注意看发布说明。最后如果你做的是跨端项目iOS和Android在手势冲突解决的底层实现并不完全一致。比如iOS上旋转手势很灵敏Android上需要稍微大一点的旋转才会触发。遇到这类差异不要硬调一个统一参数而是分别在两边微调保证用户在手势识别上都有流畅体验。这个方案后续可以扩展的方向也很多比如配合react-native-worklets做更多跨线程计算或者把动画状态与后端状态同步做多端实时交互。能力边界很宽值得持续投入。
返回列表