简介
动画曲线(Curves)定义了动画在执行过程中的变化速率。合适的动画曲线可以让动画效果更加自然和流畅。
内置曲线
常用曲线
// 线性
curve: Curves.linear
// 缓入
curve: Curves.easeIn
curve: Curves.easeInQuad
curve: Curves.easeInCubic
// 缓出
curve: Curves.easeOut
curve: Curves.easeOutQuad
curve: Curves.easeOutCubic
// 缓入缓出
curve: Curves.easeInOut
curve: Curves.easeInOutQuad
curve: Curves.easeInOutCubic
// 弹性
curve: Curves.elasticIn
curve: Curves.elasticOut
curve: Curves.elasticInOut
// 弹跳
curve: Curves.bounceIn
curve: Curves.bounceOut
curve: Curves.bounceInOut
曲线示例
class CurvesDemo extends StatefulWidget {
@override
_CurvesDemoState createState() => _CurvesDemoState();
}
class _CurvesDemoState extends State<CurvesDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
final List<Curve> curves = [
Curves.linear,
Curves.easeIn,
Curves.easeOut,
Curves.easeInOut,
Curves.elasticIn,
Curves.elasticOut,
Curves.bounceIn,
Curves.bounceOut,
];
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Curves Demo')),
body: ListView.builder(
itemCount: curves.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
Text(curves[index].toString()),
SizedBox(height: 8),
SlideTransition(
position: Tween(
begin: Offset(-1, 0),
end: Offset(1, 0),
).animate(CurvedAnimation(
parent: _controller,
curve: curves[index],
)),
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
],
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Icon(Icons.play_arrow),
),
);
}
}
自定义曲线
基本实现
class CustomCurve extends Curve {
@override
double transform(double t) {
// t 从 0.0 到 1.0
// 返回值也应该从 0.0 到 1.0
return t * t; // 示例:平方曲线
}
}
复杂曲线
import 'dart:math'; // pow、sin、pi 需要这个导入
class ElasticOutCurve extends Curve {
final double period;
const ElasticOutCurve([this.period = 0.4]);
@override
double transform(double t) {
if (t == 0.0 || t == 1.0) {
return t;
}
double s = period / 4.0;
t = t - 1.0;
return pow(2.0, -10.0 * t) *
sin((t - s) * (pi * 2.0) / period) + 1.0;
}
}
组合曲线
Interval
CurvedAnimation(
parent: controller,
curve: Interval(
0.0,
0.5,
curve: Curves.easeOut,
),
)
曲线序列
class SequenceCurve extends Curve {
final List<Curve> curves;
final List<double> weights;
SequenceCurve(this.curves, this.weights) {
assert(curves.length == weights.length);
assert(weights.reduce((a, b) => a + b) == 1.0);
}
@override
double transform(double t) {
double total = 0.0;
for (int i = 0; i < curves.length; i++) {
double weight = weights[i];
if (t < total + weight) {
double localT = (t - total) / weight;
return curves[i].transform(localT);
}
total += weight;
}
return t;
}
}
完整示例
class AnimationCurvesDemo extends StatefulWidget {
@override
_AnimationCurvesDemoState createState() => _AnimationCurvesDemoState();
}
class _AnimationCurvesDemoState extends State<AnimationCurvesDemo>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation1;
late Animation<double> _animation2;
late Animation<double> _animation3;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 3),
vsync: this,
);
// 基本曲线
_animation1 = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
// 自定义曲线
_animation2 = CurvedAnimation(
parent: _controller,
curve: CustomCurve(),
);
// 组合曲线
_animation3 = CurvedAnimation(
parent: _controller,
curve: SequenceCurve(
[Curves.easeOut, Curves.elasticOut],
[0.3, 0.7],
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('Animation Curves')),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildAnimationDemo('Ease In Out', _animation1),
_buildAnimationDemo('Custom Curve', _animation2),
_buildAnimationDemo('Sequence Curve', _animation3),
],
),
floatingActionButton: FloatingActionButton(
onPressed: () {
if (_controller.status == AnimationStatus.completed) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Icon(Icons.play_arrow),
),
);
}
Widget _buildAnimationDemo(String title, Animation<double> animation) {
return Column(
children: [
Text(title),
SizedBox(height: 8),
AnimatedBuilder(
animation: animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(200 * animation.value - 100, 0),
child: Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8),
),
),
);
},
),
],
);
}
}
最佳实践
- 入场/位移动画默认用 Curves.easeOut(快进慢收),符合手指滑动后内容”滑入”的直觉;元素离开/收起用 easeIn;需要对称感的缩放(如对话框弹出)用 easeInOut 或 fastOutSlowIn(Material 默认的强调曲线)。
- elastic/bounce 只用在”点状强调”:收藏按钮点按的那一下回弹、开关拨动的抖动,位移范围限制在几十 dp 内;用在大段位移(页面转场、抽屉滑出)上会显得拖沓廉价。
- 曲线和时长是绑定的:easeOut 类 200
400ms 足够;elasticOut 自带多次回弹,总时长要给到 600800ms,否则回弹被截断、动画看起来”没播完”——只调曲线不调时长等于白调。 - 自定义 Curve 的 transform 里只做纯数值计算,sin/pow 级运算可以,但别做 IO、别分配大对象、别读 MediaQuery——它每帧对每个动画都会被调用,高频调用下这些都是帧率杀手。
注意事项
- 曲线的输入范围是 0.0 到 1.0,但输出可以超调(elastic、bounce 类曲线的返回值会大于 1 或小于 0),写自定义 Curve 时不要假设输出也被夹在 0~1
- 拼接曲线时检查拼接点的连续性:分段函数在 t=0/t=1 或拼接处左右值对不上,动画起止帧会肉眼可见地”跳一下”;transform(0) 必须返回 0、transform(1) 必须返回 1。
- 关键动效至少在 60Hz 真机上看一遍:高刷屏(120Hz)上同样的 duration 看起来比 60Hz 更快,动画”感觉”不一样;低端机掉帧会让弹性曲线卡成三段——模拟器和开发机说了不算。
- 嵌套曲线别超过两层:Interval 里再包 easeOut 已经是极限,再往里塞自定义曲线,出了节奏问题根本说不清是哪一层在捣乱——一层 CurvedAnimation 解决不了再加。
实战:曲线选错比没动画更伤
曲线这东西,选错了比不做动画更伤体验。我的经验就两条:第一,页面进入、列表 item 出现这类”位移”动画,用 Curves.easeOut(快进慢出)最保险,符合手指滑动的直觉;第二,elastic、bounce 这类弹性曲线只适合小范围的强调——比如收藏按钮被点的那一下回弹,用在大面积位移上会显得廉价、拖沓。
另外要纠正这篇正文里的一个说法:Curve 的输入确实是 0.01.0,但输出不一定被夹在这个区间——elasticOut 在中间过程返回值会大于 1(超调),这就是弹性动画的 widget 会”冲过头”再弹回来的原因。写自定义 Curve 时记住这点,别在 transform 里假设输出也被限制在 01,否则弹性效果会被你亲手掐掉。
参考:Flutter 官方文档《Animations》(docs.flutter.dev/ui/animations)
总结
动画曲线是创建流畅自然动画的关键。通过合理使用内置曲线、自定义曲线和组合曲线,可以实现各种丰富的动画效果。理解并掌握动画曲线的使用对于开发高质量的 Flutter 动画很有帮助。
