React Native Reanimated 4 动画开发完全指南:CSS 动画、过渡效果与手势交互实战

Reanimated 4 将 CSS 动画和过渡效果带入 React Native,本文从 SharedValue、Worklet 核心概念到 CSS 过渡、关键帧动画、手势驱动交互,通过底部面板、缩放图片等实战案例,全面讲解动画开发与性能优化。

Reanimated 4 动画开发完全指南 2026:CSS与手势

引言:React Native 动画的新纪元

2026 年,React Native 生态终于迎来了一个让人兴奋的里程碑——Reanimated 4 正式发布稳定版。说实话,这是自 Reanimated 2 引入 Worklet 以来,我觉得最让人期待的一次大版本升级。它把 CSS 动画和过渡效果带进了 React Native 的世界,同时还保持了跟 Worklet 动画系统的完全向后兼容。

这意味着什么呢?简单来说,你现在可以用熟悉的 CSS 动画语法来构建流畅的 60+ FPS 原生动画。而当遇到手势控制、滚动驱动这类复杂场景时,依然可以用强大的 Worklet API。

本文会从核心概念到实战案例,带你全面掌握 Reanimated 4 的动画开发。话不多说,我们开始吧。

安装与环境配置

前置要求

先说个重要的事:Reanimated 4 只支持 React Native 新架构(Fabric)。所以你得确保项目用的是 React Native 0.76 或更高版本。如果还在旧架构上,就需要先完成迁移,或者暂时继续用 Reanimated 3.x。

Expo 项目安装

npx expo install react-native-reanimated react-native-worklets

Expo 项目比较省心——Reanimated 的 Babel 插件已经在 babel-preset-expo 里自动配好了,不需要额外操作。

React Native CLI 项目安装

npm install react-native-reanimated react-native-worklets
cd ios && pod install && cd ..

装完之后,需要去 babel.config.js 里把插件从 'react-native-reanimated/plugin' 改成 'react-native-worklets/plugin'

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: [
    // ... 其他插件
    'react-native-worklets/plugin', // 必须放在最后
  ],
};

划重点:Worklet 插件必须放在 plugins 数组的最后一个位置。这点千万别忘了,否则 Worklet 代码没法正确处理,debug 起来会很头疼。

架构变更:Worklet 独立化

Reanimated 4 有个值得关注的架构变更——Worklet 被抽到了独立的 react-native-worklets 包里。这种模块化设计挺聪明的,意味着其他库(不只是 Reanimated)也能用上 Worklet 的能力,对整个 React Native 生态来说是件好事。

核心概念详解

SharedValue(共享值)

共享值是 Reanimated 动画系统的地基。它们是同时存在于 JavaScript 线程和 UI 线程的特殊变量——你在 JS 端更新数据,UI 线程直接读取来刷新界面,中间没有任何桥接通信的开销

import { useSharedValue } from 'react-native-reanimated';

function MyComponent() {
  const offset = useSharedValue(0);
  const isActive = useSharedValue(false);

  // 通过 .value 属性读写
  const handlePress = () => {
    offset.value = 100;
    isActive.value = true;
  };

  // 使用 React Compiler 时,推荐使用 get/set 方法
  const handlePressWithCompiler = () => {
    offset.set(100);
    isActive.set(true);
    console.log(offset.get()); // 100
  };

  return /* ... */;
}

注意:如果你的项目用了 React Compiler,就别直接访问 .value 属性了,改用 .get().set() 方法来确保兼容性。这个坑我见不少人踩过。

useAnimatedStyle(动画样式)

useAnimatedStyle 是连接共享值和组件样式的桥梁。它接受一个回调函数,共享值一变,样式就自动重算——整个过程都在 UI 线程跑,完全不阻塞 JS 线程。

import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withTiming,
  Easing,
} from 'react-native-reanimated';
import { View, Button, StyleSheet } from 'react-native';

export default function AnimatedBox() {
  const width = useSharedValue(100);

  const animatedStyle = useAnimatedStyle(() => ({
    width: withTiming(width.value, {
      duration: 500,
      easing: Easing.bezier(0.25, 0.1, 0.25, 1),
    }),
  }));

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, animatedStyle]} />
      <Button
        title="随机宽度"
        onPress={() => {
          width.value = Math.random() * 300 + 50;
        }}
      />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, alignItems: 'center', justifyContent: 'center' },
  box: { height: 80, backgroundColor: '#6C63FF', borderRadius: 12 },
});

