CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
React Native 开发入门/课时/动画
RawBlame
课时目录12 节

React Native 开发入门

  1. 01RN 与 Expo
  2. 02核心组件
  3. 03布局与列表
  4. 04平台差异
  5. 05导航与手势
  6. 06State 与 JSX
  7. 07网络
  8. 08本地存储
  9. 09原生模块
  10. 10动画
  11. 11性能优化
  12. 12测试与发布

动画

Animated API 入门。

30 分钟

动画

这一课你将能做到

  1. 用 Animated API 实现淡入淡出、位移、缩放动画,并正确开启原生驱动
  2. 用插值让一个动画值同时驱动多个样式属性,做出「卡片入场」效果
  3. 用 LayoutAnimation 一行代码搞定布局过渡,并说清楚 Reanimated 3 解决什么问题、什么时候必须用它

为什么要学

没有动画的 App 像 PPT 翻页——功能都在,但每个状态切换都是「瞬移」,用户感知不到界面之间的关联。好的动画是产品的「高级感」主要来源:点赞时爱心弹跳一下、弹窗从屏幕底部升起、列表项依次滑入,这些细节让界面从「能用」变成「好用」。

对开发者来说,RN 动画还有一个特殊关注点:性能。动画每秒钟要更新 60 帧,如果每一帧都要「JS 线程计算 → 跨桥通知原生 → 原生渲染」,一旦 JS 线程繁忙(比如在解析大 JSON),动画立刻掉帧卡顿。所以 RN 动画的核心知识不是「怎么动」,而是「怎么让动画跑在正确的线程上」。

人话:动画不是让元素「瞬移」,而是告诉系统「从 A 状态花 300 毫秒滑到 B 状态」,中间每一帧长什么样由系统自动补出来。你只需要管三件事:起点、终点、时长。而 useNativeDriver 就是决定「补帧」这件事交给谁做——交给原生线程做,就算 JS 在忙动画也流畅;交给 JS 做,JS 一卡动画就定格。

先建立图像

一个最小可运行的淡入动画,先感受全貌:

import { useRef } from 'react';
import { Animated, Button, StyleSheet, View } from 'react-native';

export default function App() {
  // 动画值必须放 useRef,不能放 useState
  const opacity = useRef(new Animated.Value(0)).current;

  const fadeIn = () => {
    Animated.timing(opacity, {
      toValue: 1,           // 终点
      duration: 500,        // 时长 500ms
      useNativeDriver: true, // 交给原生线程执行
    }).start();
  };

  return (
    <View style={styles.container}>
      {/* 必须用 Animated.View,普通 View 不认识动画值 */}
      <Animated.View style={[styles.box, { opacity }]} />
      <Button title="淡入" onPress={fadeIn} />
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, justifyContent: 'center', alignItems: 'center', gap: 20 },
  box: { width: 100, height: 100, backgroundColor: '#007bff', borderRadius: 12 },
});

点按钮,蓝色方块在 0.5 秒内从完全透明渐变到不透明。Animated.Value 就是一个「会自己爬坡的数值」——你告诉它「从 0 爬到 1,用 500 毫秒」,它每帧更新一次当前值,绑在样式上的组件就跟着每帧重绘。

整个动画的执行模型:

Animated.timing(起点 0 → 终点 1, 500ms)
  ↓ 开启 useNativeDriver 时
动画声明一次性发给原生线程
  ↓
原生线程每帧计算当前值并渲染(JS 线程忙也不影响)
  ↓
屏幕上看到平滑过渡

跟着做

步骤 1:Animated 三板斧——位移入场

把淡入升级成「从屏幕下方滑入 + 淡入」的卡片入场效果:

import { useEffect, useRef } from 'react';
import { Animated, StyleSheet, Text } from 'react-native';

export default function EntryCard() {
  const translateY = useRef(new Animated.Value(60)).current; // 初始在下方 60dp
  const opacity = useRef(new Animated.Value(0)).current;

  useEffect(() => {
    // parallel:位移和淡入同时执行
    Animated.parallel([
      Animated.timing(translateY, { toValue: 0, duration: 400, useNativeDriver: true }),
      Animated.timing(opacity, { toValue: 1, duration: 400, useNativeDriver: true }),
    ]).start();
  }, []);

  return (
    <Animated.View style={[styles.card, { opacity, transform: [{ translateY }] }]}>
      <Text style={styles.title}>欢迎回来</Text>
    </Animated.View>
  );
}

