从零开始:用 react-native-reanimated 构建流畅的动画

87 阅读4分钟

在 React Native 开发中,动画性能一直是开发者关注的重点。传统的 Animated API 在 JS 线程运行,当 JS 线程负载较高时,动画容易出现卡顿。React Native Reanimated 通过将动画逻辑迁移到 UI 线程,实现了接近原生的流畅体验。

本文将从实际应用角度出发,系统介绍 Reanimated 的核心概念和实战技巧,帮助你快速掌握这个强大的动画库。


为什么选择 Reanimated?

技术对比一览

特性Animated APIReanimated
执行线程仅 JS 线程支持 UI 线程(主线程)
性能表现受 JS 线程阻塞影响高并发下依然流畅,60fps
手势处理需借助第三方实现原生内置手势支持
TypeScript类型支持有限丰富、完善的类型定义
新架构兼容Fabric 支持有限完全兼容 Fabric/Turbo
复杂动画配置复杂,扩展性有限支持中断、组合与嵌套
动画曲线基础动画曲线内置弹性、衰减等多样曲线

核心优势

  • UI 线程执行:动画逻辑运行于 UI 线程,不受 JS 线程阻塞影响,效果丝滑流畅。
  • 手势支持完善:与 react-native-gesture-handler 深度集成,交互体验自然流畅。
  • 类型友好:具备完善的 TypeScript 类型声明,开发体验优异。
  • 兼容新架构:完美支持 Fabric 与 TurboModules,紧跟 React Native 发展。
  • 丰富动画曲线:内置弹性、弹跳、衰减等多种缓动曲线,满足多样动画需求。
  • 动画中断与组合:灵活支持动画中断、组合,复杂交互轻松实现。
  • 生态活跃:官方文档详尽,社区讨论丰富,兼容主流三方库,生态完善。

快速开始

1. 安装依赖

本文依赖的库版本如下:

  • react-native(0.83.0)
  • react-native-gesture-handler(^2.30.0):高性能手势库,后续会出文章单独介绍。
  • react-native-reanimated(^4.2.1):动画主库。
  • react-native-worklets(^0.7.2):让 JS 代码能在 UI 线程和 JSI 环境下高效运行,辅助扩展动画效果。
npm install react-native-reanimated react-native-gesture-handler react-native-worklets

2. 配置 Babel

babel.config.js 中添加插件:目的是为了能解析 worklets 函数,使其可以再 UI 线程中运行

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['react-native-worklets/plugin'],
};

3. Native 配置

  • Android

无需多余配置,RN会自动把三方库的 Native 功能打包进 apk 包

  • iOS
cd ios && pod install

4. 基础示例

下面是一个简单的动画示例,展示透明度和位移的动画效果:

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

export default function BasicAnimation() {
  // 声明动画值
  const opacity = useSharedValue(0);
  const translateX = useSharedValue(0);

  // 计算动画样式
  const animatedStyle = useAnimatedStyle(() => ({
    opacity: opacity.value,
    transform: [{ translateX: translateX.value }],
  }));

  const handlePress = () => {
    // 使用线性动画修改动画值
    opacity.value = withTiming(opacity.value === 0 ? 1 : 0, {
      duration: 500,
    });
    translateX.value = withTiming(translateX.value === 0 ? 100 : 0, {
      duration: 500,
    });
  };

  return (
    <View style={styles.container}>
      <Animated.View style={[styles.box, animatedStyle]} />
      <Button title="Animate" onPress={handlePress} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
  },
  box: {
    width: 100,
    height: 100,
    backgroundColor: '#4CAF50',
    marginBottom: 20,
  },
});

效果如下:

20260128-224256.578-4.gif


核心概念

1. Shared Values

useSharedValue 用于存储动画状态,是 Reanimated 的核心数据结构:

const scale = useSharedValue(1);

// 读取当前值
console.log(scale.value);

// 修改值并触发动画
scale.value = withTiming(2, { duration: 300 });

2. Animated Styles

useAnimatedStyle 将 shared values 转换为动画样式:

const animatedStyle = useAnimatedStyle(() => ({
  transform: [
    { scale: scale.value },
    { rotate: `${rotation.value}deg` },
  ],
  opacity: opacity.value,
}));

3. Worklets

Worklets 是在 UI 线程执行的 JavaScript 函数,通过 'worklet' 指令标记,上面安装的 react-native-worklets/plugin 插件就是为了解析此关键字

const handlePress = () => {
  'worklet';
  scale.value = withSpring(1.5);
};

4. 动画函数

Reanimated 提供多种动画函数,满足不同场景需求:

// 线性动画
withTiming(targetValue, { duration: 500 })

// 弹簧动画
withSpring(targetValue, {
  damping: 10,
  stiffness: 100,
})

// 衰减动画
withDecay({ velocity: 1000 })

// 序列动画
withSequence(
  withTiming(100, { duration: 200 }),
  withDelay(100, withTiming(0, { duration: 200 }))
)

