在 Flutter 动画系统中,Curve(曲线) 是控制动画变化速率的核心概念。它决定了动画在时间线上的变化节奏,使动画效果更加自然流畅。
曲线的作用原理
在动画系统中,曲线将线性时间进度(0.0 ~ 1.0)转换为非线性的变化过程:
常用内置曲线类型
Flutter 提供了丰富的内置曲线,以下是主要分类:
1. 基础缓动曲线
曲线名称 | 效果描述 | 适用场景 |
---|---|---|
Curves.linear | 匀速变化 | 机械运动、进度条 |
Curves.easeIn | 开始慢,逐渐加速 | 物体从静止开始运动 |
Curves.easeOut | 开始快,逐渐减速 | 物体逐渐停止运动 |
Curves.easeInOut | 开始和结束慢,中间快 | 最常见的自然运动效果 |
Curves.fastOutSlowIn | 快速开始,慢速结束(Material Design默认) | 移动设备UI动画 |
2. 弹性曲线
曲线名称 | 效果描述 |
---|---|
Curves.elasticIn | 开始时有弹性收缩效果 |
Curves.elasticOut | 结束时有弹性伸展效果 |
Curves.elasticInOut | 开始和结束都有弹性效果 |
3. 弹跳曲线
曲线名称 | 效果描述 |
---|---|
Curves.bounceIn | 结束时产生弹跳效果 |
Curves.bounceOut | 开始时产生弹跳效果 |
Curves.bounceInOut | 开始和结束都产生弹跳效果 |
4. 特殊效果曲线
曲线名称 | 效果描述 |
---|---|
Curves.decelerate | 急剧减速效果 |
Curves.slowMiddle | 中间慢,两端快 |
Curves.easeInBack | 开始时有轻微后退效果 |
Curves.easeOutBack | 结束时有轻微超出效果 |
曲线可视化示例
可复制代码查看运行效果
import 'package:flutter/material.dart';
class CurveVisualizer extends StatefulWidget {
const CurveVisualizer({super.key});
@override
State<CurveVisualizer> createState() => _CurveVisualizerState();
}
class _CurveVisualizerState extends State<CurveVisualizer> with SingleTickerProviderStateMixin {
late AnimationController _controller;
final Map<String, Curve> curves = {
'linear': Curves.linear,
'easeIn': Curves.easeIn,
'easeOut': Curves.easeOut,
'easeInOut': Curves.easeInOut,
'bounceIn': Curves.bounceIn,
'bounceOut': Curves.bounceOut,
'bounceInOut': Curves.bounceInOut,
'elasticIn': Curves.elasticIn,
'elasticOut': Curves.elasticOut,
'elasticInOut': Curves.elasticInOut,
'fastOutSlowIn': Curves.fastOutSlowIn,
'decelerate': Curves.decelerate,
};
String selectedCurve = 'linear';
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
// 曲线选择器
Padding(
padding: const EdgeInsets.all(16.0),
child: Wrap(
spacing: 8,
children: curves.keys.map((name) {
return ChoiceChip(
label: Text(name),
selected: selectedCurve == name,
onSelected: (selected) {
setState(() => selectedCurve = name);
},
);
}).toList(),
),
),
// 曲线可视化
Expanded(
child: CustomPaint(
painter: CurvePainter(
curve: curves[selectedCurve]!,
animation: _controller,
curveName: selectedCurve,
),
),
),
// 动画预览
SizedBox(
height: 100,
child: Center(
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final animation = CurvedAnimation(
parent: _controller,
curve: curves[selectedCurve]!,
);
return Container(
width: 50 + animation.value * 200,
height: 50,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(25),
),
);
},
),
),
),
],
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
}
class CurvePainter extends CustomPainter {
final Curve curve;
final Animation<double> animation;
final String curveName;
CurvePainter({
required this.curve,
required this.animation,
required this.curveName,
}) : super(repaint: animation);
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.blue
..strokeWidth = 3
..style = PaintingStyle.stroke;
final path = Path();
const pointCount = 100;
final points = List.generate(pointCount, (i) {
final t = i / (pointCount - 1);
final y = curve.transform(t);
return Offset(t * size.width, (1 - y) * size.height);
});
path.moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
path.lineTo(points[i].dx, points[i].dy);
}
canvas.drawPath(path, paint);
// 绘制当前动画点
final currentT = animation.value;
final currentY = curve.transform(currentT);
final currentPoint = Offset(
currentT * size.width,
(1 - currentY) * size.height
);
canvas.drawCircle(
currentPoint,
8,
Paint()..color = Colors.red
);
// 添加曲线名称
final textPainter = TextPainter(
text: TextSpan(
text: curveName,
style: const TextStyle(
color: Colors.black,
fontSize: 16,
fontWeight: FontWeight.bold
),
),
textDirection: TextDirection.ltr,
);
textPainter.layout();
textPainter.paint(
canvas,
Offset(size.width/2 - textPainter.width/2, 20)
);
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
自定义曲线实现
Flutter 允许开发者创建自定义动画曲线:
class CustomStepCurve extends Curve {
final int steps;
const CustomStepCurve({this.steps = 5});
@override
double transform(double t) {
return (t * steps).floorToDouble() / (steps - 1);
}
}
使用自定义曲线
// 在动画控制器中使用
_animation = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const CustomBounceCurve(elasticity: 0.5),
),
);
解析:
-
数学原理:离散化连续输入
t * steps
:将 [0,1] 映射到 [0, steps].floorToDouble()
:取整实现离散化/ (steps - 1)
:归一化到 [0,1] 范围
-
参数作用:
steps
:控制动画跳跃次数(必须 ≥2)- 默认 5 步:值变化序列为 0, 0.25, 0.5, 0.75, 1.0
-
动画效果:非连续的阶梯状跳跃动画
-
示例输出:
- t=0.1 → 0.0
- t=0.3 → 0.25
- t=0.7 → 0.75
- t=1.0 → 1.0
曲线选择建议
动画场景 | 推荐曲线 | 说明 |
---|---|---|
页面过渡 | Curves.fastOutSlowIn | Material Design 标准 |
按钮点击 | Curves.easeInOut | 自然平滑效果 |
加载指示器 | Curves.linear | 匀速运动 |
弹出对话框 | Curves.elasticOut | 弹性效果增强视觉反馈 |
下拉刷新 | Curves.bounceOut | 模拟物理弹跳 |
滑动消失 | Curves.decelerate | 快速开始,急剧减速 |
分步动画 | CustomStepCurve(steps:5) | 离散变化效果 |
背景波动效果 | CustomWaveCurve() | 波浪式变化 |
性能考虑
动画开发对性能的影响是非常大的,同一个动画的不同实现方式对性能的影响很可能不一样,所以在日常开发中有一些对性能产生明显有影响的地方尽量避免。
-
简单曲线性能最佳:
linear
,easeIn
,easeOut
等基础曲线计算开销最小- 弹性、弹跳曲线计算复杂度较高
-
避免频繁重建:
// 错误:每次build都重建CurvedAnimation Widget build() { final animation = CurvedAnimation(parent: controller, curve: curve); // ... } // 正确:在initState或类成员中创建 late final Animation<double> animation; @override void initState() { super.initState(); animation = CurvedAnimation(parent: controller, curve: curve); }
-
预计算曲线值: 对于特别复杂的自定义曲线,可以预计算值表:
final valueTable = List.generate(100, (i) { return customCurve.transform(i / 100); }); double getCurveValue(double t) { final index = (t * 99).clamp(0, 99).toInt(); return valueTable[index]; }
总结
Flutter 的曲线系统为动画提供了强大的节奏控制能力:
- 内置曲线丰富:覆盖了大多数常见动画场景
- 自定义灵活:通过继承 Curve 类实现任意变化规律
- 物理集成:可与 SpringSimulation 等物理模拟结合
- 性能可控:简单曲线高效,复杂曲线需谨慎使用
掌握曲线原理是创建自然流畅动画的关键,合理选择曲线可以:
- 增强用户界面的响应感
- 提供有意义的视觉反馈
- 引导用户注意力
- 提升整体用户体验