简介
Flutter 提供了丰富的动画系统,除了使用内置的动画组件外,我们还可以通过自定义 Tween、动画控制器和动画组件来实现更加复杂的动画效果。
自定义 Tween
基本实现
class ColorTween extends Tween<Color> {
ColorTween({required Color begin, required Color end})
: super(begin: begin, end: end);
@override
Color lerp(double t) {
return Color.lerp(begin, end, t)!;
}
}
// 使用示例
final colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
复杂 Tween
class BoxDecorationTween extends Tween<BoxDecoration> {
final ColorTween? colorTween;
final BorderRadiusTween? borderRadiusTween;
final BoxShadowTween? shadowTween;
BoxDecorationTween({
BoxDecoration? begin,
BoxDecoration? end,
}) : colorTween = begin?.color != null || end?.color != null
? ColorTween(
begin: begin?.color ?? Colors.transparent,
end: end?.color ?? Colors.transparent,
)
: null,
borderRadiusTween = begin?.borderRadius != null ||
end?.borderRadius != null
? BorderRadiusTween(
begin: begin?.borderRadius as BorderRadius?,
end: end?.borderRadius as BorderRadius?,
)
: null,
shadowTween = begin?.boxShadow?.first != null ||
end?.boxShadow?.first != null
? BoxShadowTween(
begin: begin?.boxShadow?.first,
end: end?.boxShadow?.first,
)
: null,
super(begin: begin, end: end);
@override
BoxDecoration lerp(double t) {
return BoxDecoration(
color: colorTween?.lerp(t),
borderRadius: borderRadiusTween?.lerp(t),
boxShadow: shadowTween?.lerp(t) != null
? [shadowTween!.lerp(t)!]
: null,
);
}
}
自定义动画控制器
基本实现
class CustomAnimationController extends AnimationController {
CustomAnimationController({
required TickerProvider vsync,
Duration? duration,
Duration? reverseDuration,
}) : super(
vsync: vsync,
duration: duration,
reverseDuration: reverseDuration,
);
void playWithDelay(Duration delay) async {
await Future.delayed(delay);
forward();
}
void repeatWithInterval(Duration interval) async {
while (true) {
await forward();
await Future.delayed(interval);
await reverse();
await Future.delayed(interval);
}
}
}
使用示例
class CustomAnimationDemo extends StatefulWidget {
@override
_CustomAnimationDemoState createState() => _CustomAnimationDemoState();
}
class _CustomAnimationDemoState extends State<CustomAnimationDemo>
with SingleTickerProviderStateMixin {
late CustomAnimationController _controller;
@override
void initState() {
super.initState();
_controller = CustomAnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
_controller.playWithDelay(Duration(seconds: 2));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.scale(
scale: _controller.value,
child: child,
);
},
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
);
}
}
自定义动画组件
基本实现
class CustomAnimatedWidget extends StatefulWidget {
final Widget child;
final Duration duration;
final Curve curve;
CustomAnimatedWidget({
required this.child,
this.duration = const Duration(milliseconds: 300),
this.curve = Curves.easeInOut,
});
@override
_CustomAnimatedWidgetState createState() => _CustomAnimatedWidgetState();
}
class _CustomAnimatedWidgetState extends State<CustomAnimatedWidget>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: widget.duration,
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: widget.curve,
);
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: Opacity(
opacity: _animation.value,
child: child,
),
);
},
child: widget.child,
);
}
}
完整示例
class WaveAnimation extends StatefulWidget {
@override
_WaveAnimationState createState() => _WaveAnimationState();
}
class _WaveAnimationState extends State<WaveAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late List<Animation<double>> _animations;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animations = List.generate(
5,
(index) => Tween<double>(
begin: 0.0,
end: 1.0,
).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
index * 0.2,
(index + 1) * 0.2,
curve: Curves.easeOutCirc,
),
),
),
);
_controller.repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Stack(
alignment: Alignment.center,
children: List.generate(5, (index) {
return Transform.scale(
scale: 1 + _animations[index].value,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.blue.withOpacity(
(1 - _animations[index].value) * 0.5,
),
),
),
);
}),
);
},
),
);
}
}
最佳实践
- 先穷尽内置:Curves 有几十个,Tween 也有 ColorTween/BorderRadiusTween 等十几种——“两个 BoxDecoration 带圆角和阴影一起过渡”官方文档就有现成模式;只有内置表达不了”值”的插值规则时,才值得手写 lerp。
- 自定义 Tween 的 lerp 必须保证 t=0 返回 begin、t=1 返回 end;Color.lerp 在两端都为 null 时返回 null——用 ! 断言前先想清楚 null 语义,别无脑断言,否则切主题/透明色过渡时直接 crash。
- 写完收进组件库:自定义动画类是长期维护成本,别散落在业务页面——放进 ui 包或组件目录,配好默认 duration 和 curve 参数,业务方只传 child 调用。
- 自定义 controller 的 async 方法(如文中的 playWithDelay)在 await 之后先检查 mounted:等待期间页面可能已 pop,此时再 forward 会在已释放的 controller 上抛异常。
注意事项
- lerp 每帧都会被调用:里面只做纯数值插值,别分配对象、别调 setState、别读 MediaQuery——高频调用下这些都会变成帧率杀手;需要外部数据先在外面算好传进来。
- 继承 AnimationController 加 async 循环(如 repeatWithInterval 的 while(true))必须有退出条件:页面 dispose 时跳出循环,否则 controller dispose 后 forward 抛异常,无限循环还一直占着事件循环。
- 自定义 Curve/Tween 配三个单元断言:transform(0)==0、transform(1)==1、lerp(0.5) 落在 begin/end 中点——5 分钟写完,能拦住大部分”我改了一行结果动画抽风”。
实战:这个场景我还没在项目里遇到
说实话,手写自定义 Tween、自定义 Curve 的场景,我在实际项目里还没遇到过——电商 App 的动画需求,隐式动画加 Interval 组合基本全覆盖了,carousel_slider 这类组件又把轮播这种高频需求包掉了。
但我按官方文档的思路给自己定了条线:只有当内置 Tween 表达不了”值”的变化时,才值得写自定义 Tween——比如两个 BoxDecoration 之间带圆角和阴影一起过渡,官方文档的 BoxDecorationTween 示例就是干这个的;自定义 Curve 同理,先看 Curves 内置的几十个够不够,不够再写 transform。自定义动画的代价是维护成本:每个自定义类都是一份长期要读的代码,能不写就不写,真写了就把它收进组件库,别散落在业务页面里。
参考:Flutter 官方文档《Animations》(docs.flutter.dev/ui/animations)
总结
自定义动画为我们提供了更大的灵活性,通过合理使用可以实现更加复杂和独特的动画效果。理解并掌握自定义动画的开发对于创建高质量的 Flutter 应用很有帮助。