// 重复动画
withRepeat(withTiming(1, { duration: 500 }), -1, true)

实战案例

案例 1:可拖拽卡片

实现类似 Tinder 的卡片拖拽效果,包含拖拽、回弹和飞出动画:

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

const { width: SCREEN_WIDTH } = Dimensions.get('window');

export default function DraggableCard() {
  const translateX = useSharedValue(0);
  const translateY = useSharedValue(0);
  const scale = useSharedValue(1);
  const color = useSharedValue('#2196F3');

  const gesture = Gesture.Pan()
    .onStart(() => {
      scale.value = withSpring(1.1);
      color.value = withSpring('#FF5722');
    })
    .onUpdate((event) => {
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      scale.value = withSpring(1);
      color.value = withSpring('#2196F3');

      if (Math.abs(translateX.value) > SCREEN_WIDTH / 2) {
        translateX.value = withSpring(
          translateX.value > 0 ? SCREEN_WIDTH : -SCREEN_WIDTH
        );
      } else {
        translateX.value = withSpring(0);
        translateY.value = withSpring(0);
      }
    });

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

  return (
    <GestureHandlerRootView style={styles.container}>
      <GestureDetector gesture={gesture}>
        <Animated.View style={[styles.card, animatedStyle]}>
          <Text style={styles.text}>拖拽我!</Text>
        </Animated.View>
      </GestureDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: '#f5f5f5',
  },
  card: {
    width: 300,
    height: 200,
    backgroundColor: '#2196F3',
    borderRadius: 16,
    justifyContent: 'center',
    alignItems: 'center',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 4 },
    shadowOpacity: 0.3,
    shadowRadius: 8,
    elevation: 8,
  },
  text: {
    color: 'white',
    fontSize: 24,
    fontWeight: 'bold',
  },
});

效果如下:

20260128-224256.578-2.gif

案例 2:可展开/收起的列表项

实现常见的展开/收起效果,包含高度动画和箭头旋转:

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

export default function ExpandableItem({
  title,
  content,
}: {
  title?: string;
  content?: string;
}) {
  const expanded = useSharedValue(false);
  const height = useSharedValue(0);
  const opacity = useSharedValue(0);

  const toggleExpand = () => {
    expanded.value = !expanded.value;

    if (expanded.value) {
      // 展开增加高度和透明度
      height.value = withTiming(100, {
        duration: 500,
        // 使用贝塞尔曲线动画
        easing: Easing.bezier(0.4, 0, 0.2, 1),
      });
      opacity.value = withTiming(1, { duration: 300 });
    } else {
      // 收起减小高度,透明度变为 0
      height.value = withTiming(0, { duration: 300 });
      opacity.value = withTiming(0, { duration: 200 });
    }
  };

  // 内容卡片样式
  const contentStyle = useAnimatedStyle(() => ({
    height: height.value,
    opacity: opacity.value,
  }));

  // 展开箭头样式
  const arrowStyle = useAnimatedStyle(() => ({
    transform: [
      {
        rotate: withTiming(expanded.value ? '180deg' : '0deg', {
          duration: 300,
        }),
      },
    ],
  }));

  return (
    <View style={styles.container}>
      <TouchableOpacity
        style={styles.header}
        onPress={toggleExpand}
        activeOpacity={0.7}
      >
        <Text style={styles.title}>{title}</Text>
        <Animated.Text style={[styles.arrow, arrowStyle]}></Animated.Text>
      </TouchableOpacity>
      <Animated.View style={[styles.content, contentStyle]}>
        <Text style={styles.contentText}>{content}</Text>
      </Animated.View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    backgroundColor: 'white',
    marginHorizontal: 16,
    marginVertical: 8,
    borderRadius: 12,
    overflow: 'hidden',
    shadowColor: '#000',
    shadowOffset: { width: 0, height: 2 },
    shadowOpacity: 0.1,
    shadowRadius: 4,
    elevation: 3,
  },
  header: {
    flexDirection: 'row',
    justifyContent: 'space-between',
    alignItems: 'center',
    padding: 16,
    backgroundColor: '#fff',
  },
  title: {
    fontSize: 16,
    fontWeight: '600',
    color: '#333',
  },
  arrow: {
    fontSize: 12,
    color: '#666',
  },
  content: {
    overflow: 'hidden',
  },
  contentText: {
    padding: 16,
    paddingTop: 0,
    fontSize: 14,
    color: '#666',
    lineHeight: 20,
  },
});

效果如下:

20260128-224256.578-3.gif

案例 3:双指缩放图片

实现图片的双指缩放和拖拽功能:

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

const { width: SCREEN_WIDTH } = Dimensions.get('window');