关键点:动画样式必须用在 Animated 命名空间下的组件上(比如 Animated.ViewAnimated.Text),普通的 View 上用了等于没用。这是新手最容易犯的错误之一。

Worklet(工作线程函数)

Worklet 是标记了 'worklet' 的函数,它们直接在 UI 线程而不是 JS 主线程上运行。这也是 Reanimated 能实现丝滑动画的关键——就算 JS 线程忙得不可开交,动画照样不卡。

import { runOnJS } from 'react-native-reanimated';

// Worklet 函数示例
function clampValue(value, min, max) {
  'worklet';
  return Math.min(Math.max(value, min), max);
}

// 在 Worklet 中调用 JS 线程函数
function updateState(newValue) {
  // 这是一个普通 JS 函数
  setState(newValue);
}

function onGestureUpdate(value) {
  'worklet';
  const clamped = clampValue(value, 0, 100);
  // 需要通过 runOnJS 回到 JS 线程
  runOnJS(updateState)(clamped);
}

Worklet 本质上是闭包,能访问外部作用域的变量。不过这里有个性能陷阱需要注意:只有 Worklet 里实际引用的变量才会被捕获。千万别在 Worklet 里捕获大型对象,不然性能会很糟糕。正确的做法是先把需要的属性提出来:

// ❌ 不推荐:捕获整个对象
const config = { threshold: 50, maxSpeed: 200, /* ... 更多属性 */ };
const worklet = () => {
  'worklet';
  return config.threshold; // 整个 config 对象都被捕获了
};

// ✅ 推荐:只捕获需要的值
const threshold = config.threshold;
const worklet = () => {
  'worklet';
  return threshold; // 只捕获一个数字
};

Reanimated 4 新特性:CSS 动画与过渡

这是 Reanimated 4 最重磅的新功能。Software Mansion 团队一直在琢磨一件事——怎么给最常见的那类动画(状态变化触发的动画)提供一个又简洁又好用的声明式 API。最终他们选了 CSS 动画和过渡标准,说实话这个决定很明智。毕竟 CSS 动画是经过了无数 Web 项目实战验证的规范,Web 开发者基本零学习成本就能上手。

CSS 过渡效果(Transitions)

CSS 过渡提供了一种非常简洁的声明式方式来处理状态变化驱动的动画。你只需要告诉它"哪些属性要动画"和"怎么动",然后更新状态就行——剩下的全交给 Reanimated。

import Animated from 'react-native-reanimated';
import { useState } from 'react';
import { Pressable, Text, StyleSheet } from 'react-native';

export default function TransitionDemo() {
  const [expanded, setExpanded] = useState(false);

  return (
    <Pressable onPress={() => setExpanded(!expanded)}>
      <Animated.View
        style={{
          width: expanded ? 300 : 150,
          height: expanded ? 200 : 80,
          backgroundColor: expanded ? '#6C63FF' : '#A29BFE',
          borderRadius: expanded ? 24 : 12,
          // CSS 过渡属性
          transitionProperty: ['width', 'height', 'backgroundColor', 'borderRadius'],
          transitionDuration: 400,
          transitionTimingFunction: 'ease-in-out',
        }}
      >
        <Text style={styles.text}>
          {expanded ? '点击收起' : '点击展开'}
        </Text>
      </Animated.View>
    </Pressable>
  );
}

支持的过渡属性有这些:

  • transitionProperty — 指定哪些样式属性要做动画
  • transitionDuration — 过渡持续时间(毫秒)
  • transitionDelay — 延迟多久开始过渡(毫秒)
  • transitionTimingFunction — 缓动函数,比如 'ease''ease-in-out''linear'
  • transitionBehavior — 过渡行为控制

跟传统 Worklet 写法比起来,CSS 过渡的好处是代码量大幅减少。不用创建 SharedValue,不用写 useAnimatedStyle 回调,直接在 style 里声明过渡规则就完事了。对于简单的状态切换动画来说,体验提升非常明显。

CSS 关键帧动画(Keyframe Animations)

过渡处理的是从状态 A 到状态 B 的变化。但如果你想做多步骤的动画序列呢?那就得用关键帧动画了。你可以精确控制动画时间线上每个节点的样式——有点像在做动画分镜。

import Animated from 'react-native-reanimated';

