Flutter 的动画系统是其核心优势之一,它非常强大、灵活且性能卓越。我会为你全面解析 Flutter 动画,从最简单的入门到高级复杂的应用。

与许多UI框架不同,Flutter 的动画系统不是基于“开始状态”和“结束状态”的简单过渡。它的核心是 Animation<T> 对象。

Animation<T> 是一个不关心UI的对象,它只知道在一段时间内,它的值 T(比如 doubleColorSize)会如何变化。你可以监听这个值的变化,然后用它来驱动任何你想要的东西。


两大类动画:隐式动画 vs 显式动画

Flutter 的动画可以分为两大阵营,理解它们的区别是掌握动画的关键。

特性 隐式动画 (Implicitly Animated Widgets) 显式动画 (Explicit Animations)
核心思想 状态驱动,自动执行 手动控制,完全掌握
使用难度 非常简单 较复杂,但更强大
控制力 有限(只能定义时长和曲线) 完全控制(播放、暂停、反向、循环)
典型代表 AnimatedContainerAnimatedOpacity AnimationControllerTweenAnimatedBuilder
适用场景 简单的、一次性的UI状态变化 复杂的、可重复的、需要精细控制的动画

1. 隐式动画 (Implicitly Animated Widgets) - 最简单的入门

这是最快、最简单的添加动画的方式。你只需要使用那些以 Animated 开头的 Widget,然后改变它们的属性,动画就会自动发生

核心原理
  1. 你提供一个目标值(比如新的宽度、颜色)。

  2. 你提供一个 duration (动画时长) 和一个 curve (动画曲线)。

  3. 当 setState 触发 Widget 重建时,它会发现目标值变了,然后自动为你生成从当前值到目标值的平滑过渡动画。

最常用的隐式动画 Widget:
  • AnimatedContainer: 一个万能的动画容器。可以动画化 widthheightcolorpaddingmargindecorationtransform 等几乎所有属性。

  • AnimatedOpacity: 动画化子 Widget 的透明度。

  • AnimatedPositioned: 在 Stack 布局中,动画化子 Widget 的位置。

  • AnimatedCrossFade: 在两个子 Widget 之间进行平滑的淡入淡出切换。

示例:一个点击后会变大变色的方块
import 'package:flutter/material.dart';

class ImplicitAnimationExample extends StatefulWidget {
  const ImplicitAnimationExample({super.key});

  @override
  State<ImplicitAnimationExample> createState() => _ImplicitAnimationExampleState();
}

class _ImplicitAnimationExampleState extends State<ImplicitAnimationExample> {
  // 1. 定义状态变量
  bool _isBig = false;
  double _size = 100.0;
  Color _color = Colors.blue;

  void _toggleAnimation() {
    // 2. 使用 setState 更新状态
    setState(() {
      _isBig = !_isBig;
      _size = _isBig ? 200.0 : 100.0;
      _color = _isBig ? Colors.red : Colors.blue;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('隐式动画')),
      body: Center(
        child: GestureDetector(
          onTap: _toggleAnimation,
          // 3. 使用 AnimatedContainer
          child: AnimatedContainer(
            // 4. 定义动画时长和曲线
            duration: const Duration(seconds: 1),
            curve: Curves.fastOutSlowIn, // 一个很舒服的缓动曲线
            // 5. 将状态变量绑定到属性上
            width: _size,
            height: _size,
            color: _color,
            child: const Center(
              child: Text('Click Me', style: TextStyle(color: Colors.white)),
            ),
          ),
        ),
      ),
    );
  }
}

解析: 你完全没有手动管理动画的播放。你做的仅仅是改变状态 (_size 和 _color),AnimatedContainer 就自动处理了剩下的一切。这就是隐式动画的魅力。


2. 显式动画 (Explicit Animations) - 完全的控制力