const styles = StyleSheet.create({
  card: { margin: 24, padding: 24, backgroundColor: '#fff', borderRadius: 16, elevation: 4 },
  title: { fontSize: 20, fontWeight: 'bold' },
});

组件挂载后,卡片从下方 60dp 处滑入并淡入——这就是大部分信息流 App 的卡片入场效果。

Animated 提供三种基础动画函数:

  • timing:匀速/缓动地过渡到目标值,最常用
  • spring:弹簧物理,到终点会轻微回弹,适合点赞、弹窗这类「活泼」场景
  • decay:给一个初速度然后摩擦减速,主要用于拖拽松手后的惯性滑动
// 弹簧动画:缩放到 1.2 再回弹到 1
Animated.spring(scale, { toValue: 1, friction: 4, useNativeDriver: true }).start();

步骤 2:组合动画——sequence / parallel / stagger

复杂动效都是简单动画的组合:

// sequence:一个接一个,做完 A 再做 B
Animated.sequence([fadeIn, slideUp]).start();

// parallel:同时开始,全部完成才算结束
Animated.parallel([fadeIn, slideUp]).start();

// stagger:交错执行——列表项逐个入场的经典手法
const animations = items.map((item) =>
  Animated.timing(item.anim, { toValue: 1, duration: 300, useNativeDriver: true })
);
Animated.stagger(80, animations).start(); // 每个比前一个晚 80ms 开始

stagger 是「高级感」性价比之王:给列表每一项配一个 Animated.Value,挂载时 stagger 启动,整个列表像波浪一样依次浮现,代码只要几行。

步骤 3:插值 interpolate——一个值驱动多个样式

插值是 Animated 最强大的概念:一个动画值可以映射出任意多个派生值。

const progress = useRef(new Animated.Value(0)).current;

// progress 从 0 → 1 的过程中:
const translateY = progress.interpolate({
  inputRange: [0, 1],
  outputRange: [50, 0],   // 位移从 50 → 0
});
const scale = progress.interpolate({
  inputRange: [0, 1],
  outputRange: [0.8, 1],  // 缩放从 0.8 → 1
});
const rotate = progress.interpolate({
  inputRange: [0, 1],
  outputRange: ['8deg', '0deg'], // 角度也支持
});

<Animated.View style={{ opacity: progress, transform: [{ translateY }, { scale }, { rotate }] }} />

一个 progress 值同时控制透明度、位移、缩放、旋转四个属性,启动一次 timing(progress, { toValue: 1 }),四个属性按各自映射同步变化——这就是做精致入场卡片的完整手法。

插值的经典应用场景还有:滚动驱动动画。把 ScrollView 的滚动位置绑到 Animated.Value 上,用插值算出导航栏透明度,就实现了「滚动时导航栏渐显」的效果:

const scrollY = useRef(new Animated.Value(0)).current;
const headerOpacity = scrollY.interpolate({
  inputRange: [0, 100],
  outputRange: [0, 1],
  extrapolate: 'clamp', // 超出范围取边界值,必备
});

步骤 4:LayoutAnimation——一行代码的布局过渡

很多动画其实不需要精细控制,只需要「变化别那么生硬」。LayoutAnimation 就是为这个而生:在 setState 之前调一句,接下来所有布局变化(元素增删、尺寸改变)自动平滑过渡。

一个完整的折叠面板:

import { useState } from 'react';
import { LayoutAnimation, Pressable, StyleSheet, Text, View } from 'react-native';

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

  const toggle = () => {
    // 关键就这一句:下一次布局变化请动画过渡
    LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
    setExpanded(!expanded);
  };

  return (
    <View style={styles.container}>
      <Pressable onPress={toggle} style={styles.header}>
        <Text style={styles.title}>{expanded ? '▼' : '▶'} 点击查看详情</Text>
      </Pressable>
      {expanded && (
        <View style={styles.body}>
          <Text style={styles.text}>这里是展开后的详情内容。注意展开/收起的过程是平滑的,而不是瞬间闪现。</Text>
        </View>
      )}
    </View>
  );
}

const styles = StyleSheet.create({
  container: { margin: 16, backgroundColor: '#fff', borderRadius: 12, overflow: 'hidden' },
  header: { padding: 16 },
  title: { fontSize: 16, fontWeight: '600' },
  body: { padding: 16, paddingTop: 0 },
  text: { color: '#666', lineHeight: 22 },
});

除了 easeInEaseOut,预设还有 linear(匀速)和 spring(弹性)。折叠面板、表单校验提示的出现、标签增删——这类「布局变了,希望它变的时候别闪」的场景,LayoutAnimation 一行搞定。

