Skip to content

显式动画

显式动画(Explicit Animation)需要你手动控制 AnimationController,适合需要精确控制的动画场景。

一句话理解显式动画

想象你在做定格动画——每一帧画面由你决定,播放、暂停、倒放、循环,全由你控制。显式动画就是这种"手动挡"模式。

本章内容

  • 显式动画的基本结构(AnimationController + Tween + AnimatedBuilder)
  • AnimationController 的常用方法
  • Tween 类型与组合动画
  • AnimatedWidget 简化写法
  • 隐式动画 vs 显式动画的对比

学习建议

如果你还没看过 隐式动画,建议先学习那一章。隐式动画更简单,能帮你建立对 Flutter 动画的基本理解。只有当隐式动画满足不了需求时,才需要用到显式动画。

为什么需要显式动画?

隐式动画很方便,但它有明显的限制:

需求隐式动画显式动画
简单过渡(变大变小)
循环播放(如旋转加载)
精确控制播放/暂停/倒放
多个动画同步协调
监听动画的每一帧
自定义动画值的变化范围有限

结论:当隐式动画"力不从心"时,就该上显式动画了。

基本结构(四步走)

显式动画的代码结构可以归纳为四步:

第 1 步:混入 SingleTickerProviderStateMixin  —— 提供 vsync 信号
第 2 步:创建 AnimationController             —— 控制动画的播放
第 3 步:用 Tween 定义动画值范围               —— 定义从什么值变到什么值
第 4 步:用 AnimatedBuilder 构建 UI             —— 每一帧重建 UI

完整示例:点击放大缩小

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

  @override
  State<ScaleAnimationDemo> createState() => _ScaleAnimationDemoState();
}

class _ScaleAnimationDemoState extends State<ScaleAnimationDemo>
    with SingleTickerProviderStateMixin {          // 第 1 步:混入

  late AnimationController _controller;             // 第 2 步:控制器
  late Animation<double> _animation;                // 第 3 步:动画对象

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,                                   // vsync 由 Mixin 提供
      duration: const Duration(seconds: 1),          // 动画时长
    );

    // 用 Tween 定义值范围:从 0.0 到 1.0
    _animation = Tween<double>(begin: 0, end: 1).animate(_controller);

    // 监听动画完成
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        print('动画播放完成');
      }
    });
  }

  @override
  void dispose() {
    _controller.dispose();                           // 必须释放!
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        AnimatedBuilder(                             // 第 4 步:构建 UI
          animation: _animation,
          builder: (context, child) {
            return Transform.scale(
              scale: _animation.value,               // 使用动画值
              child: child,
            );
          },
          child: const Icon(Icons.star, size: 100, color: Colors.amber),
        ),
        const SizedBox(height: 20),
        ElevatedButton(
          onPressed: () {
            if (_controller.isCompleted) {
              _controller.reverse();                 // 已完成 → 反向播放
            } else {
              _controller.forward();                 // 未完成 → 正向播放
            }
          },
          child: const Text('播放动画'),
        ),
      ],
    );
  }
}

四步详解

第 1 步:SingleTickerProviderStateMixin

  • 它的作用是给 AnimationController 提供 vsync 信号(垂直同步)
  • 简单理解:它告诉系统"我在屏幕上,请给我刷新信号"
  • 如果页面不可见(比如切到后台),它会自动暂停动画,避免浪费资源

第 2 步:AnimationController

  • 它是动画的"遥控器"——播放、暂停、倒放、循环都由它控制
  • 默认值范围是 0.0 ~ 1.0,代表动画的进度

第 3 步:Tween

  • 它定义了动画值的起止范围,比如"从 0 变到 100"、"从红色变到蓝色"
  • 它把 Controller 的 0.0~1.0 映射到你的实际值范围

第 4 步:AnimatedBuilder

  • 它监听动画的每一帧变化,自动调用 builder 重建 UI
  • child 参数放不依赖动画的部分,避免不必要的重建(性能优化)

AnimationController 详解

常用方法

dart
_controller.forward();                    // 从头播放到结束
_controller.reverse();                    // 从当前位置反向播放
_controller.repeat();                     // 循环播放
_controller.stop();                       // 停止(保持当前位置)
_controller.reset();                      // 重置到起始位置(不播放)
_controller.dispose();                    // 释放资源(必须在 dispose 中调用)

