Flutter 动画系统

FreeGuideOnline 最新 2026-07-13

dart class _MyWidgetState extends State with SingleTickerProviderStateMixin { late AnimationController _controller;

@override void initState() { super.initState(); _controller = AnimationController( duration: const Duration(seconds: 2), vsync: this, // 提供 Ticker )..forward(); // 启动动画 }

@override void dispose() { _controller.dispose(); super.dispose(); } }


#### 2. Animation:可聆听的“值流”

`AnimationController` 本身继承自 `Animation<double>`,所以它直接提供线性变化的 double 值。但大多数时候我们需要曲线、颜色、大小等非线性的值。这时就要用到 `Animation<T>` 的其他子类或通过 `Tween` 与控制器组合得到。

- `Animation` 对象可以添加监听器 `addListener`,值变化时触发,我们通常在该回调中调用 `setState` 触发界面重绘。
- 可以添加状态监听 `addStatusListener`,在动画完成或恢复时执行特定逻辑。

#### 3. Tween:定义值的变化范围

`Tween<T>` 定义了动画的起点和终点,它本身不是一个 `Animation`,需要通过 `animate()` 方法作用于一个 `AnimationController` 来生成一个 `Animation<T>`。例如:

```dart
final Animation<Color?> colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue)
    .animate(_controller);
final Animation<double> sizeAnimation = Tween<double>(begin: 0, end: 300)
    .animate(_controller);

animate() 方法还可以接受一个 Curve 参数,改变动画的节奏。例如 Curves.easeInOut 会让动画缓入缓出。

常用曲线与节奏控制

Curve 类定义了动画值随时间变化的速率。Flutter 内置了大量曲线,从常见的 Curves.easeInCurves.easeOutCurves.bounceOutCurves.elasticOut 等。只需在动画对象上指定即可:

final Animation<double> curvedAnimation = CurvedAnimation(
  parent: _controller,
  curve: Curves.easeInOut,
);

如果需要复合曲线(例如前半段使用一种曲线,后半段使用另一种),可以使用 Interval 配合 Curve 使用。

从头开始构建一个动画(显式动画)

初学者最容易混淆的是隐式动画显式动画。我们先学习显式动画,因为它能让你完全掌控动画过程,并深入理解底层机制。

步骤拆解:

  1. 创建 AnimationController:绑定 vsync 并设置时长。
  2. 创建 Tween 并生成 Animation 对象:可以结合曲线。
  3. 添加监听器,在 setState 中更新 UI:根据动画当前值构建组件。
  4. dispose 中释放控制器
  5. 通过按钮或生命周期触发动画播放

下面是一个让方块大小从 0 变化到 200,颜色从红变蓝的完整示例:

class ExplicitAnimationDemo extends StatefulWidget {
  @override
  _ExplicitAnimationDemoState createState() => _ExplicitAnimationDemoState();
}

class _ExplicitAnimationDemoState extends State<ExplicitAnimationDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _sizeAnimation;
  late Animation<Color?> _colorAnimation;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,
    );

    _sizeAnimation = Tween<double>(begin: 0, end: 200).animate(
      CurvedAnimation(parent: _controller, curve: Curves.bounceOut),
    );

    _colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue).animate(_controller);

    // 动画值变化时刷新界面
    _controller.addListener(() {
      setState(() {});
    });

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

  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Center(
      child: Container(
        width: _sizeAnimation.value,
        height: _sizeAnimation.value,
        color: _colorAnimation.value,
      ),
    );
  }
}

虽然在监听器中调用 setState 能实现动画,但代码较为繁琐。Flutter 提供了 AnimatedBuilder 小部件来简化这种模式。

使用 AnimatedBuilder 简化监听

AnimatedBuilder 是一个通用动画构建器,它接受一个 animation 和一个 builder 方法,当动画值变化时自动调用 builder,且只会重建 builder 内部的部分,无需手动管理 addListenersetState

AnimatedBuilder 重写上述示例:

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,
    builder: (context, child) {
      return Center(
        child: Container(
          width: _sizeAnimation.value,
          height: _sizeAnimation.value,
          color: _colorAnimation.value,
        ),
      );
    },
  );
}

这样代码更加清晰,并且能将性能敏感的 child 提取出来(如果它不依赖于动画)以避免不必要的重建。

隐式动画:开箱即用的便捷方案

如果你只需要让某个属性从 A 动到 B,而不需要复杂的控制(如暂停、反向、重复等),隐式动画 是最佳选择。Flutter 提供了一系列 AnimatedFoo 小部件,它们内部已经封装了控制器、补间和构建逻辑,你只需指定目标值和时长。

常见隐式动画组件:

  • AnimatedContainer:动画改变容器的大小、颜色、边距等。
  • AnimatedOpacity:淡入淡出。
  • AnimatedAlign:平滑改变对齐方式。
  • AnimatedPadding:动画改变内边距。
  • AnimatedDefaultTextStyle:文本样式过渡。

示例:动态改变容器大小和颜色

class ImplicitAnimationDemo extends StatefulWidget {
  @override
  _ImplicitAnimationDemoState createState() => _ImplicitAnimationDemoState();
}

class _ImplicitAnimationDemoState extends State<ImplicitAnimationDemo> {
  bool _expanded = false;

  void _toggle() {
    setState(() {
      _expanded = !_expanded;
    });
  }

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: _toggle,
      child: AnimatedContainer(
        duration: const Duration(seconds: 1),
        curve: Curves.easeInOut,
        width: _expanded ? 200 : 100,
        height: _expanded ? 200 : 100,
        color: _expanded ? Colors.blue : Colors.red,
      ),
    );
  }
}

只需修改状态并触发 setStateAnimatedContainer 就会自动插值过渡到新值。

交错动画:协调多个动画的时间线

实际项目中经常需要多个动画按顺序或叠加播放,这就是交错动画(Staggered Animation)。核心思想是让同一个 AnimationController 驱动多个补间,但每个补间使用不同的 Interval 曲线(通过 Interval 指定开始和结束的百分比)。

例如:先旋转,再逐渐放大,最后改变颜色。

late Animation<double> rotation;
late Animation<double> scale;
late Animation<Color?> color;

void initState() {
  super.initState();
  _controller = AnimationController(
    duration: const Duration(seconds: 3),
    vsync: this,
  );

  rotation = Tween<double>(begin: 0, end: 2 * pi).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(0.0, 0.3, curve: Curves.easeOut),
    ),
  );

  scale = Tween<double>(begin: 1.0, end: 2.0).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(0.3, 0.7, curve: Curves.elasticOut),
    ),
  );

  color = ColorTween(begin: Colors.blue, end: Colors.orange).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Interval(0.7, 1.0, curve: Curves.linear),
    ),
  );

  _controller.forward();
}

构建时同时应用多个动画值即可。Interval 确保了每个动画只在控制器进度的一段起作用。

英雄动画与路由过渡

体验性极强的动画之一是 Hero 动画(共享元素过渡)。当从页面 A 跳转到页面 B 时,两个页面中具有相同 tagHero 小部件会执行平滑的飞行过渡。

使用方式:在两个页面的相似元素外包裹 Hero,并赋予相同的 tag

// 页面 A
Hero(
  tag: 'avatar',
  child: Image.asset('avatar.png', width: 100),
)

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