// 脉冲动画关键帧
const pulse = {
  from: {
    transform: [{ scale: 1 }],
    opacity: 1,
  },
  '50%': {
    transform: [{ scale: 1.15 }],
    opacity: 0.8,
  },
  to: {
    transform: [{ scale: 1 }],
    opacity: 1,
  },
};

// 旋转加淡入动画
const spinFadeIn = {
  '0%': {
    transform: [{ rotate: '0deg' }, { scale: 0.5 }],
    opacity: 0,
  },
  '70%': {
    transform: [{ rotate: '300deg' }, { scale: 1.1 }],
    opacity: 0.9,
  },
  '100%': {
    transform: [{ rotate: '360deg' }, { scale: 1 }],
    opacity: 1,
  },
};

export default function CSSKeyframeDemo() {
  return (
    <Animated.View
      style={{
        width: 120,
        height: 120,
        backgroundColor: '#FF6B6B',
        borderRadius: 60,
        // CSS 关键帧动画属性
        animationName: pulse,
        animationDuration: 2000,
        animationIterationCount: 'infinite',
        animationTimingFunction: 'ease-in-out',
      }}
    />
  );
}

关键帧动画支持的属性:

  • animationName — 关键帧定义对象
  • animationDuration — 单次动画时长(毫秒)
  • animationDelay — 开始前的延迟(毫秒)
  • animationIterationCount — 重复次数,或者 'infinite' 无限循环
  • animationDirection — 播放方向('normal''reverse''alternate'
  • animationFillMode — 填充模式('forwards''backwards''both'
  • animationTimingFunction — 缓动函数

CSS 动画预设库

Software Mansion 还贴心地提供了 react-native-css-animations 预设库,里面有一大堆现成的动画效果可以直接用:

npm install react-native-css-animations
import { bounce, fadeIn, shakeX } from 'react-native-css-animations';

// 直接应用预设动画
<Animated.View
  style={{
    animationName: bounce,
    animationDuration: 1000,
    animationIterationCount: 'infinite',
  }}
/>

对于一些常见的动画效果(弹跳、抖动、淡入淡出之类的),用预设库能省不少时间。

布局动画:进场与退场

布局动画解决的是一个很实际的问题:元素被添加到界面或从界面移除时,怎么让它优雅地出现和消失?Reanimated 提供了丰富的预定义动画,当然你也可以用 Keyframe 来自定义。

预定义进场/退场动画

import Animated, {
  FadeIn,
  FadeOut,
  SlideInLeft,
  SlideOutRight,
  BounceIn,
  ZoomIn,
  FlipInXUp,
  Layout,
} from 'react-native-reanimated';
import { useState } from 'react';
import { Button, View, StyleSheet } from 'react-native';

export default function LayoutAnimationDemo() {
  const [items, setItems] = useState([1, 2, 3]);

  const addItem = () => {
    setItems([...items, Date.now()]);
  };

  const removeItem = (id) => {
    setItems(items.filter((item) => item !== id));
  };

  return (
    <View style={styles.container}>
      <Button title="添加项目" onPress={addItem} />
      {items.map((item) => (
        <Animated.View
          key={item}
          entering={SlideInLeft.duration(400).springify()}
          exiting={SlideOutRight.duration(300)}
          layout={Layout.springify()}
          style={styles.item}
        >
          <Pressable onPress={() => removeItem(item)}>
            <Text>项目 {item} (点击删除)</Text>
          </Pressable>
        </Animated.View>
      ))}
    </View>
  );
}

自定义修饰链

所有预定义动画都支持链式调用来做自定义。这个 API 设计得挺舒服的:

// 自定义进场动画
FadeIn
  .delay(200)           // 延迟 200ms
  .duration(600)        // 持续 600ms
  .springify()          // 使用弹簧物理效果
  .damping(12)          // 弹簧阻尼
  .withCallback((finished) => {
    'worklet';
    console.log('动画完成:', finished);
  });

Keyframe 进场/退场动画

当预定义动画不够用的时候(总有那种设计师给的天马行空的动效需求吧),可以用 Keyframe 类来定义完全自定义的进退场效果:

import { Keyframe, Easing } from 'react-native-reanimated';

const customEntering = new Keyframe({
  0: {
    opacity: 0,
    transform: [{ translateY: -50 }, { scale: 0.8 }, { rotate: '-10deg' }],
  },
  60: {
    opacity: 0.8,
    transform: [{ translateY: 10 }, { scale: 1.05 }, { rotate: '2deg' }],
    easing: Easing.out(Easing.quad),
  },
  100: {
    opacity: 1,
    transform: [{ translateY: 0 }, { scale: 1 }, { rotate: '0deg' }],
  },
}).duration(800);

const customExiting = new Keyframe({
  from: {
    opacity: 1,
    transform: [{ translateX: 0 }, { scale: 1 }],
  },
  to: {
    opacity: 0,
    transform: [{ translateX: 200 }, { scale: 0.5 }],
  },
}).duration(500);

// 使用
<Animated.View entering={customEntering} exiting={customExiting}>
  {/* 内容 */}
</Animated.View>

小提醒:如果要动画 transform 属性,确保所有关键帧里 transform 数组内的变换属性顺序要一致。不然你会遇到一些很诡异的动画表现。

手势驱动动画

手势交互可以说是移动端动画最核心的场景了。Reanimated 4 跟 react-native-gesture-handler 的集成非常深入——手势回调自动在 UI 线程跑,响应速度几乎零延迟。

拖拽与弹回效果

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

export default function DraggableCard() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const scale = useSharedValue(1);

  const panGesture = Gesture.Pan()
    .onStart(() => {
      scale.value = withSpring(1.1);
    })
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
      scale.value = withSpring(1);
    });

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

  return (
    <GestureDetector gesture={panGesture}>
      <Animated.View style={[styles.card, animatedStyle]}>
        {/* 卡片内容 */}
      </Animated.View>
    </GestureDetector>
  );
}