当你需要循环动画、暂停/继续、或者更复杂的动画序列时,就需要使用显式动画。它有几个核心组件:

  1. Ticker: 动画的“心跳”,它以屏幕刷新率(如 60fps)稳定地发出信号,告诉动画该刷新下一帧了。通常通过混入 TickerProviderStateMixin 来获得。

  2. AnimationController: 动画的“总指挥”。它在给定的 duration 内,生成一个从 0.0 到 1.0 的线性值。你可以命令它 forward() (播放), reverse() (反向), stop() (停止), repeat() (循环)。

  3. Tween: 值的“映射器”。它定义了一个值的范围(比如从 100.0 到 200.0,或者从 Colors.blue 到 Colors.red)。它使用 AnimationController 产生的 0.0 - 1.0 的值来计算出在这个范围内的具体值。

  4. Animation: 持有动画当前值的对象。Tween 和 AnimationController 通过 .animate() 方法结合后,就会产生一个 Animation 对象。

  5. AnimatedBuilder: 构建动画UI的最佳实践。它监听 AnimationController 的变化,并且只重建需要动画的部分,从而获得最佳性能。

示例:一个无限放大缩小的呼吸效果方块

这个例子我们在上一个回答中已经详细解析过了,它完美地展示了显式动画的完整流程。这里再回顾一下它的核心代码结构:

// 1. 混入 TickerProviderStateMixin
class _ExplicitAnimationState extends State<ExplicitAnimationExample> with SingleTickerProviderStateMixin {
  
  // 2. 声明 Controller 和 Animation
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;

  @override
  void initState() {
    super.initState();
    // 3. 初始化 Controller
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    );

    // 4. 使用 Tween 和 Controller 创建 Animation
    _sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
    );

    // 5. 添加监听器以实现循环
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reverse();
      } else if (status == AnimationStatus.dismissed) {
        _controller.forward();
      }
    });

    // 6. 启动动画
    _controller.forward();
  }

  @override
  Widget build(BuildContext context) {
    // 7. 使用 AnimatedBuilder 高效构建UI
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Container(
          // 8. 从 Animation 对象中获取当前值
          width: _sizeAnimation.value,
          height: _sizeAnimation.value,
          color: Colors.green,
        );
      },
    );
  }
  
  @override
  void dispose() {
    // 9. 必须销毁 Controller
    _controller.dispose();
    super.dispose();
  }
}

这个流程是所有复杂显式动画的基础。

以下是这段 Flutter 显式动画代码的详细逐行解释:

1. 混入 TickerProviderStateMixin

class _ExplicitAnimationState extends State<ExplicitAnimationExample> with SingleTickerProviderStateMixin {
  • SingleTickerProviderStateMixin是一个混入类,为动画提供 vsync信号

  • 它确保动画只在屏幕刷新时更新(通常60fps),避免不必要的资源消耗

  • 当有多个控制器时,应使用 TickerProviderStateMixin

2. 声明动画控制器和动画对象

late AnimationController _controller;
late Animation<double> _sizeAnimation;
  • AnimationController控制动画的播放状态(开始/停止/反转)和进度

  • Animation<double>是一个动画对象,会在指定范围内生成连续的 double 值

  • late关键字表示这些变量将在初始化时被赋值

3. 初始化动画控制器

_controller = AnimationController(
  vsync: this,
  duration: const Duration(seconds: 2),
);
  • vsync: this使用混入的 TickerProvider 来同步屏幕刷新

  • duration设置动画完成一次正向播放的时间(2秒)

  • 控制器默认范围是 0.0 到 1.0

4. 创建动画曲线和值范围

_sizeAnimation = Tween<double>(begin: 100.0, end: 200.0).animate(
  CurvedAnimation(parent: _controller, curve: Curves.easeInOut)
);
  • Tween定义动画值的范围(从100.0到200.0)

  • CurvedAnimation应用非线性曲线(easeInOut)使动画更自然

