关于 animations
面向 Flutter 开发者的动画技能,依据内置动画 API 和 Material 3 动效规范指导实现与审查。重点是在足够时采用隐式动画,并避免触发布局的动画方式。
主要功能
- 01选择隐式或显式动画
- 02使用 Material 3 时长与缓动令牌
- 03控制器生命周期与 Ticker 指引
- 04交错动画与页面转场模式
- 05动画性能与反模式检查
适用场景
- 01实现目标值驱动的界面转场
- 02构建分步或循环动画
- 03审查 Flutter 动画代码
- 04添加 GoRouter 页面转场
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ verygoodopensource/ vgv-ai-flutter-plugin --skill animations安装后在 Claude Code 中输入/animations确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
agents/openai.yamlreferences/explicit-animations.mdimplicit-animations.mdlooping-animations.mdpage-transitions.mdstaggered-animations.md详细介绍
动画
使用内置动画框架并遵循 Material 3 动效指南的 Flutter 动画最佳实践。不使用第三方动画库(Lottie、Rive 等)。
核心标准
所有动画工作都应遵循以下标准:
- 需求含糊时,先明确视觉意图 —— 如果开发者只说“添加动画”或“让它更流畅”,却没有指定属性、触发条件、时长或曲线,请先询问,再编写代码。如果开发者提供了明确规格(例如“卡片出现时,以 300ms 时长和 ease-in 曲线淡入”),则直接着手实现
- 采用可行的最简单动画方案 —— 遵循下方的决策树;只要隐式动画足以满足需求,就绝不要使用
AnimationController,即使要同时动画多个属性也一样 - 即使按名称指定控制器,也坚持使用隐式形式 —— 如果目标值动画本来只需隐式动画,却要求“用
AnimationController和AnimatedBuilder实现”,这就属于下方所述的反模式。请编写隐式版本,用一句话说明它为何足够,然后结束。不要一边说明一边提供控制器实现,也不要反问对方想要哪种实现。如果开发者读完理由后再次明确要求使用控制器,再实现控制器版本 - 时长和缓动使用 Material 3 动效令牌 —— 绝不硬编码任意
Duration或Curve值 - 提取动画常量 —— 时长、曲线和偏移量应放入具名常量或集中管理的
AppMotion类中,而非内联 - 释放控制器 —— 每个
AnimationController都必须在State的dispose()方法中、调用super.dispose()之前释放 - 单个控制器使用
SingleTickerProviderStateMixin—— 仅当 widget 拥有多个控制器时才使用TickerProviderStateMixin - 缩小动画子树范围 —— 只将发生变化的 widget 放入动画构建器中,不要包裹整个 widget 树
- 绝不对触发布局的属性进行动画处理 ——
width、height、padding和SizedBox尺寸会在每一帧触发新一轮布局,无论树中只有一个子节点还是结构很深。改用Transform:用Transform.scale调整大小,用Transform.translate调整位置,用Opacity实现淡入淡出。这些操作在合成层运行,不会触发布局 - Dart 3.13 主构造函数 —— 基线版本为 Dart 3.13+ 时,将 widget 字段声明为主构造函数的声明参数(
class const LabelChip({required final String label, super.key}) extends StatelessWidget),而不是使用this.field;仅在版本低于 3.13 时保留经典形式
动画决策树
选择满足需求的最简单方案:
Does the widget rebuild when the value changes?
|
YES --> Does the framework provide an AnimatedFoo widget?
| |
| YES --> Use the implicit AnimatedFoo widget
| | (AnimatedContainer, AnimatedOpacity, AnimatedAlign, etc.)
| |
| NO --> Use TweenAnimationBuilder
|
NO --> Do you need fine-grained control?
(repeat, reverse, sequence, listen to status)
|
YES --> Use AnimationController + AnimatedBuilder
|
NO --> Use TweenAnimationBuilder
经验法则: 如果动画是“设定目标值并让它动到那里”,就使用隐式动画。 如果动画必须按指令播放、暂停、反向或重复,就使用显式动画。
同时为两个属性添加动画仍属于隐式动画。 数据到达时淡入并向上滑动的卡片,可以嵌套两个隐式动画 widget,每个只设置一个目标值。同步发生不等于按顺序播放:只有当第二个属性必须在第一个开始后才开始变化,或动画需要播放控制时,才使用控制器。由标志切换驱动的入场动画——例如值到达、布尔值切换或项目出现——无论有多少属性发生变化,都属于隐式动画。
Material 3 动效令牌
使用 Flutter 内置的 Durations 和 Easing 类——新代码中绝不硬编码 Duration(milliseconds: ...),也不使用 Curves.*。这些框架常量与 Material 3 动效规范保持一致。
// Bad — arbitrary values with no semantic meaning
AnimatedContainer(
duration: Duration(milliseconds: 375),
curve: Curves.easeInOutCubic,
)
// Good — M3 tokens with clear intent
AnimatedContainer(
duration: Durations.medium2,
curve: Easing.standard,
)
集中管理的动效常量
如果项目在多个功能中使用动画,请引入 AppMotion 类。若应用中只有一个动画,内联使用 M3 令牌即可。
abstract class AppMotion {
// Standard transitions
static const Duration standardDuration = Durations.medium2;
static const Curve standardCurve = Easing.standard;
// Page transitions
static const Duration pageDuration = Durations.medium4;
static const Curve pageEnterCurve = Easing.emphasizedDecelerate;
static const Curve pageExitCurve = Easing.emphasizedAccelerate;
// Fades
static const Duration fadeDuration = Durations.short3;
static const Curve fadeCurve = Easing.standard;
}
隐式动画
当组件以新的目标值重建时,使用隐式动画。框架会自动插值。Flutter 提供了内置的 AnimatedFoo 组件(AnimatedContainer、AnimatedOpacity、AnimatedSlide、AnimatedSwitcher 等)——请使用与所要设置动画的属性相匹配的组件。如果没有内置组件可用,请使用 TweenAnimationBuilder。
当多个属性一起变化时,每个属性都组合使用一个 AnimatedFoo。下面是入场动画的模式——组件在数据到达前保持隐藏,随后淡入并滑动到位:
class const SummaryCard({required final Summary? summary, super.key})
extends StatelessWidget {
@override
Widget build(BuildContext context) {
final hasData = summary != null;
return AnimatedOpacity(
opacity: hasData ? 1 : 0,
duration: Durations.medium2,
curve: Easing.standard,
child: AnimatedSlide(
offset: hasData ? Offset.zero : const Offset(0, 0.1),
duration: Durations.medium2,
curve: Easing.emphasizedDecelerate,
child: Card(child: _SummaryContents(summary: summary)),
),
);
}
}
无需 StatefulWidget、控制器、ticker 或 dispose。两个属性会在同一次重建中开始动画。
TweenAnimationBuilder
当没有适用于该属性的内置 AnimatedFoo 组件,但仍希望使用隐式风格的“设置后无需操心”动画时,请使用 TweenAnimationBuilder。
TweenAnimationBuilder<double>(
tween: Tween(begin: 0, end: isActive ? 1.0 : 0.0),
duration: Durations.medium2,
curve: Easing.standard,
builder: (context, value, child) {
return Transform.scale(
scale: 0.8 + (0.2 * value),
child: Opacity(
opacity: value,
child: child,
),
);
},
child: child, // child is not rebuilt — optimization
)
child 参数至关重要:传入不依赖动画值的组件,以避免不必要的重建。
显式动画
当需要控制播放行为时,使用显式动画:播放、暂停、反向播放、重复播放,或监听动画状态。
AnimationController 设置
class _MyWidgetState extends State<MyWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _fadeAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Durations.medium2,
vsync: this,
);
_fadeAnimation = CurvedAnimation(
parent: _controller,
curve: Easing.standard,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _fadeAnimation,
builder: (context, child) {
return Opacity(
opacity: _fadeAnimation.value,
child: child,
);
},
child: child, // static child — not rebuilt each frame
);
}
}
关于 didUpdateWidget 模式、用于创建可测试控制器的构造函数注入,以及过渡组件与 AnimatedBuilder 的使用建议,请参阅 references/explicit-animations.md。
使用 Interval 实现交错动画
在 CurvedAnimation 中使用 Interval,让单个控制器上的动画交错播放——滑动动画会在淡入动画开始一段时间后启动,而不是与其同时开始。Interval 区间彼此重叠,正是这种模式的核心。
如果多个属性要同时动画到目标值,就不该使用这种工具。若淡入和滑动都在同一次重建中开始,应使用两个隐式动画组件,而不是使用一个带两个区间的控制器:
late final Animation<double> _fadeAnimation = CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Easing.standard),
);
late final Animation<Offset> _slideAnimation = Tween<Offset>(
begin: const Offset(0, 0.25),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.2, 0.8, curve: Easing.emphasized),
),
);
完整的交错入场和交错列表示例,请参阅 references/staggered-animations.md。重复动画和脉冲动画模式,请参阅 references/looping-animations.md。
页面过渡
自定义页面过渡可通过 GoRouteData.buildPage 中的 CustomTransitionPage 与 GoRouter 集成。当多个路由需要页面过渡时,请将其提取到共享的 AppPageTransitions 助手中——切勿在多个路由中内联相同的 transitionsBuilder。关于该助手、GoRouter 接线和 Hero 共享元素过渡,请参阅 references/page-transitions.md。
性能
应该做
- 为
Transform和Opacity设置动画——它们作用于合成层,可跳过布局和绘制 - 在
AnimatedBuilder和TweenAnimationBuilder中使用child参数,避免静态组件在每一帧都重建 - 在复杂布局中的动画组件周围使用
RepaintBoundary,以隔离重绘
不应该做
- 不要将整个屏幕包在
AnimatedBuilder中——只包裹发生变化的子树 - 不要为共享计时的动画创建多个
AnimationController实例——请在单个控制器上使用Interval
反模式
为宽度设置动画,而不是使用 Transform
// Bad — every frame re-runs layout on the SizedBox and everything under it
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return SizedBox(
width: 200 + (_controller.value * 120),
child: child,
);
},
child: const ExpensiveChart(),
)
// Good — Transform.scale runs on the compositing layer, no layout pass
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scaleX: 1 + (_controller.value * 0.6),
child: child,
);
},
child: const ExpensiveChart(),
)
代码审查时,请明确指出此问题:动画化的 width 或 height 会在每一帧强制执行布局,解决方法是使用 Transform.scale 或 Transform.translate。
每帧重建静态子项
// Bad — entire subtree rebuilds 60 times/second
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _controller.value,
child: const ExpensiveWidget(), // rebuilt every frame
);
},
)
能用隐式动画时却显式使用动画。这种情况尤其需要留意,因为通常请求一开始就已经把错误答案写进去了。
// Bad — unnecessary complexity for a simple target-value animation
class _FadeWidgetState extends State<FadeWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
// ... 20+ lines of boilerplate
// Good — one widget, zero boilerplate
AnimatedOpacity(
duration: Durations.short3,
curve: Easing.standard,
opacity: isVisible ? 1.0 : 0.0,
child: child,
)
“在 StatefulWidget 中使用 AnimationController 和 AnimatedBuilder 进行设置,让动画正确接入”——对于由布尔值驱动的淡入淡出,这个请求说的正是上面那种错误写法。请给出 AnimatedOpacity 版本,简要说明理由,并省略控制器。即使代码片段符合要求,只在备注中推荐更简单的写法,最终仍会带上那些样板代码。
其他资源
- references/implicit-animations.md —
AnimatedFoo组件、组合多个属性、TweenAnimationBuilder - references/explicit-animations.md — 控制器设置、使用
Interval错开动画、didUpdateWidget、可测试的控制器、过渡组件与AnimatedBuilder的对比 - references/staggered-animations.md — 错开的入场动画和错开的列表项动画
- references/page-transitions.md — 可复用的
AppPageTransitions辅助工具、GoRouter 集成,以及Hero共享元素过渡 - references/looping-animations.md — 重复、脉动和持续旋转模式