const styles = StyleSheet.create({
  card: {
    width: 200,
    height: 280,
    backgroundColor: '#6C63FF',
    borderRadius: 20,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 8,
  },
});

实战案例:可滑动底部面板

底部面板(Bottom Sheet)几乎是每个移动应用都会用到的交互模式。下面这个例子展示了怎么用手势和 Worklet 做一个可拖拽、带吸附点的底部面板——我个人觉得这是 Reanimated 手势动画最典型的应用场景之一:

import { GestureDetector, Gesture } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
  interpolate,
  Extrapolation,
} from 'react-native-reanimated';
import { Dimensions, StyleSheet, View } from 'react-native';

const SCREEN_HEIGHT = Dimensions.get('window').height;
const SNAP_POINTS = {
  COLLAPSED: SCREEN_HEIGHT - 100,
  HALF: SCREEN_HEIGHT * 0.5,
  EXPANDED: 80,
};

export default function BottomSheet({ children }) {
  const translateY = useSharedValue(SNAP_POINTS.COLLAPSED);
  const startY = useSharedValue(0);

  const panGesture = Gesture.Pan()
    .onStart(() => {
      startY.value = translateY.value;
    })
    .onUpdate((event) => {
      translateY.value = Math.max(
        SNAP_POINTS.EXPANDED,
        startY.value + event.translationY
      );
    })
    .onEnd((event) => {
      const velocity = event.velocityY;
      const currentY = translateY.value;

      // 根据速度和位置决定吸附到哪个点
      let snapPoint;
      if (velocity > 500) {
        // 快速下滑 → 收起
        snapPoint = SNAP_POINTS.COLLAPSED;
      } else if (velocity < -500) {
        // 快速上滑 → 展开
        snapPoint = SNAP_POINTS.EXPANDED;
      } else if (currentY < SCREEN_HEIGHT * 0.35) {
        snapPoint = SNAP_POINTS.EXPANDED;
      } else if (currentY < SCREEN_HEIGHT * 0.65) {
        snapPoint = SNAP_POINTS.HALF;
      } else {
        snapPoint = SNAP_POINTS.COLLAPSED;
      }

      translateY.value = withSpring(snapPoint, {
        damping: 20,
        stiffness: 150,
        mass: 0.8,
      });
    });

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

  const backdropStyle = useAnimatedStyle(() => ({
    opacity: interpolate(
      translateY.value,
      [SNAP_POINTS.EXPANDED, SNAP_POINTS.COLLAPSED],
      [0.5, 0],
      Extrapolation.CLAMP
    ),
  }));

  return (
    <>
      <Animated.View style={[styles.backdrop, backdropStyle]} />
      <GestureDetector gesture={panGesture}>
        <Animated.View style={[styles.sheet, sheetStyle]}>
          <View style={styles.handle} />
          {children}
        </Animated.View>
      </GestureDetector>
    </>
  );
}

