在这里插入图片描述

在这里插入图片描述

子玥酱 (掘金 / 知乎 / CSDN / 简书 同名)

大家好,我是 子玥酱,一名长期深耕在一线的前端程序媛 👩‍💻。曾就职于多家知名互联网大厂,目前在某国企负责前端软件研发相关工作,主要聚焦于业务型系统的工程化建设与长期维护。

我持续输出和沉淀前端领域的实战经验,日常关注并分享的技术方向包括 前端工程化、小程序、React / RN、Flutter、跨端方案
在复杂业务落地、组件抽象、性能优化以及多端协作方面积累了大量真实项目经验。

技术方向:前端 / 跨端 / 小程序 / 移动端工程化
内容平台:
掘金、知乎、CSDN、简书
创作特点:
实战导向、源码拆解、少空谈多落地
文章状态:
长期稳定更新,大量原创输出

我的内容主要围绕 前端技术实战、真实业务踩坑总结、框架与方案选型思考、行业趋势解读 展开。文章不会停留在“API 怎么用”,而是更关注为什么这么设计、在什么场景下容易踩坑、真实项目中如何取舍,希望能帮你在实际工作中少走弯路。

子玥酱 · 前端成长记录官 ✨
👋 如果你正在做前端,或准备长期走前端这条路
📚 关注我,第一时间获取前端行业趋势与实践总结
🎁 可领取 11 类前端进阶学习资源(工程化 / 框架 / 跨端 / 面试 / 架构)
💡 一起把技术学“明白”,也用“到位”

持续写作,持续进阶。
愿我们都能在代码和生活里,走得更稳一点 🌱

引言

很多人第一次写 Flutter 动画的时候都会有一种错觉:
“这也太简单了吧?”

一个 AnimationController,配个 Tween,加个 AnimatedBuilder,效果就出来了。

但项目一旦做大,你会发现动画代码越来越难维护:

  • UI 里到处是 controller
  • 多个动画组合起来逻辑混乱
  • 页面状态和动画状态纠缠在一起

动画本身不难,难的是“怎么把它写得不痛苦”。

下面我们聊聊几个核心问题。

显式动画和隐式动画,怎么选才不会后悔

Flutter 提供了两套体系:

显式动画:
AnimationController + Tween

隐式动画:
AnimatedContainerAnimatedOpacity 这一类

很多人一上来就用显式动画,因为“更灵活”。但实际上,大多数场景根本不需要。

比如简单的状态切换:

AnimatedContainer(
  duration: Duration(milliseconds: 300),
  width: isExpand ? 200 : 100,
  height: 100,
)

这里你完全不需要自己管理 controller。

问题在于:当你习惯用显式动画后,哪怕只是个渐变效果,也会写成一整套控制逻辑。

选型原则其实很简单:

  • 单一属性变化,用隐式动画
  • 多段控制、可打断、可反转,用显式动画

如果没有时间轴控制需求,就不要引入 AnimationController

因为一旦用了 controller,你就要处理:

  • 生命周期 dispose
  • 状态同步
  • 重建时机

复杂度是成倍增加的。

动画代码侵入 UI,是维护痛苦的开始

很多人会这样写:

late AnimationController _controller;
late Animation<double> _opacity;


void initState() {
  _controller = AnimationController(
    vsync: this,
    duration: Duration(milliseconds: 300),
  );
  _opacity = Tween(begin: 0.0, end: 1.0).animate(_controller);
  _controller.forward();
}

然后在 build 里:

FadeTransition(
  opacity: _opacity,
  child: child,
)

看起来没问题,但当页面动画多起来时:

  • 5 个 controller
  • 10 个 Tween
  • 一堆状态变量

UI 文件会迅速膨胀。

真正的问题不是代码多,而是:

动画逻辑和业务逻辑混在一起。

当产品改需求,比如:

“这个动画延迟 100ms 再开始”,你就得改页面代码,而不是改动画模块,这就是耦合带来的成本。

动画应该抽象,而不是堆在页面里

一个更清晰的做法是:把动画封装成组件。

例如封装一个淡入组件:

class FadeIn extends StatefulWidget {
  final Widget child;
  final Duration duration;

  const FadeIn({required this.child, this.duration = const Duration(milliseconds: 300)});
}

内部自己管理 controller,对外只暴露 child。

页面只写:

FadeIn(child: Text("Hello"))

这样:

  • 页面干净
  • 动画逻辑可复用
  • 修改动画不会影响业务代码

当动画变复杂时,可以进一步抽象成“动画驱动器”,而不是散落在页面里。

多动画组合为什么会失控

真实项目里很少只有一个动画。比如一个卡片展开效果,可能包含:

  • 高度变化
  • 透明度变化
  • 阴影变化
  • 位移动画

很多人会写多个 Tween:

_height = Tween(begin: 100, end: 200).animate(_controller);
_opacity = Tween(begin: 0.5, end: 1.0).animate(_controller);
_offset = Tween(begin: Offset(0, 20), end: Offset.zero).animate(_controller);

问题是:

  • 状态依赖 controller.value
  • 动画耦合时间轴
  • 修改某个动画必须考虑其他动画

更清晰的方式是使用 TweenSequence 或封装“组合动画对象”。

核心思路是:

一个动画场景,一个控制器。

不要一个 controller 管多个无关逻辑,也不要多个 controller 控同一个场景。

动画组合如果没有统一时间轴,很容易变成维护灾难。

动画状态不要和业务状态混在一起

另一个常见问题是:

bool isExpanded = false;

onTap() {
  setState(() {
    isExpanded = !isExpanded;
  });
  if (isExpanded) {
    _controller.forward();
  } else {
    _controller.reverse();
  }
}

这里有两个状态:

  • 业务状态 isExpanded
  • 动画状态 controller.status

当用户快速点击、多次打断动画时,很容易出现状态不同步。

更好的方式是:以动画状态驱动 UI,而不是用业务布尔值控制动画。

例如通过:

_controller.addStatusListener(...)

或者将动画状态抽象成单独的状态类,让业务层只发“展开”意图,不直接控制动画。

动画是表现层逻辑,不要让它反向控制业务。

一套更舒服的动画实践

结合实际项目经验,可以总结一套比较稳定的策略:

  • 能用隐式动画就不用显式
  • 一个场景一个 controller
  • 动画组件化,避免侵入页面
  • 动画与业务状态分离
  • 所有 controller 必须集中管理生命周期

当动画复杂到一定程度,可以考虑:

  • 把动画逻辑放入独立类
  • 使用统一的 AnimationConfig
  • 页面只关心“触发”和“结束”

这样做的好处是:产品改动时,你只改动画层,而不是重写页面结构。

总结

Flutter 动画之所以“写着简单,维护痛苦”,不是因为 API 复杂。而是因为我们太容易把动画直接塞进页面里。动画本质上是表现逻辑,它应该服务于 UI,而不是和 UI 纠缠。

当你开始:

  • 抽象动画
  • 隔离状态
  • 控制复杂度

你会发现动画不再是负担,而是可以被管理的模块。动画难的从来不是实现,而是结构设计。结构清晰,动画才会优雅。

Logo

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

更多推荐