理解隐式动画的内部机制,并精细地驱动你自己的 AnimationController。
在 Kodokon 中打开本课AnimatedContainer 属于隐式动画这一大家族:你改变一个目标值,框架就为这个过渡制作动画。在底层,每个 ImplicitlyAnimatedWidget 都在它的 state 里藏着一个 AnimationController。当一份新的配置到来时,didUpdateWidget 会通过 forEachTween 遍历各个属性,并给每个 tween 重新设定目标:begin 边界变成当前插值出的值,end 边界变成新的目标,然后控制器从零重新开始。一个宝贵的结果是:在动画中途改变目标不会产生视觉上的跳变,动画会精确地从它所在的位置继续。
import 'package:flutter/material.dart';
class SelectableCard extends StatefulWidget {
const SelectableCard({super.key});
@override
State<SelectableCard> createState() =>
_SelectableCardState();
}
class _SelectableCardState extends State<SelectableCard> {
bool _selected = false;
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () =>
setState(() => _selected = !_selected),
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
curve: Curves.easeOutCubic,
width: _selected ? 220 : 160,
height: 96,
decoration: BoxDecoration(
color: _selected
? Colors.indigo
: Colors.indigo.shade200,
borderRadius: BorderRadius.circular(
_selected ? 28 : 12,
),
),
),
);
}
}隐式动画覆盖了框架能预料到的那些属性,而 TweenAnimationBuilder 则把这个过程推广到任何可插值的值。一旦你需要驱动动画,比如暂停、反向、重复、对状态做出反应,就切换到显式的 AnimationController。vsync 参数期待一个 TickerProvider:mixin 会创建一个 Ticker,也就是由 SchedulerBinding 在每一帧调用的回调。一个鲜为人知的微妙之处:TickerMode 会静默隐藏子树(例如被覆盖的路由之下)的 ticker。时间照常流逝,但再也没有回调运行了,不会在看不见的动画上浪费一丁点 CPU。
import 'package:flutter/material.dart';
class RevealPanel extends StatefulWidget {
const RevealPanel({super.key, required this.child});
final Widget child;
@override
State<RevealPanel> createState() => _RevealPanelState();
}
class _RevealPanelState extends State<RevealPanel>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(milliseconds: 400),
reverseDuration: const Duration(milliseconds: 200),
);
late final Animation<double> _opacity = _controller
.drive(CurveTween(curve: Curves.easeOutCubic));
void toggle() {
switch (_controller.status) {
case AnimationStatus.forward:
case AnimationStatus.completed:
_controller.reverse();
case AnimationStatus.reverse:
case AnimationStatus.dismissed:
_controller.forward();
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: toggle,
child: FadeTransition(
opacity: _opacity,
child: widget.child,
),
);
}
}一个控制器不过是一个从 0 跑到 1 的 Animation<double>:要组合它,而不是直接生用。controller.drive(CurveTween(...)) 会产生一个不带任何额外状态的派生动画。要把这个值应用到树上,优先使用 Transition 系列 widget(FadeTransition、RotationTransition):它们直接在渲染对象层面监听动画,不会每帧重建哪怕一个 widget。当变换是任意的时候,AnimatedBuilder 能胜任这份工作,前提是你要通过 child 传入静态子树:它只会构建一次,然后在每一帧被重新注入到 builder 中。这是显式动画中回报最高的优化。
import 'dart:math' as math;
import 'package:flutter/material.dart';
class Orbit extends StatefulWidget {
const Orbit({super.key});
@override
State<Orbit> createState() => _OrbitState();
}
class _OrbitState extends State<Orbit>
with SingleTickerProviderStateMixin {
late final AnimationController _controller =
AnimationController(
vsync: this,
duration: const Duration(seconds: 4),
)..repeat();
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
child: const Icon(Icons.satellite_alt, size: 32),
builder: (context, child) {
final angle = _controller.value * 2 * math.pi;
return Transform.translate(
offset: Offset(
math.cos(angle) * 80,
math.sin(angle) * 80,
),
child: child,
);
},
);
}
}