控制器值与状态

dart
_controller.value                         // 当前值(0.0 ~ 1.0)
_controller.isAnimating                   // 是否正在播放
_controller.isCompleted                   // 是否播放完成
_controller.isDismissed                   // 是否在起始位置

事件监听

dart
// 监听动画状态变化
_controller.addStatusListener((status) {
  switch (status) {
    case AnimationStatus.dismissed:    // 在起始位置
      break;
    case AnimationStatus.forward:      // 正在正向播放
      break;
    case AnimationStatus.reverse:      // 正在反向播放
      break;
    case AnimationStatus.completed:    // 播放完成
      break;
  }
});

// 监听每一帧(慎用,性能敏感)
_controller.addListener(() {
  print('当前值: ${_controller.value}');
});

addListener vs AnimatedBuilder

  • addListener + setState():手动刷新 UI,性能差,不推荐
  • AnimatedBuilder:自动重建,性能好,推荐

除非你需要做非 UI 的事情(比如同步另一个控制器),否则优先使用 AnimatedBuilder

自定义值范围

AnimationController 默认值范围是 0.0 ~ 1.0。你也可以自定义:

dart
// 默认:0.0 ~ 1.0
_controller = AnimationController(
  vsync: this,
  duration: const Duration(seconds: 1),
);

// 自定义:0 ~ 100
_controller = AnimationController(
  vsync: this,
  duration: const Duration(seconds: 1),
  lowerBound: 0,
  upperBound: 100,
);

推荐做法

更推荐保持默认的 0.0 ~ 1.0,然后用 Tween 来映射到实际值范围。这样代码更清晰,也更容易复用。

repeat() 进阶用法

dart
// 基本循环
_controller.repeat();

// 循环 3 次后停止
_controller.repeat(count: 3);

// 来回循环(正播 → 反播 → 正播 → ...)
_controller.repeat(reverse: true);

Tween 类型

Tween 定义了动画值从起点终点的范围。不同类型的数据有不同的 Tween:

Tween 类型起止值类型示例
Tween<double>数值Tween(begin: 0.0, end: 1.0)
IntTween整数IntTween(begin: 0, end: 255)
ColorTween颜色ColorTween(begin: Colors.red, end: Colors.blue)
OffsetTween偏移OffsetTween(begin: Offset.zero, end: Offset(100, 100))
SizeTween尺寸SizeTween(begin: Size(50, 50), end: Size(200, 200))
RectTween矩形RectTween(begin: Rect.fromLTWH(0, 0, 50, 50), end: ...)
AlignmentTween对齐AlignmentTween(begin: Alignment.topLeft, end: Alignment.bottomRight)

颜色动画示例

dart
late Animation<Color?> _colorAnimation;

@override
void initState() {
  super.initState();
  _controller = AnimationController(
    vsync: this,
    duration: const Duration(seconds: 2),
  );
  _colorAnimation = ColorTween(begin: Colors.red, end: Colors.blue)
      .animate(_controller);
  _controller.repeat(reverse: true);  // 来回循环
}

// 在 build 中
AnimatedBuilder(
  animation: _colorAnimation,
  builder: (context, child) {
    return Container(
      width: 100,
      height: 100,
      color: _colorAnimation.value,  // 颜色从红渐变到蓝
    );
  },
)

组合动画

很多时候我们需要多个属性同时动画,而且每个属性用不同的曲线。这就需要 CurvedAnimation

示例:大小和透明度同时变化,但曲线不同

dart
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<double> _opacityAnimation;

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

  // 大小:先快后慢
  _sizeAnimation = Tween<double>(begin: 50, end: 200).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Curves.easeOut,            // 先快后慢
    ),
  );

  // 透明度:先慢后快
  _opacityAnimation = Tween<double>(begin: 0, end: 1).animate(
    CurvedAnimation(
      parent: _controller,
      curve: Curves.easeIn,             // 先慢后快
    ),
  );

  _controller.forward();
}

