Reanimated动画性能优化与开发踩坑经验分享

上官奥哲 移动 阅读 2,011
赞 17 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近刚做完一个电商类的小程序,核心功能是商品详情页的交互动画。说白了就是那种用户上下滑动时,商品图片会跟着缩放、位移,还会触发一些动态效果。

Reanimated动画性能优化与开发踩坑经验分享

最开始我其实想用原生的Animated库来做,毕竟它简单易用。但写了一版demo后发现性能不太理想,尤其是在低端安卓机上卡顿很明显。后来同事推荐了Reanimated 2,说它的性能优化做得不错,底层直接用C++实现的,听起来挺靠谱。

不过说实话,刚开始我对这玩意儿是有点抗拒的。为啥?因为文档太简陋了,很多API都没有详细说明,官方例子也不够丰富。但最后还是硬着头皮上了,毕竟性能才是王道。

最大的坑:性能问题

项目中遇到了几个让人头疼的问题,首当其冲的就是性能调优。刚把Reanimated集成进来的时候,我发现虽然比Animated流畅了不少,但依然存在掉帧的情况。

经过一番排查,发现问题出在以下几个地方:

  • 过度依赖JS线程:有些逻辑我习惯性地写在JS里,结果发现这些代码还是会在主线程执行,导致卡顿。
  • 复杂的共享值计算:一开始我把所有的动画逻辑都塞在一个useSharedValue里,结果计算量太大,直接影响了渲染效率。
  • 不必要的重渲染:有些组件明明不需要更新,却因为状态变化被重新渲染了。

后来我调整了方案,把所有动画相关的计算都尽量移到worklet里去处理。比如下面这段代码:

import { useSharedValue, useAnimatedStyle, withTiming } from 'react-native-reanimated';

const offset = useSharedValue(0);

const animatedStyle = useAnimatedStyle(() => {
  return {
    transform: [{ translateY: offset.value }],
  };
});

// 在某个事件中更新offset
offset.value = withTiming(newOffset, { duration: 300 });

这里的核心就是把动画逻辑放到worklet里,这样可以绕过JS线程,直接在UI线程运行,性能提升非常明显。

踩坑提醒:手势冲突问题

另一个让我折腾了很久的问题是手势冲突。我们的页面里既有ScrollView的滚动,又有拖拽的手势操作,结果发现两者经常互相干扰。

举个例子,用户在图片上滑动时,系统不知道应该触发ScrollView的滚动,还是触发图片的拖拽动画。这种问题真的很烦人。

我的解决思路是通过simultaneousHandlersshouldCancelWhenOutside来协调这两个手势。具体代码如下:

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

const dragY = useSharedValue(0);
const scrollViewRef = useRef(null);

const dragGesture = Gesture.Pan()
  .onStart(() => {
    // 阻止ScrollView滚动
    scrollViewRef.current?.setNativeProps({ scrollEnabled: false });
  })
  .onUpdate((event) => {
    dragY.value = event.translationY;
  })
  .onEnd(() => {
    // 恢复ScrollView滚动
    scrollViewRef.current?.setNativeProps({ scrollEnabled: true });
  });

return (
  <ScrollView ref={scrollViewRef}>
    <GestureDetector gesture={dragGesture}>
      <Animated.View style={animatedStyle} />
    </GestureDetector>
  </ScrollView>
);

这个方案亲测有效,但也不是完美无缺。比如在某些极端情况下(快速切换手势),还是会有一点点卡顿,不过影响不大,暂时就这么用了。

最终的解决方案

除了上面提到的两个大坑,还有一些小问题也值得一提。比如Reanimated的调试工具支持不太好,出了问题很难定位。后来我干脆自己写了一些日志函数,手动打印关键变量的值。

还有一个问题就是动画的边界处理。用户滑动太快或者超出范围时,动画会显得很生硬。我的做法是加了一个阻尼效果:

const clampedOffset = useDerivedValue(() => {
  return Math.max(-100, Math.min(100, offset.value));
});

这样一来,即使用户滑动过头了,动画也不会一下子崩掉。

回顾与反思

总体来说,这次用Reanimated的体验还算不错。性能确实比Animated强不少,尤其是在复杂动画场景下。不过也踩了不少坑,主要是因为文档不够完善,很多时候只能靠自己摸索。

如果让我重新选一次,我还是会选择Reanimated,但会提前做好更充分的技术调研。另外,我觉得有些地方还可以继续优化:

  • 手势冲突的处理还能更优雅一些,目前的方案略显粗糙。
  • 部分动画的边界条件还可以再精细化调整。
  • 希望能找到更好的调试方法,减少排查问题的时间。

以上是我个人对这个项目的完整讲解,有更优的实现方式欢迎评论区交流。Reanimated的确是个好工具,但也需要花时间去熟悉它的脾气。希望我的踩坑经验能帮到你!

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论