2、补间动画Tween:Tween用法、线性插值、自定义Tween、链式动画
好,咱们接着聊补间动画。说白了,Tween 就是 Flutter 动画里最基础的那个“数值搬运工”。你给它一个起始值、一个结束值,它就在中间帮你算出所有过渡帧。我刚开始学 Flutter 动画时,觉得这玩意儿太简单了,不就是个线性插值嘛。后来踩了几个坑才发现,Tween 的玩法比想象中深得多。
2.1 Tween 的基本用法
先看最基础的用法。你有一个 AnimationController,它输出 0.0 到 1.0 的数值。Tween 的作用,就是把 0.0~1.0 映射到你想要的区间。
final AnimationController controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
final Animation<double> animation = Tween<double>(
begin: 0.0,
end: 200.0,
).animate(controller);
这段代码,就是把 0~1 映射成 0~200。controller 走到 0.5 时,animation.value 就是 100.0。嗯,这里要注意:Tween 本身不驱动动画,它只是做映射。驱动动画的还是 controller。
2.2 线性插值的本质
Tween 默认用的是线性插值。什么意思?就是匀速变化。从 begin 到 end,时间走多少,数值就变多少。
公式其实很简单:
value = begin + (end - begin) * t
其中 t 是 0.0~1.0 的进度。这个公式,就是 Tween 的 lerp 方法干的事。
我在项目中遇到过一个问题:用 Tween<double> 做透明度动画,从 0.0 到 1.0,看起来没问题。但换成 Tween<Color> 时,发现颜色过渡有点“愣”。为什么?因为 Color 的线性插值,是在 RGB 空间里直接算的。红绿蓝各自线性变化,结果就是颜色会经过一些“脏”的中间色。
2.3 自定义 Tween
Flutter 内置了 DoubleTween、ColorTween、SizeTween 等。但有时候你需要处理自定义类型,比如一个自定义的渐变对象,或者一个复杂的数学曲线。这时候就得自己写 Tween。
自定义 Tween 其实很简单,继承 Tween<T>,然后重写 lerp 方法就行。
class GradientTween extends Tween<Gradient> {
GradientTween({Gradient? begin, Gradient? end})
: super(begin: begin, end: end);
@override
Gradient lerp(double t) {
// 这里实现你自己的插值逻辑
return LinearGradient(
colors: [
Color.lerp(begin!.colors[0], end!.colors[0], t)!,
Color.lerp(begin!.colors[1], end!.colors[1], t)!,
],
);
}
}
你看,核心就是 lerp 方法。它接收一个 t 值(0.0~1.0),返回你想要的中间状态。我建议你在写自定义 Tween 时,先想清楚你的数据在“中间状态”应该长什么样。比如一个圆角矩形的 borderRadius,中间状态就是四个角半径的线性插值。
lerp 方法会被频繁调用(每帧一次)。如果你的插值逻辑里有耗时操作,比如图片解码、网络请求,那动画就会卡顿。我曾经犯过这个错,在 lerp 里做了字符串拼接和正则匹配,结果动画掉帧严重。后来改成预计算,问题就解决了。
2.4 链式动画
链式动画,说白了就是让动画一个接一个地跑。Flutter 里实现链式动画,最常用的方式是 TweenSequence。
final Animation<double> animation = TweenSequence<double>([
TweenSequenceItem(
tween: Tween<double>(begin: 0.0, end: 100.0),
weight: 1,
),
TweenSequenceItem(
tween: Tween<double>(begin: 100.0, end: 200.0),
weight: 2,
),
]).animate(controller);
这里 weight 控制每个阶段的时间占比。第一个阶段 weight 是 1,第二个是 2,所以第二个阶段占的时间是第一个的两倍。总时长还是由 controller 的 duration 决定。
我个人习惯用 TweenSequence 做“弹跳效果”。比如一个按钮先放大,再缩小回原样。用两个 Tween 串起来,比用 Curves 调半天要直观得多。
还有一种链式动画的方式,是用 addStatusListener 监听动画状态,在动画结束时启动下一个动画。这种方式更灵活,但代码会分散一些。
controller.addStatusListener((status) {
if (status == AnimationStatus.completed) {
// 启动下一个动画
secondController.forward();
}
});
2.5 实战小技巧
最后分享几个我在项目中积累的小技巧:
- 复用 Tween 对象:Tween 是不可变对象,可以安全复用。我一般把常用的 Tween 定义成静态常量,避免重复创建。
- 调试时打印中间值:在
addListener里打印 animation.value,能帮你快速定位插值问题。 - 注意边界情况:t 值可能小于 0 或大于 1(比如用了弹性曲线)。你的自定义 Tween 要能处理这些边界值,否则会崩溃。
- 用
evaluate方法:如果你只想算一次插值,不想创建 Animation,可以直接用tween.evaluate(controller)。这个我经常在初始化时用。
嗯,Tween 这块内容就这些。说白了,它就是动画的“翻译官”,把 controller 的 0~1 翻译成你需要的任何数值。掌握了自定义 Tween,你就能做出各种花里胡哨的动画效果。下一章咱们聊聊曲线动画,看看怎么让动画“动得更有感觉”。