const styles = StyleSheet.create({
  backdrop: {
    ...StyleSheet.absoluteFillObject,
    backgroundColor: '#000',
  },
  sheet: {
    position: 'absolute',
    left: 0,
    right: 0,
    height: SCREEN_HEIGHT,
    backgroundColor: '#fff',
    borderTopLeftRadius: 24,
    borderTopRightRadius: 24,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: -4 },
    shadowOpacity: 0.15,
    shadowRadius: 12,
    elevation: 16,
    paddingHorizontal: 20,
  },
  handle: {
    width: 40,
    height: 5,
    backgroundColor: '#DDD',
    borderRadius: 3,
    alignSelf: 'center',
    marginTop: 10,
    marginBottom: 20,
  },
});

手势组合:双击缩放

实际开发中经常需要同时处理多种手势。比如一个图片查看器,既要支持双指缩放,又要支持双击放大,还要能拖拽平移。Reanimated 4 和 Gesture Handler 配合起来处理这种场景毫不费力:

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

export default function ZoomableImage() {
  const scale = useSharedValue(1);
  const savedScale = useSharedValue(1);
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);

  const pinchGesture = Gesture.Pinch()
    .onUpdate((event) => {
      scale.value = savedScale.value * event.scale;
    })
    .onEnd(() => {
      if (scale.value < 1) {
        scale.value = withSpring(1);
        savedScale.value = 1;
      } else {
        savedScale.value = scale.value;
      }
    });

  const doubleTap = Gesture.Tap()
    .numberOfTaps(2)
    .onEnd(() => {
      if (scale.value > 1) {
        scale.value = withTiming(1);
        savedScale.value = 1;
        translateX.value = withTiming(0);
        translateY.value = withTiming(0);
      } else {
        scale.value = withTiming(2.5);
        savedScale.value = 2.5;
      }
    });

  const panGesture = Gesture.Pan()
    .onUpdate((event) => {
      if (scale.value > 1) {
        translateX.value = event.translationX;
        translateY.value = event.translationY;
      }
    })
    .onEnd(() => {
      if (scale.value <= 1) {
        translateX.value = withSpring(0);
        translateY.value = withSpring(0);
      }
    });

  const composed = Gesture.Simultaneous(
    pinchGesture,
    Gesture.Race(doubleTap, panGesture)
  );

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

  return (
    <GestureDetector gesture={composed}>
      <Animated.Image
        source={{ uri: 'https://example.com/photo.jpg' }}
        style={[{ width: '100%', height: 400 }, animatedStyle]}
        resizeMode="contain"
      />
    </GestureDetector>
  );
}

CSS 动画 vs Worklet:到底怎么选?

有了两种动画方案之后,很多人会纠结该用哪个。其实选择起来并不复杂,关键看你的具体场景。

用 CSS 动画/过渡就够了的场景

  • 状态驱动的 UI 变化 — 展开/收起、显示/隐藏、主题切换这类
  • Loading 动画 — 旋转、脉冲、闪烁等循环效果
  • 按钮交互反馈 — 悬停、按下状态的视觉变化
  • 简单的入场动画 — 淡入、滑入等一次性效果
  • 任何已知起止状态的动画 — 你清楚地知道从 A 到 B 要怎么变

该用 Worklet 的场景

  • 手势驱动动画 — 拖拽、缩放、旋转这些需要逐帧控制的交互
  • 滚动驱动动画 — 视差效果、吸顶动画等基于滚动位置的效果
  • 屏幕转场动画 — 页面间的共享元素过渡
  • 多动画编排 — 需要从一个动画值派生出多个其他动画
  • 物理模拟 — 弹簧、衰减等需要实时计算的效果

混合使用示例

实际项目中,最聪明的做法是混合使用。比如一个卡片组件里,背景色切换用 CSS 过渡(简单高效),拖拽交互用 Worklet(精确控制):

import { Gesture, GestureDetector } from 'react-native-gesture-handler';
import Animated, {
  useSharedValue,
  useAnimatedStyle,
  withSpring,
} from 'react-native-reanimated';
import { useState } from 'react';
import { Pressable, Text } from 'react-native';