export default function PinchToZoom() {
  // 定义缩放值
  const scale = useSharedValue(1);
  // 用于记录每次缩放结束后的缩放值,实现连续缩放。
  const savedScale = useSharedValue(1);
  // 定义拖拽X值
  const translateX = useSharedValue(0);
  // 定义拖拽Y值
  const translateY = useSharedValue(0);

  // 定义双指手势
  const pinchGesture = Gesture.Pinch()
    .onStart(() => {
      // 双指缩放时,保存当前缩放值
      savedScale.value = scale.value;
    })
    .onUpdate(event => {
      // 双指缩放时,根据保存的缩放值和当前缩放值计算新的缩放值
      scale.value = savedScale.value * event.scale;
    })
    .onEnd(() => {
      // 双指缩放结束时,恢复缩放值
      scale.value = withSpring(1);
    });

  // 定义拖拽手势
  const panGesture = Gesture.Pan()
    .onUpdate(event => {
      // 拖拽时,根据当前拖拽值计算新的拖拽值
      translateX.value = event.translationX;
      translateY.value = event.translationY;
    })
    .onEnd(() => {
      // 拖拽结束时,恢复拖拽值
      translateX.value = withSpring(0);
      translateY.value = withSpring(0);
    });

  // 定义同时执行双指缩放和拖拽手势
  const composed = Gesture.Simultaneous(pinchGesture, panGesture);

  // 定义动画样式
  const animatedStyle = useAnimatedStyle(() => ({
    // 根据当前拖拽值和缩放值计算新的拖拽值和缩放值
    transform: [
      { translateX: translateX.value },
      { translateY: translateY.value },
      { scale: scale.value },
    ],
  }));

  return (
    <GestureHandlerRootView style={styles.container}>
      <GestureDetector gesture={composed}>
        <Animated.View style={[styles.imageContainer, animatedStyle]}>
          <View style={styles.image} />
        </Animated.View>
      </GestureDetector>
    </GestureHandlerRootView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    alignItems: 'center',
    backgroundColor: 'rgba(0,0,0,0.5)',
  },
  imageContainer: {
    width: SCREEN_WIDTH * 0.8,
    height: SCREEN_WIDTH * 0.8,
  },
  image: {
    flex: 1,
    backgroundColor: 'orange',
    borderRadius: 8,
  },
});

效果如下:

20260128-224256.578-1.gif


性能优化技巧

1. 避免在动画中调用 JS 函数

不推荐的做法:

const animatedStyle = useAnimatedStyle(() => ({
  transform: [{ scale: Math.sin(Date.now()) }],
}));

推荐的做法:

const time = useSharedValue(0);

useEffect(() => {
  const interval = setInterval(() => {
    time.value = Date.now();
  }, 16);
  return () => clearInterval(interval);
}, []);

const animatedStyle = useAnimatedStyle(() => ({
  transform: [{ scale: Math.sin(time.value) }],
}));

2. 使用 useDerivedValue 缓存计算

const scale = useSharedValue(1);
const rotation = useSharedValue(0);

const combinedTransform = useDerivedValue(() => {
  return {
    scale: scale.value,
    rotation: rotation.value,
  };
});

const animatedStyle = useAnimatedStyle(() => ({
  transform: [
    { scale: combinedTransform.value.scale },
    { rotate: `${combinedTransform.value.rotation}deg` },
  ],
}));

3. 减少不必要的重渲染

const AnimatedComponent = React.memo(({ value }) => {
  const animatedStyle = useAnimatedStyle(() => ({
    opacity: value.value,
  }));

  return <Animated.View style={animatedStyle} />;
});

4. 使用 runOnJS 调用 JS 函数

const handleAnimationComplete = () => {
  console.log('Animation completed!');
};

const animatedStyle = useAnimatedStyle(() => ({
  opacity: withTiming(1, { duration: 500 }, (finished) => {
    if (finished) {
      runOnJS(handleAnimationComplete)();
    }
  }),
}));

5. 使用 useAnimatedScrollHandler 优化滚动动画

const scrollOffset = useSharedValue(0);

const scrollHandler = useAnimatedScrollHandler({
  onScroll: (event) => {
    scrollOffset.value = event.contentOffset.y;
  },
});

const headerStyle = useAnimatedStyle(() => ({
  opacity: 1 - scrollOffset.value / 200,
}));

return (
  <Animated.ScrollView
    onScroll={scrollHandler}
    scrollEventThrottle={16}
  >
    <Animated.View style={[styles.header, headerStyle]}>
      <Text>Header</Text>
    </Animated.View>
  </Animated.ScrollView>
);

总结

Reanimated 通过 UI 线程执行动画,解决了传统 Animated API 的性能瓶颈。掌握以下核心概念即可快速上手:

  1. Shared Values - 动画状态管理
  2. Animated Styles - 样式动画化
  3. Worklets - UI 线程函数
  4. Gestures - 手势交互
  5. 动画函数 - withTiming、withSpring 等

进阶学习方向

  • 深入学习 react-native-gesture-handler 的高级手势
  • 探索 Reanimated 的新特性(Layout Animations、Shared Transitions)
  • 学习性能优化最佳实践
  • 构建复杂的动画组合效果

参考资源


通过本文的学习,你应该能够使用 Reanimated 构建流畅的动画效果了。建议在实际项目中多加练习,逐步掌握更高级的动画技巧。