动画
精心设计的动画能够让用户界面更加直观,提升应用的精致感,并改善用户体验。Flutter 提供了强大的动画支持,让开发者可以轻松实现各种动画效果。许多 Widget(尤其是 Material 组件)都内置了标准的动效,同时 Flutter 也支持完全自定义的动画效果。
动画类型概述
Flutter 中的动画主要分为两大类:
| 类型 | 特点 | 适用场景 |
|---|---|---|
| 隐式动画 | 框架自动管理,代码简单 | 简单的属性变化动画 |
| 显式动画 | 开发者完全控制,灵活性高 | 复杂的、需要精确控制的动画 |
如何选择动画方式?
根据你的需求,可以按照以下思路选择合适的动画实现方式:
- 只需要简单动画? 使用预置的隐式动画 Widget(如
AnimatedContainer、AnimatedOpacity) - 预置 Widget 不够用? 使用
TweenAnimationBuilder创建自定义隐式动画 - 需要精确控制动画过程? 使用内置的显式动画 Widget(如
SlideTransition、FadeTransition) - 需要完全自定义? 使用
AnimatedBuilder或AnimatedWidget创建自定义显式动画
动画基础概念
Animation 对象
Animation 对象是 Flutter 动画系统的核心。它是一个抽象类,能够:
- 知道当前的值(通过
.value访问) - 知道当前的状态(完成、 dismissed 等)
- 生成两个值之间的插值
重要理解:Animation 对象本身不涉及屏幕渲染或 build() 函数。它只是一个值生成器,需要配合其他 Widget 使用才能产生视觉效果。
// Animation 对象的基本用法
Animation<double> animation; // 定义
// 访问当前值
double currentValue = animation.value;
// 监听值变化
animation.addListener(() {
print('当前值: ${animation.value}');
});
// 监听状态变化
animation.addStatusListener((status) {
print('状态: $status');
});
AnimationController
AnimationController 是一个特殊的 Animation 对象,它在每一帧生成一个新值。默认情况下,它在给定的持续时间内线性生成 0.0 到 1.0 之间的数值。
class MyAnimation extends StatefulWidget {
@override
_MyAnimationState createState() => _MyAnimationState();
}
class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin { // 需要 mixin
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2), // 持续时间
vsync: this, // 防止离屏动画消耗资源
);
}
@override
void dispose() {
_controller.dispose(); // 必须释放资源
super.dispose();
}
}
关于 vsync 参数:vsync 参数用于防止离屏动画消耗不必要的资源。通过将 SingleTickerProviderStateMixin 添加到类定义中,可以将 Stateful 对象作为 vsync 使用。
Tween(补间)
默认情况下,AnimationController 的值范围是 0.0 到 1.0。如果需要不同的范围或数据类型,可以使用 Tween。
// 数值范围
final tween = Tween<double>(begin: -200, end: 0);
// 颜色渐变
final colorTween = ColorTween(begin: Colors.red, end: Colors.blue);
// 尺寸变化
final sizeTween = SizeTween(
begin: Size(100, 100),
end: Size(200, 200),
);
// 使用 Tween
Animation<double> animation = Tween<double>(begin: 0, end: 300).animate(_controller);
Tween 的工作原理:Tween 是一个无状态对象,它只定义了从输入范围到输出范围的映射。它的 evaluate() 方法将动画的当前值(通常是 0.0 到 1.0)转换为实际的动画值。
CurvedAnimation(动画曲线)
CurvedAnimation 定义动画的非线性进度,让动画更加自然。
final curvedAnimation = CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
);
// 常用曲线
Curves.linear // 线性(匀速)
Curves.ease // 默认缓动
Curves.easeIn // 开始慢,然后加速
Curves.easeOut // 开始快,然后减速
Curves.easeInOut // 两头慢,中间快
Curves.bounceIn // 弹跳进入
Curves.bounceOut // 弹跳出
Curves.elasticIn // 弹性进入
Curves.elasticOut // 弹性出
Curves.fastOutSlowIn // 快出慢进
// 自定义曲线
class ShakeCurve extends Curve {
@override
double transform(double t) => sin(t * pi * 2);
}
隐式动画
隐式动画是最简单的动画方式,使用带 Animated 前缀的 Widget。框架会自动处理动画的启动和停止,开发者只需指定目标值即可。
AnimatedContainer
class AnimatedContainerExample extends StatefulWidget {
@override
_AnimatedContainerExampleState createState() =>
_AnimatedContainerExampleState();
}
class _AnimatedContainerExampleState
extends State<AnimatedContainerExample> {
bool _isExpanded = false;
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedContainer(
duration: Duration(milliseconds: 300),
curve: Curves.easeInOut,
width: _isExpanded ? 200 : 100,
height: _isExpanded ? 200 : 100,
decoration: BoxDecoration(
color: _isExpanded ? Colors.blue : Colors.red,
borderRadius: BorderRadius.circular(_isExpanded ? 50 : 0),
),
child: Center(child: Text('点击改变')),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
setState(() => _isExpanded = !_isExpanded);
},
child: Text(_isExpanded ? '缩小' : '放大'),
),
],
);
}
}
AnimatedOpacity
AnimatedOpacity(
opacity: _visible ? 1.0 : 0.0,
duration: Duration(milliseconds: 500),
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
AnimatedPositioned
Stack(
children: [
AnimatedPositioned(
duration: Duration(milliseconds: 500),
left: _isMoved ? 200 : 50,
top: _isMoved ? 200 : 50,
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
),
],
)
AnimatedDefaultTextStyle
AnimatedDefaultTextStyle(
duration: Duration(milliseconds: 300),
style: TextStyle(
fontSize: _isLarge ? 32 : 16,
color: _isLarge ? Colors.blue : Colors.black,
fontWeight: _isLarge ? FontWeight.bold : FontWeight.normal,
),
child: Text('动画文本'),
)
TweenAnimationBuilder:自定义隐式动画
当预置的隐式动画 Widget 不能满足需求时,可以使用 TweenAnimationBuilder 创建自定义隐式动画:
class CustomImplicitAnimation extends StatefulWidget {
@override
_CustomImplicitAnimationState createState() => _CustomImplicitAnimationState();
}
class _CustomImplicitAnimationState extends State<CustomImplicitAnimation> {
double _value = 0;
@override
Widget build(BuildContext context) {
return Column(
children: [
TweenAnimationBuilder<double>(
tween: Tween<double>(begin: 0, end: _value),
duration: Duration(milliseconds: 500),
curve: Curves.easeOutCubic,
builder: (context, value, child) {
// value 从 begin 动画过渡到 end
return Transform.rotate(
angle: value * 2 * pi, // 旋转
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(
child: Text(
'${(value * 100).toStringAsFixed(0)}%',
style: TextStyle(color: Colors.white),
),
),
),
);
},
),
SizedBox(height: 20),
Slider(
value: _value,
onChanged: (newValue) {
setState(() => _value = newValue);
},
),
],
);
}
}
TweenAnimationBuilder 的优势:
- 不需要管理 AnimationController
- 代码更简洁
- 适合一次性的属性变化动画
内置过渡动画(显式动画 Widget)
Flutter 提供了一系列内置的过渡动画 Widget,它们结合了隐式动画的简单性和显式动画的灵活性。这些 Widget 接受 Animation 对象作为参数,但不需要手动管理监听器。
SlideTransition(滑动过渡)
class SlideTransitionExample extends StatefulWidget {
@override
_SlideTransitionExampleState createState() => _SlideTransitionExampleState();
}
class _SlideTransitionExampleState extends State<SlideTransitionExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<Offset> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 500),
vsync: this,
);
_animation = Tween<Offset>(
begin: Offset(-1, 0), // 从左侧
end: Offset.zero, // 到中间
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
SlideTransition(
position: _animation,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('滑动')),
),
),
ElevatedButton(
onPressed: () => _controller.forward(),
child: Text('滑入'),
),
],
);
}
}
FadeTransition(淡入淡出)
FadeTransition(
opacity: _animation, // Animation<double> 类型
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
ScaleTransition(缩放过渡)
ScaleTransition(
scale: _animation, // Animation<double> 类型
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
RotationTransition(旋转过渡)
RotationTransition(
turns: _animation, // Animation<double> 类型,单位是圈
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
SizeTransition(尺寸过渡)
SizeTransition(
sizeFactor: _animation, // Animation<double> 类型
axis: Axis.vertical, // 垂直或水平
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
PositionedTransition(位置过渡)
Stack(
children: [
PositionedTransition(
rect: RelativeRectTween(
begin: RelativeRect.fromLTRB(0, 0, 200, 200),
end: RelativeRect.fromLTRB(100, 100, 0, 0),
).animate(_animation),
child: Container(
color: Colors.blue,
),
),
],
)
DecoratedBoxTransition(装饰过渡)
DecoratedBoxTransition(
decoration: DecorationTween(
begin: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(0),
),
end: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(50),
),
).animate(_animation),
child: Container(
width: 100,
height: 100,
),
)
显式动画
显式动画提供更精细的控制,使用 AnimationController 和 Animation。
基本结构
class ExplicitAnimationExample extends StatefulWidget {
@override
_ExplicitAnimationExampleState createState() =>
_ExplicitAnimationExampleState();
}
class _ExplicitAnimationExampleState extends State<ExplicitAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// 创建动画控制器
_controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this,
);
// 创建动画
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(parent: _controller, curve: Curves.easeInOut),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _startAnimation() {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
}
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: Transform.scale(
scale: _animation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _startAnimation,
child: Text('播放动画'),
),
],
);
}
}
AnimationController 方法
_controller.forward() // 从头播放到尾
_controller.reverse() // 从尾播放到头
_controller.repeat() // 重复播放
_controller.stop() // 停止
_controller.reset() // 重置到初始状态
_controller.animateTo(0.5) // 动画到指定值
Tween 类型
// 数值
Tween<double>(begin: 0, end: 100)
// 颜色
ColorTween(begin: Colors.red, end: Colors.blue)
// 尺寸
SizeTween(begin: Size(100, 100), end: Size(200, 200))
// 位置
OffsetTween(begin: Offset.zero, end: Offset(100, 100))
// 边界
EdgeInsetsTween(begin: EdgeInsets.all(10), end: EdgeInsets.all(20))
// 对齐
AlignmentTween(begin: Alignment.topLeft, end: Alignment.bottomRight)
动画曲线
Curves.linear // 线性
Curves.ease // 缓入缓出
Curves.easeIn // 缓入
Curves.easeOut // 缓出
Curves.easeInOut // 缓入缓出
Curves.bounceIn // 弹跳进入
Curves.bounceOut // 弹跳出
Curves.elasticIn // 弹性进入
Curves.elasticOut // 弹性出
交错动画
交错动画让多个动画按顺序或重叠执行:
class StaggeredAnimationExample extends StatefulWidget {
@override
_StaggeredAnimationExampleState createState() =>
_StaggeredAnimationExampleState();
}
class _StaggeredAnimationExampleState extends State<StaggeredAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(4, (index) {
final delay = index * 0.2;
final animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(
delay,
delay + 0.5,
curve: Curves.easeOut,
),
),
);
return Transform.translate(
offset: Offset(0, -50 * animation.value),
child: Container(
width: 40,
height: 40,
color: Colors.primaries[index * 2],
),
);
}),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
if (_controller.isCompleted) {
_controller.reverse();
} else {
_controller.forward();
}
},
child: Text('播放'),
),
],
);
}
}
Hero 动画
Hero 动画在页面切换时实现共享元素过渡:
// 源页面
GestureDetector(
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => DetailPage()),
);
},
child: Hero(
tag: 'hero-image',
child: Image.network(
'https://picsum.photos/100',
width: 100,
height: 100,
),
),
)
// 目标页面
Hero(
tag: 'hero-image', // 相同的 tag
child: Image.network(
'https://picsum.photos/400',
width: 300,
height: 300,
),
)
物理动画
使用物理模拟实现更自然的动画效果。物理动画模拟真实世界的行为,例如弹簧、重力、摩擦等,让动画更加逼真。
弹簧动画
class SpringAnimationExample extends StatefulWidget {
@override
_SpringAnimationExampleState createState() => _SpringAnimationExampleState();
}
class _SpringAnimationExampleState extends State<SpringAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: Duration(seconds: 1),
);
// 使用 SpringSimulation 创建弹簧动画
_animation = Tween<double>(begin: 0, end: 1).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut, // 使用内置的弹性曲线
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: Container(
width: 100,
height: 100,
color: Colors.blue,
child: Center(child: Text('弹跳')),
),
);
},
),
SizedBox(height: 20),
ElevatedButton(
onPressed: () {
_controller.forward(from: 0);
},
child: Text('弹跳'),
),
],
);
}
}
自定义物理模拟
使用 Simulation 类创建自定义物理效果:
void _animateWithPhysics() {
final simulation = SpringSimulation(
SpringDescription.withDampingRatio(
mass: 1.0, // 质量
stiffness: 100.0, // 刚度
ratio: 0.7, // 阻尼比
),
0.0, // 起始位置
1.0, // 结束位置
0.0, // 初始速度
);
_controller.animateWith(simulation);
}
物理模拟参数说明:
| 参数 | 说明 | 影响 |
|---|---|---|
mass | 质量 | 质量越大,惯性越大,动画越慢 |
stiffness | 刚度 | 刚度越大,弹簧越硬,振荡越快 |
ratio | 阻尼比 | 1.0 为临界阻尼(不振荡),小于 1.0 会振荡 |
AnimatedBuilder 详解
AnimatedBuilder 是创建自定义显式动画的核心工具,它可以高效地重建 Widget 树的一部分。
基本用法
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// 每帧都会调用
return Transform.scale(
scale: _animation.value,
child: child, // 使用缓存的 child
);
},
// child 只构建一次,提高性能
child: Container(
width: 100,
height: 100,
color: Colors.blue,
),
)
性能优化关键:child 参数只构建一次,然后在每次动画帧中重用。如果将所有内容放在 builder 中,每帧都会重建整个子树。
组合多个动画
class MultiAnimationExample extends StatefulWidget {
@override
_MultiAnimationExampleState createState() => _MultiAnimationExampleState();
}
class _MultiAnimationExampleState extends State<MultiAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _widthAnimation;
late Animation<double> _heightAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
);
// 使用 Interval 创建交错动画
_widthAnimation = Tween<double>(begin: 100, end: 200).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.5, curve: Curves.easeOut), // 前半段
),
);
_heightAnimation = Tween<double>(begin: 100, end: 150).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.3, 0.8, curve: Curves.easeOut), // 中间段
),
);
_colorAnimation = ColorTween(begin: Colors.blue, end: Colors.red).animate(
CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeOut), // 后半段
),
);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
width: _widthAnimation.value,
height: _heightAnimation.value,
color: _colorAnimation.value,
);
},
),
ElevatedButton(
onPressed: () => _controller.forward(from: 0),
child: Text('播放组合动画'),
),
],
);
}
}
AnimatedBuilder vs AnimatedWidget
除了 AnimatedBuilder,还可以使用 AnimatedWidget 创建自定义动画:
// 使用 AnimatedWidget
class AnimatedBox extends AnimatedWidget {
const AnimatedBox({Key? key, required Animation<double> animation})
: super(key: key, listenable: animation);
@override
Widget build(BuildContext context) {
final animation = listenable as Animation<double>;
return Container(
width: animation.value * 100,
height: animation.value * 100,
color: Colors.blue,
);
}
}
// 使用
AnimatedBox(animation: _animation)
两种方式的对比:
| 特性 | AnimatedBuilder | AnimatedWidget |
|---|---|---|
| 代码组织 | 内联在 build 中 | 独立类 |
| 复用性 | 较低 | 较高 |
| 适用场景 | 简单动画 | 可复用的动画组件 |
AnimatedList 动画列表
AnimatedList 为列表项的添加和删除提供内置动画支持:
class AnimatedListExample extends StatefulWidget {
@override
_AnimatedListExampleState createState() => _AnimatedListExampleState();
}
class _AnimatedListExampleState extends State<AnimatedListExample> {
final GlobalKey<AnimatedListState> _listKey = GlobalKey<AnimatedListState>();
final List<String> _items = ['项目 1', '项目 2', '项目 3'];
int _counter = 4;
void _addItem() {
final newIndex = _items.length;
final newItem = '项目 $_counter';
_items.add(newItem);
_counter++;
_listKey.currentState?.insertItem(newIndex);
}
void _removeItem(int index) {
final removedItem = _items[index];
_items.removeAt(index);
_listKey.currentState?.removeItem(
index,
(context, animation) => _buildItem(removedItem, animation),
);
}
Widget _buildItem(String item, Animation<double> animation) {
return SizeTransition(
sizeFactor: animation,
child: ListTile(
title: Text(item),
trailing: IconButton(
icon: Icon(Icons.delete),
onPressed: () => _removeItem(_items.indexOf(item)),
),
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: AnimatedList(
key: _listKey,
initialItemCount: _items.length,
itemBuilder: (context, index, animation) {
return _buildItem(_items[index], animation);
},
),
),
ElevatedButton(
onPressed: _addItem,
child: Text('添加项目'),
),
],
);
}
}
性能优化
1. 使用 const 构造函数
// 好:const Widget 只构建一次
const Text('不变的文本')
// 差:每次重建都创建新实例
Text('不变的文本')
2. 正确使用 AnimatedBuilder 的 child 参数
// 好:child 只构建一次
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: child,
);
},
child: ExpensiveWidget(), // 复杂的 Widget
)
// 差:每帧都重建整个子树
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.scale(
scale: _animation.value,
child: ExpensiveWidget(), // 每帧都重建!
);
},
)
3. 避免在动画中重建整个 Widget 树
// 差:整个页面每帧都重建
setState(() {
_animationValue = animation.value;
});
// 好:只重建动画部分
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(_animation.value * 100, 0),
child: child,
);
},
child: RestOfTheUI(),
)
4. 使用 RepaintBoundary
对于复杂动画,使用 RepaintBoundary 减少重绘范围:
RepaintBoundary(
child: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return CustomPaint(
painter: MyPainter(_animation.value),
);
},
),
)
5. 合理选择动画类型
| 场景 | 推荐方案 |
|---|---|
| 简单属性变化 | 隐式动画(AnimatedXxx) |
| 复杂但一次性的动画 | TweenAnimationBuilder |
| 需要精确控制 | 显式动画 + AnimatedBuilder |
| 可复用的动画组件 | AnimatedWidget |
| 列表项动画 | AnimatedList |
小结
本章我们详细学习了 Flutter 动画系统:
- 动画类型:隐式动画和显式动画的选择
- 核心概念:Animation、AnimationController、Tween、CurvedAnimation
- 隐式动画:AnimatedContainer、AnimatedOpacity、TweenAnimationBuilder
- 内置过渡动画:SlideTransition、FadeTransition、ScaleTransition 等
- 显式动画:AnimationController 的完整控制
- AnimatedBuilder:自定义动画的高效方式
- 交错动画:Interval 实现多动画协调
- Hero 动画:页面间共享元素过渡
- 物理动画:弹簧模拟等真实物理效果
- 性能优化:正确使用 child 参数、RepaintBoundary 等
练习
- 创建一个带动画的按钮,按下时有缩放和颜色变化效果
- 实现一个卡片翻转动画(3D 旋转效果)
- 创建一个带动画的列表项删除效果
- 实现一个波浪效果的加载动画
- 使用 AnimatedList 实现一个待办事项列表