export default function HybridCard() {
  const [isActive, setIsActive] = useState(false);
  const translateX = useSharedValue(0);

  // 拖拽使用 Worklet
  const pan = Gesture.Pan()
    .onUpdate((e) => {
      translateX.value = e.translationX;
    })
    .onEnd(() => {
      translateX.value = withSpring(0);
    });

  const dragStyle = useAnimatedStyle(() => ({
    transform: [{ translateX: translateX.value }],
  }));

  return (
    <GestureDetector gesture={pan}>
      <Animated.View
        style={[
          dragStyle,
          {
            padding: 20,
            borderRadius: 16,
            // 背景色变化使用 CSS 过渡
            backgroundColor: isActive ? '#6C63FF' : '#E8E6FF',
            transitionProperty: ['backgroundColor'],
            transitionDuration: 300,
          },
        ]}
      >
        <Pressable onPress={() => setIsActive(!isActive)}>
          <Text>{isActive ? '已激活' : '未激活'}</Text>
        </Pressable>
      </Animated.View>
    </GestureDetector>
  );
}

性能优化最佳实践

Reanimated 4 的 CSS 声明式动画在性能方面有天然优势——框架能更好地理解你的动画意图,从而做出更优的调度。但还是有几个点值得注意。

1. 布局动画构建器要在组件外部定义

// ✅ 推荐:在组件外部定义
const enterAnimation = SlideInLeft.duration(300).springify();

function ListItem({ item }) {
  return (
    <Animated.View entering={enterAnimation}>
      {/* ... */}
    </Animated.View>
  );
}

// ❌ 避免:在组件内部每次渲染都重新创建
function ListItem({ item }) {
  return (
    <Animated.View entering={SlideInLeft.duration(300).springify()}>
      {/* ... */}
    </Animated.View>
  );
}

这个看起来是小事,但在长列表里影响可不小。每次渲染都重新创建动画对象,积少成多开销很可观。

2. 避免不必要的 SharedValue 频繁更新

// ✅ 推荐:使用 withSpring/withTiming 包装
offset.value = withSpring(targetValue);

// ❌ 避免:在高频事件中直接设置(如果不需要实时跟踪的话)
onScroll={(e) => {
  offset.value = e.nativeEvent.contentOffset.y; // 滚动监听中这样做是合理的
}}

3. CSS 过渡属性要精确指定

// ✅ 推荐:只过渡你需要的属性
transitionProperty: ['backgroundColor', 'borderRadius']

// ❌ 避免:过渡所有属性
transitionProperty: 'all'

'all' 虽然写起来方便,但性能开销更大。养成指定具体属性的习惯吧。

4. 记得用 cancelAnimation 做清理

import { cancelAnimation } from 'react-native-reanimated';
import { useEffect } from 'react';

function MyComponent() {
  const progress = useSharedValue(0);

  useEffect(() => {
    progress.value = withTiming(1, { duration: 2000 });

    return () => {
      // 组件卸载时取消动画
      cancelAnimation(progress);
    };
  }, []);
}

忘记取消动画是个常见的内存泄漏来源,尤其是在频繁挂载/卸载的组件上。

从 Reanimated 3 迁移

好消息:Reanimated 4 的向后兼容性做得很不错。你现有的 SharedValue、useAnimatedStyle 和 Worklet 代码基本不用改就能跑。不过还是有几个迁移步骤要走:

  1. 安装 react-native-worklets — 这是 v4 新增的依赖
  2. 更新 Babel 插件 — 把 'react-native-reanimated/plugin' 换成 'react-native-worklets/plugin'
  3. 确认新架构已启用 — v4 不再支持旧架构了
  4. 检查弃用 API — v4 移除了部分 v3 里标记为弃用的 API
  5. 逐步引入 CSS 动画 — 按自己的节奏在新代码里开始用,不需要一口气重写所有动画

如果你的应用还没迁移到新架构,建议先留在 Reanimated 3.x。别着急,先搞定架构迁移再升 v4 也不迟。

实战案例:动画通知提示(Toast)组件

最后,来个完整的实战。我们用 Reanimated 4 的 CSS 动画和布局动画来做一个带动画效果的 Toast 通知组件——这在实际项目里用得非常多:

import Animated, { FadeIn, FadeOut, Layout } from 'react-native-reanimated';
import { useState, useCallback, useRef } from 'react';
import { View, Text, Pressable, StyleSheet } from 'react-native';