步骤 5:认识 Reanimated 3

Animated 有一个结构性短板:手势场景。手指在屏幕上拖动,每一帧都要「原生检测到手势 → 通知 JS → JS 算新位置 → 通知原生移动」,快速拖动时这个往返跟不上 60fps,动画就「不跟手」。

Reanimated 3 的解法:worklet——把你的动画函数直接编译放到 UI 线程上执行,手势和动画在原生线程内闭环,零跨桥开销。

npx expo install react-native-reanimated
import { Pressable, Text } from 'react-native';
import Animated, { useSharedValue, useAnimatedStyle, withSpring } from 'react-native-reanimated';

export default function BounceButton() {
  const scale = useSharedValue(1); // 对应 Animated.Value

  // 对应样式绑定,注意是函数返回值
  const animatedStyle = useAnimatedStyle(() => ({
    transform: [{ scale: scale.value }],
  }));

  return (
    <Pressable
      onPressIn={() => { scale.value = withSpring(0.9); }}  // 按下缩小
      onPressOut={() => { scale.value = withSpring(1); }}   // 松开弹回
    >
      <Animated.View style={[{ backgroundColor: '#007bff', padding: 20, borderRadius: 12 }, animatedStyle]}>
        <Text style={{ color: '#fff' }}>按住我有弹性</Text>
      </Animated.View>
    </Pressable>
  );
}

按住的瞬间按钮缩小、松开弹回,全程在 UI 线程执行。Reanimated 的 API 思路与 Animated 对应:useSharedValue ↔ Animated.Value,useAnimatedStyle ↔ 样式绑定,withSpring/withTiming ↔ spring/timing。入门阶段的静态动画用 Animated 足够,涉及手势拖拽再切 Reanimated。

讲清楚要点

  1. useNativeDriver: true 是性能的分水岭:开启后动画声明一次性发给原生线程,之后每帧由原生独立计算渲染,JS 线程即使繁忙动画也不掉帧。代价是它只支持 transform 和 opacity 两类属性——想动宽高背景色,要么设 false 回 JS 线程,要么换 transform 方案模拟。
  2. 动画值必须放 useRef,不能放 useState:useState(new Animated.Value(0)) 每次组件重渲染都会创建新的动画值,动画直接重置。正确写法 useRef(new Animated.Value(0)).current,整个组件生命周期内引用稳定。
  3. 插值是一个值驱动多个样式的核心手法:interpolate({ inputRange, outputRange }) 把一个进度值映射成位移、缩放、颜色、角度等任意样式,滚动驱动动画、精致入场效果都靠它。记得配 extrapolate: 'clamp' 防止越界。
  4. LayoutAnimation 是性价比之王:setState 前一句 configureNext,布局变化自动平滑。覆盖折叠面板、列表增删等 80% 的「非精细」场景,缺点是控制不了动画细节,精细效果回 Animated。
  5. Reanimated 3 解决的是「跟手」问题:worklet 让动画逻辑在 UI 线程执行,手势拖拽场景零桥接开销。不涉及手势的普通入场动画,Animated + 原生驱动已经足够流畅,不用为用而用。
  6. 动画要克制:时长 200-400ms 是舒适区,超过 500ms 用户会觉得「肉」;同时屏幕上别超过 3 个动画在跑,动效是调味品不是主菜。

你可能会懵的地方

  • useNativeDriver 到底什么时候开不了? 改布局属性的时候:width、height、top、left、backgroundColor 这些都不支持。想动宽高,优先改用 transform: [{ scaleX }, { scaleY }] 模拟——视觉等效且能走原生驱动。实在绕不开(比如真的要改布局高度),设 false 接受 JS 线程执行。

  • 循环动画(呼吸灯、骨架屏闪烁)怎么写? Animated.loop() 包一层:Animated.loop(Animated.sequence([变亮, 变暗])).start()。组件卸载时记得调 stopAnimation(),否则动画还在跑会造成内存泄漏。

  • 手势拖拽的完整方案是什么? Reanimated + react-native-gesture-handler 是官方推荐的现代组合:Gesture Handler 负责在原生线程识别手势,Reanimated 负责在 UI 线程跟手动画。Expo 项目里两个库都能 npx expo install 直接装。

  • 动画执行完想接着做点事(比如跳转页面)? .start() 接收回调:Animated.timing(...).start(({ finished }) => { if (finished) navigation.replace('Home'); })。注意判断 finished 标志,动画被中途打断时回调也会触发但 finished 为 false。

  • Animated 和 CSS transition 思路一样吗? 不一样。CSS transition 是「声明属性变化,浏览器自动补帧」;Animated 是「显式操作一个数值对象,数值绑到样式上」。前者是声明式,后者是命令式。习惯了 CSS 的前端需要适应「先建值、再驱动、后绑定」这三步。