@override
Widget build(BuildContext context) {
  return AnimatedBuilder(
    animation: _controller,             // 只需监听 controller
    builder: (context, child) {
      return Opacity(
        opacity: _opacityAnimation.value,
        child: SizedBox(
          width: _sizeAnimation.value,
          height: _sizeAnimation.value,
          child: child,
        ),
      );
    },
    child: const Icon(Icons.star, size: 100, color: Colors.amber),
  );
}

CurvedAnimation 的原理

CurvedAnimation 就像一个"滤镜"——它把 Controller 线性的 0→1 进度,通过曲线函数变换成非线性的进度。这样同一个 Controller 可以驱动多个动画,每个动画的"速度感"不同。

交错动画(Staggered Animation)

如果你想让多个动画依次开始(而不是同时),可以用 Interval

dart
// 第一个动画:0% ~ 60% 的时间内完成
_firstAnimation = Tween<double>(begin: 0, end: 1).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.0, 0.6, curve: Curves.easeOut),
  ),
);

// 第二个动画:40% ~ 100% 的时间内完成
// (和第一个有 20% 的重叠,产生交错效果)
_secondAnimation = Tween<double>(begin: 0, end: 1).animate(
  CurvedAnimation(
    parent: _controller,
    curve: const Interval(0.4, 1.0, curve: Curves.easeOut),
  ),
);

Interval(begin, end) 表示动画在总时间轴的哪个区间内播放。0.0 是起点,1.0 是终点。

AnimatedWidget 简化写法

如果整个 Widget 都依赖动画,可以继承 AnimatedWidget 来简化代码。它的好处是不需要手写 AnimatedBuilder

dart
// 定义一个旋转图标的 Widget
class SpinningIcon extends AnimatedWidget {
  const SpinningIcon({super.key, required Animation<double> animation})
      : super(listenable: animation);

  Animation<double> get _progress => listenable as Animation<double>;

  @override
  Widget build(BuildContext context) {
    return Transform.rotate(
      angle: _progress.value * 2 * 3.14159,   // 0 → 2π(一圈)
      child: const Icon(Icons.refresh, size: 48),
    );
  }
}

使用时,只需传入 animation:

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

  @override
  State<SpinningDemo> createState() => _SpinningDemoState();
}

class _SpinningDemoState extends State<SpinningDemo>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat();  // 创建时直接循环
  }

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

  @override
  Widget build(BuildContext context) {
    return SpinningIcon(animation: _controller);  // 传入 controller 即可
  }
}

AnimatedBuilder vs AnimatedWidget 怎么选?

  • AnimatedBuilder:动画逻辑和使用动画的 Widget 在同一个地方,适合简单场景,代码直观
  • AnimatedWidget:把动画 Widget 封装成独立组件,适合复用场景,或者动画逻辑比较复杂时

初学阶段,推荐先用 AnimatedBuilder,等你发现某个动画组件需要复用时,再抽取成 AnimatedWidget

多个动画控制器

如果你的页面需要多个独立的动画(比如一个旋转、一个缩放),需要用 TickerProviderStateMixin(注意不是 SingleTickerProviderStateMixin):

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

  @override
  State<MultiAnimationDemo> createState() => _MultiAnimationDemoState();
}

class _MultiAnimationDemoState extends State<MultiAnimationDemo>
    with TickerProviderStateMixin {                    // ← 注意:没有 Single

  late AnimationController _scaleController;
  late AnimationController _rotateController;

  @override
  void initState() {
    super.initState();
    _scaleController = AnimationController(
      vsync: this,
      duration: const Duration(milliseconds: 300),
    );
    _rotateController = AnimationController(
      vsync: this,
      duration: const Duration(seconds: 2),
    )..repeat();
  }

  @override
  void dispose() {
    _scaleController.dispose();      // 每个都要释放
    _rotateController.dispose();
    super.dispose();
  }

  // ...
}

SingleTickerProviderStateMixin vs TickerProviderStateMixin

  • SingleTickerProviderStateMixin:只能创建一个 AnimationController,性能更好
  • TickerProviderStateMixin:可以创建多个 AnimationController

如果你只需要一个控制器,用 Single 版本;需要多个,用非 Single 版本。不要混用。

隐式动画 vs 显式动画