const TOAST_TYPES = {
  success: { bg: '#10B981', icon: '✓' },
  error: { bg: '#EF4444', icon: '✕' },
  info: { bg: '#3B82F6', icon: 'ℹ' },
};

function Toast({ id, message, type, onDismiss }) {
  const config = TOAST_TYPES[type];

  return (
    <Animated.View
      entering={FadeIn.duration(300).springify().damping(15)}
      exiting={FadeOut.duration(200)}
      layout={Layout.springify()}
      style={[styles.toast, { backgroundColor: config.bg }]}
    >
      <Text style={styles.icon}>{config.icon}</Text>
      <Text style={styles.message}>{message}</Text>
      <Pressable
        onPress={() => onDismiss(id)}
        style={({ pressed }) => ({
          opacity: pressed ? 0.6 : 1,
        })}
      >
        <Text style={styles.dismiss}>关闭</Text>
      </Pressable>
    </Animated.View>
  );
}

export default function ToastContainer() {
  const [toasts, setToasts] = useState([]);
  const nextId = useRef(0);

  const addToast = useCallback((message, type = 'info') => {
    const id = nextId.current++;
    setToasts((prev) => [...prev, { id, message, type }]);

    // 3 秒后自动消失
    setTimeout(() => {
      setToasts((prev) => prev.filter((t) => t.id !== id));
    }, 3000);
  }, []);

  const dismissToast = useCallback((id) => {
    setToasts((prev) => prev.filter((t) => t.id !== id));
  }, []);

  return (
    <View style={styles.container}>
      {toasts.map((toast) => (
        <Toast
          key={toast.id}
          {...toast}
          onDismiss={dismissToast}
        />
      ))}

      {/* 测试按钮 */}
      <View style={styles.buttons}>
        <Pressable onPress={() => addToast('操作成功!', 'success')}>
          <Text>成功提示</Text>
        </Pressable>
        <Pressable onPress={() => addToast('发生错误', 'error')}>
          <Text>错误提示</Text>
        </Pressable>
        <Pressable onPress={() => addToast('这是一条信息', 'info')}>
          <Text>信息提示</Text>
        </Pressable>
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    position: 'absolute',
    top: 60,
    left: 20,
    right: 20,
    zIndex: 1000,
  },
  toast: {
    flexDirection: 'row',
    alignItems: 'center',
    padding: 16,
    borderRadius: 12,
    marginBottom: 8,
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.15,
    shadowRadius: 6,
    elevation: 4,
  },
  icon: { fontSize: 18, color: '#fff', marginRight: 12 },
  message: { flex: 1, color: '#fff', fontSize: 15 },
  dismiss: { color: '#fff', opacity: 0.8, fontWeight: '600' },
  buttons: { marginTop: 20, gap: 10 },
});

总结

Reanimated 4 确实是一次让人眼前一亮的更新。CSS 动画和过渡 API 大幅降低了动画开发的门槛——对于大多数常见的 UI 动画,几行声明式的代码就搞定了,不再需要手动管理 SharedValue 和 useAnimatedStyle 那套流程。

与此同时,Worklet 动画系统依然完整且强大。手势交互、滚动效果、复杂动画编排,这些场景下 Worklet 的能力不可替代。

两种方式能在同一个项目里自由混搭,这才是 Reanimated 4 设计哲学的精髓。

我的建议是:下次遇到动画需求,先试试 CSS 过渡。它更简洁、更好维护,性能也很棒。只有当需求确实超出了 CSS 动画的能力范围,再考虑切到 Worklet。这样的组合策略,足以让你在 React Native 里做出媲美原生的动画体验。

关于作者 Devon Nakashima

Devon is a principal engineer who has been writing React Native since the 0.40 days, with fifteen years total in mobile and web. He led the rewrite of the Wealthsimple trading app from native iOS/Android to a shared RN codebase, then spent two years at Discord on the mobile experience team working on the New Architecture migration and the Hermes upgrade that shipped to 200M+ installs. These days he's an independent consultant and contracts mostly with healthtech and developer-tools companies. He's an occasional contributor to React Navigation and was a maintainer on react-native-mmkv for about a year. His writing here is opinionated and tends toward architecture-level decisions: when to drop down to native modules, how to structure feature flags across iOS and Android, and why he thinks Expo's prebuild model finally won the bare-vs-managed debate around 2024.