  • 最终 _sizeAnimation会生成从100到200的平滑变化值

5. 添加动画状态监听器实现循环

_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    _controller.reverse();
  } else if (status == AnimationStatus.dismissed) {
    _controller.forward();
  }
});
  • 当动画完成(completed)时自动反向播放

  • 当动画回到起点(dismissed)时再次正向播放

  • 这样就创建了无限循环的动画效果

6. 启动动画

_controller.forward();
  • 开始正向播放动画(从开始值到结束值)

7. 使用 AnimatedBuilder 构建UI

return AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Container(
      width: _sizeAnimation.value,
      height: _sizeAnimation.value,
      color: Colors.green,
    );
  },
);
  • AnimatedBuilder只重建动画依赖的部分,优化性能

  • 当动画值变化时,自动调用 builder 方法重建UI

  • 比在 setState中重建整个子树更高效

8. 获取当前动画值

width: _sizeAnimation.value,
height: _sizeAnimation.value,
  • _sizeAnimation.value获取当前帧的动画值

  • 随着动画播放,这个值会在100.0到200.0之间平滑变化

9. 销毁控制器

_controller.dispose();
  • 必须手动释放动画控制器资源

  • 防止内存泄漏和后台不必要的计算

  • 通常在 dispose()生命周期方法中调用

完整工作流程

  1. 初始化时创建控制器和动画配置

  2. 设置动画状态监听实现循环逻辑

  3. 启动动画

  4. 每帧根据动画曲线计算当前值

  5. AnimatedBuilder 根据新值重建UI

  6. 组件销毁时释放资源


3. 特定场景的强大动画

除了上述两种基础类型,Flutter 还内置了一些非常惊艳的、针对特定场景的动画。

Hero 动画 (共享元素过渡)

当你在两个页面之间导航时,Hero 动画可以让两个页面中相同的元素(比如一张图片)平滑地过渡过去,效果非常酷炫。

使用方法:

  1. 在第一个页面的 Widget 外面包一个 Hero Widget,并给它一个唯一的 tag

  2. 在第二个页面的对应 Widget 外面也包一个 Hero Widget,并使用完全相同的 tag

  3. 使用 Navigator.push 进行页面跳转。

// 页面A
Hero(
  tag: 'avatar',
  child: CircleAvatar(backgroundImage: AssetImage('...')),
);

// 页面B
Hero(
  tag: 'avatar',
  child: Image.asset('...'),
);

Flutter 会自动为你处理中间所有的位移、缩放动画。


4. 强大的动画库 (Packages)

当内置动画无法满足你的需求时,社区提供了许多优秀的动画库。

  • lottie-flutter: 神器!可以直接加载设计师用 Adobe After Effects 制作的复杂动画(导出为 JSON 文件)。对于实现非常炫酷、复杂的引导页、加载动画等,这是不二之选。

  • rive: 另一个强大的动画工具,允许你创建可以实时交互的、带有状态机的复杂动画。非常适合用于游戏角色、动态图标等。

总结与最佳实践

  1. 优先选择隐式动画:对于简单的UI状态变化,始终先考虑 AnimatedContainer 等隐式动画,它们代码最少,最易于维护。

  2. 需要控制时使用显式动画:当你需要循环、暂停或更复杂的动画序列时,就采用“Controller + Tween + AnimatedBuilder”的显式动画模式。

  3. 性能优化:始终使用 AnimatedBuilder 来包裹你的动画部分,避免不必要的 setState 导致整个页面重建。

  4. 释放资源:永远不要忘记在 State 的 dispose 方法中调用 _controller.dispose(),否则会造成内存泄漏。

  5. 善用曲线 (Curve):不要总是用线性动画,选择合适的 Curve(如 Curves.easeInOut)会让你的动画看起来更自然、更专业。

  6. 页面过渡用 Hero:在页面切换时,Hero 动画能极大地提升用户体验的连贯性。

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