错法对照

错法问题正解
Animated.Value 放 useState每次渲染动画值重建,动画重置useRef(new Animated.Value(x)).current
忘写 useNativeDriver动画跑 JS 线程,繁忙时掉帧能开就开:useNativeDriver: true
用 width 动画还开原生驱动直接报错提示不支持原生驱动只支持 transform/opacity,宽高改 scale 模拟
样式绑在普通 View 上动画完全不生效必须用 Animated.View(或 Animated.createAnimatedComponent)
动画时长写 1 秒以上界面感觉迟钝、「肉」常规 200-400ms
频繁触发动画不停止旧的两个动画打架,元素乱跳启动前先 stopAnimation(),或用 setValue 重置
布局增删手写 Animated几十行代码实现 LayoutAnimation 一句的效果布局过渡优先 LayoutAnimation
循环动画卸载时不停内存泄漏、控制台警告useEffect cleanup 里 stopAnimation

卡点急救

  1. 动画完全不动:按顺序排查——样式是否绑在 Animated.View 上(不是普通 View)、.start() 是否调用、toValue 和初始值是否真的不同。
  2. 报「useNativeDriver 不支持 XX 属性」:检查动画属性,布局类属性(width/height/margin/背景色)不支持,把该属性改 false 或换 transform 方案。
  3. LayoutAnimation 在 Android 上无效:RN 新版本(0.73+)和 Expo 新 SDK 默认已支持;旧版需要在原生代码手动开实验标志——用 Expo 的话升级 SDK 即可。
  4. 动画执行一半卡死:多半是 JS 线程被重计算堵住(大 JSON 解析、同步循环),把重活移出渲染路径,或确认 useNativeDriver 已开启。
  5. interpolate 结果突变不顺滑:检查 inputRange 是否单调递增、是否忘加 extrapolate: 'clamp' 导致越界后值乱跳。

面试常问

  • Animated 的 useNativeDriver 原理是什么? 开启后,动画的起点、终点、时长等声明在启动时一次性序列化发给原生线程,之后每一帧的插值计算和样式更新都由原生独立完成,全程不需要 JS 参与。因此 JS 线程繁忙(解析数据、渲染复杂组件)时动画依然流畅。未开启则每一帧都要 JS 计算当前值、跨桥通知原生更新,JS 一忙帧率就掉。

  • Reanimated 相比 Animated 解决了什么问题? Animated 即使开了原生驱动,手势场景仍是短板:手势事件在原生侧产生,每帧要跨桥通知 JS、JS 计算后再通知原生更新,快速拖拽时跟不上。Reanimated 用 worklet 技术把动画函数直接编译到 UI 线程执行,手势识别(配合 Gesture Handler)和动画更新全部在原生线程内闭环,零跨桥,实现真正的「跟手」。此外它的声明式 API(useSharedValue + useAnimatedStyle)与 React 渲染模型结合更自然。

  • 如何实现滚动驱动的动画(如导航栏渐显)? 用 Animated.event 把 ScrollView 的 onScroll 事件绑定到一个 Animated.Value(开启 useNativeDriver),再用 interpolate 把滚动距离映射成导航栏透明度、标题缩放等样式,绑到 Animated.View 上。全程原生线程执行,滚动流畅不掉帧。

自测三问

  1. useNativeDriver 为什么能提升动画流畅度?它支持哪些样式属性?
  2. 为什么 Animated.Value 必须放 useRef 而不是 useState?放 state 会发生什么?
  3. 什么场景下 Animated 不够用、必须上 Reanimated?它解决问题的核心机制是什么?

小结一句话

动画三板斧:布局过渡用 LayoutAnimation 一行搞定,精细入场动画用 Animated + 原生驱动 + 插值,手势跟手交互上 Reanimated——先让界面动起来,再追求动得优雅、动得流畅。

延伸

  • React Native Animated 官方文档
  • Reanimated 官方文档
  • 下一课:性能优化——memo、FlashList、图片缓存与启动提速,让你的 App 在中低端机上也不卡。
加载本节练习…
← 上一课原生模块下一课 →性能优化