动画
动画可以让应用更加生动和流畅。本章将介绍 Flutter 中的各种动画实现方式。
隐式动画
隐式动画是最简单的动画方式,使用带 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,
),
),
],
)
AnimatedTextStyle
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('动画文本'),
)
显式动画
显式动画提供更精细的控制,使用 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,
);
_animation = SpringSimulation(
SpringDescription.withDampingRatio(
mass: 1.0,
stiffness: 100.0,
ratio: 0.5,
),
0.0, // 起始位置
1.0, // 结束位置
0.0, // 初始速度
).drive(Tween<double>(begin: 0, end: 1));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
void _animate() {
_controller.animateWith(
SpringSimulation(
SpringDescription.withDampingRatio(
mass: 1.0,
stiffness: 100.0,
ratio: 0.5,
),
0.0,
1.0,
0.0,
),
);
}
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, 100 * _animation.value),
child: Container(
width: 50,
height: 50,
color: Colors.blue,
),
);
},
),
ElevatedButton(
onPressed: _animate,
child: Text('弹跳'),
),
],
);
}
}
小结
本章我们学习了:
- 隐式动画:AnimatedContainer、AnimatedOpacity 等
- 显式动画:AnimationController、Animation、Tween
- 交错动画:多个动画按顺序执行
- Hero 动画:页面切换时的共享元素过渡
- 物理动画:弹簧模拟等物理效果
练习
- 创建一个带动画的按钮,按下时有缩放和颜色变化效果
- 实现一个卡片翻转动画
- 创建一个带动画的列表项删除效果
- 实现一个加载动画