特性隐式动画显式动画
控制方式设置目标值,自动过渡手动控制 Controller
代码复杂度简单较复杂
控制精度
循环动画❌ 不支持✅ 支持
暂停/倒放❌ 不支持✅ 支持
多动画同步❌ 不支持✅ 支持
监听每帧❌ 不支持✅ 支持
适合场景简单过渡效果复杂动画、循环动画、精细控制

选择建议

能用隐式动画就用隐式动画,代码更少、更不容易出 bug。只有当隐式动画无法满足需求时,才使用显式动画。

完整示例:加载动画

下面是一个综合运用显式动画的例子——一个常见的加载指示器,包含旋转和缩放:

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

  @override
  State<LoadingIndicator> createState() => _LoadingIndicatorState();
}

class _LoadingIndicatorState extends State<LoadingIndicator>
    with SingleTickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _rotateAnimation;
  late Animation<double> _scaleAnimation;

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

    // 旋转:匀速一圈
    _rotateAnimation = Tween<double>(begin: 0, end: 1).animate(
      CurvedAnimation(parent: _controller, curve: Curves.linear),
    );

    // 缩放:脉冲效果
    _scaleAnimation = TweenSequence<double>([
      TweenSequenceItem(tween: Tween(begin: 1.0, end: 1.2), weight: 0.5),
      TweenSequenceItem(tween: Tween(begin: 1.2, end: 1.0), weight: 0.5),
    ]).animate(
      CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
    );

    _controller.repeat();  // 循环播放
  }

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

  @override
  Widget build(BuildContext context) {
    return AnimatedBuilder(
      animation: _controller,
      builder: (context, child) {
        return Transform.scale(
          scale: _scaleAnimation.value,
          child: Transform.rotate(
            angle: _rotateAnimation.value * 2 * 3.14159,
            child: child,
          ),
        );
      },
      child: const Icon(Icons.refresh, size: 48, color: Colors.blue),
    );
  }
}

TweenSequence

TweenSequence 可以定义多段动画,每段用 TweenSequenceItem 指定 Tween 和权重(weight)。上面的例子中,缩放动画分为两段:放大(1.0→1.2)和缩小(1.2→1.0),各占一半时间。

常见问题

1. vsync 是什么?

vsync(垂直同步)是一个 TickerProvider 对象,它告诉 AnimationController:"页面正在显示,请继续刷新动画"。当页面不可见时(如切到后台),它会自动暂停动画,避免无用的计算。

通过混入 SingleTickerProviderStateMixin,你的 State 就成了一个 TickerProvider,可以用 this 传入。

2. 忘记 dispose() 会怎样?

AnimationController 底层创建了一个 Ticker,如果不释放,即使页面销毁了,Ticker 仍在运行,导致内存泄漏。严重时可能造成应用卡顿甚至崩溃。

dart
@override
void dispose() {
  _controller.dispose();  // ← 永远不要忘记这行!
  super.dispose();
}

3. AnimatedBuilder 的 child 参数有什么用?

child 参数是性能优化的关键。把它放在 builder 外面,Flutter 只会构建一次,而不是每帧都重建:

dart
// ✅ 推荐:child 放外面,只构建一次
AnimatedBuilder(
  animation: _animation,
  builder: (context, child) {
    return Transform.scale(
      scale: _animation.value,
      child: child,           // 复用 child
    );
  },
  child: const Icon(Icons.star, size: 100),  // 不依赖动画的部分
)

// ❌ 不推荐:所有内容都写在 builder 里,每帧都重建
AnimatedBuilder(
  animation: _animation,
  builder: (context, _) {
    return Transform.scale(
      scale: _animation.value,
      child: const Icon(Icons.star, size: 100),  // 每帧都重新创建
    );
  },
)

4. 多个 Controller 怎么管理?

当页面有多个控制器时,建议统一管理:

dart
late final AnimationController _rotateController;
late final AnimationController _scaleController;
late final AnimationController _fadeController;

@override
void dispose() {
  _rotateController.dispose();
  _scaleController.dispose();
  _fadeController.dispose();
  super.dispose();
}

如果控制器太多,考虑用 AnimationController.unbounded() 或将动画逻辑抽取到单独的类中。

基于 Flutter 官方文档整理