1、Flutter动画基础:动画核心概念、AnimationController、Ticker、动画生命周期

动画,说白了就是让UI「动起来」。但Flutter的动画机制,跟传统的前端动画不太一样。我第一次接触时也有点懵——怎么还要管什么Ticker、Controller?后来做多了才发现,这套设计其实非常优雅。

咱们今天就把动画的底裤扒干净。从最核心的概念讲起,再到Controller怎么用,最后聊聊生命周期。嗯,保证你听完就能上手。

1.1 动画的核心三要素

Flutter动画其实就三个东西:值的变化时间的驱动界面的刷新

  • Animation对象:它本身不干活,只是存一个「当前值」。比如从0变到1,它告诉你现在是多少。
  • AnimationController:真正的「发动机」。控制动画开始、暂停、反向、结束。
  • Ticker:每秒60次的「心跳」。每次心跳通知Controller:该更新值了。

一句话总结:Ticker负责喊「到点了」,Controller负责算「现在值是多少」,Animation负责存「当前值」,最后Widget根据这个值重新绘制。

我在项目中遇到过不少新手,直接new了一个Animation就以为能跑。结果界面一动不动。为什么?因为没有Controller驱动,Animation就是个死数据。

1.2 AnimationController——动画的指挥官

Controller是动画的核心。它继承自Animation,所以它本身就是一个Animation对象。但它多了几个关键能力:

  • 控制能力:forward()、reverse()、repeat()、stop()、reset()
  • 时间管理:duration属性决定动画时长
  • 值范围:默认从0.0到1.0,但可以自定义

看个最简单的例子:

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
  late AnimationController _controller;

  @override
  void initState() {
    super.initState();
    _controller = AnimationController(
      duration: const Duration(seconds: 2),
      vsync: this,  // 这个vsync就是TickerProvider
    );
  }

  void _startAnimation() {
    _controller.forward(); // 从0到1,跑2秒
  }

  @override
  void dispose() {
    _controller.dispose(); // 必须释放!不然内存泄漏
    super.dispose();
  }
}

个人习惯:我一般把Controller的dispose放在dispose方法的第一行。因为曾经有一次我忘了写,结果页面切走再回来,动画还在后台跑——CPU直接拉满。嗯,血的教训。

1.3 Ticker——动画的「心跳」

Ticker是什么?说白了就是一个回调函数,每帧调用一次。Flutter的屏幕刷新率是60Hz,所以Ticker每秒会触发60次回调。

但Ticker不能直接用。你需要通过TickerProvider来获取。最常见的做法是让State混入SingleTickerProviderStateMixin:

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> with SingleTickerProviderStateMixin {
  // 现在这个State就是一个TickerProvider了
  // 可以传给AnimationController的vsync参数
}

为什么非要这么麻烦?因为Ticker需要跟Widget的生命周期绑定。页面不可见时,Ticker应该暂停,避免浪费性能。Flutter帮你做了这件事——当Widget树被移除时,Ticker自动停止。

注意:如果你需要多个Controller,就用TickerProviderStateMixin(复数)。但大多数场景下,一个Controller就够了。我见过有人为了省事,一个页面混入了两个TickerProvider,结果把自己绕晕了。

1.4 动画生命周期——从生到死

一个完整的动画生命周期,大概分这么几步:

  1. 创建:在initState中初始化Controller
  2. 启动:调用forward()或repeat()
  3. 运行:每帧更新值,触发addListener回调
  4. 结束:到达目标值,触发statusListener(状态变为completed或dismissed)
  5. 销毁:在dispose中释放Controller

这里有个容易被忽略的点:动画状态监听。你可以通过addStatusListener来监听动画的状态变化:

_controller.addStatusListener((status) {
  if (status == AnimationStatus.completed) {
    // 动画跑完了
    // 我曾经在这里做了一件蠢事:直接调用了dispose()
    // 结果后续想反向播放,Controller已经没了
    // 正确做法:用reverse()或者reset()
  } else if (status == AnimationStatus.dismissed) {
    // 动画回到了起点
  }
});

避坑指南:我曾经在动画completed时直接dispose了Controller,然后想再播放一次,发现报错了。后来我改成用forward()配合repeat(),或者用reverse()做往返动画。记住:Controller的生命周期应该由Widget的dispose管理,而不是由动画状态管理

1.5 实战小技巧

讲几个我常用的套路:

  • 延迟启动:用Future.delayed配合forward(),实现入场动画的错峰
  • 曲线控制:给Controller加CurvedAnimation,让动画更自然
  • 值映射:用Tween<double>().animate(controller)把0~1映射到任意范围

举个例子,让一个按钮从透明到完全显示,同时从底部滑上来:

_controller = AnimationController(
  duration: Duration(milliseconds: 500),
  vsync: this,
);

final Animation<double> _opacity = Tween(begin: 0.0, end: 1.0).animate(
  CurvedAnimation(parent: _controller, curve: Curves.easeIn),
);

final Animation<Offset> _slide = Tween(
  begin: Offset(0, 0.5),
  end: Offset(0, 0),
).animate(_controller);

这样写的好处是:一个Controller驱动多个动画,同步性好,代码也干净。

1.6 总结

动画基础就这些。记住三个关键词:Controller驱动Ticker心跳生命周期绑定。你想想看,Flutter这套设计其实很聪明——把动画的控制权交给开发者,但把性能优化的脏活(比如页面不可见时暂停动画)交给框架。

下一章咱们会讲Tween和Curve,到时候你会看到,动画的「值变化」可以玩出多少花样。嗯,先消化今天的内容,动手写个简单的旋转动画试试?