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

Reanimated 4 将 CSS 动画和过渡效果带入 React Native,本文从 SharedValue、Worklet 核心概念到 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 里做出媲美原生的动画体验。

关于作者 Editorial Team

Our team of expert writers and editors.