《Flutter全栈开发实战指南:从零到高级》- 17 -核心动画
引言
动画是移动应用的灵魂,能够增强页面交互效果,比如:微信下拉刷新的旋转动画、支付宝页面切换的平滑过渡、抖音点赞动效等等这些炫酷的动画效果使你的App更加出彩。今天我们就来深入探讨Flutter中那些让人惊艳的动画效果,一步步掌握动画的核心技巧!
一、动画核心类介绍
1.1 动画的本质是什么?
在深入代码之前,我们先要理解动画的本质。简单来说,动画就是在一段时间内连续改变属性值的过程。
举个例子:
- 一个按钮从透明变成不透明(改变opacity值)
- 一个图标从左边移动到右边(改变position值)
- 一个容器从小变大(改变size值)
用伪代码表示就是:
// 伪代码
开始值 = 0.0
结束值 = 1.0
持续时间 = 1.0
// 在1秒内,当前值从0.0线性变化到1.0
当前值 = 开始值 + (结束值 - 开始值) * (已用时间 / 持续时间)
1.2 动画核心类
Flutter的动画系统建立在以下四个核心类基础之上:
// 1. AnimationController
AnimationController controller;
// 2. Animation
Animation<double> animation;
// 3. Tween
Tween<double> tween;
// 4. Curve
CurvedAnimation curvedAnimation;
如果把动画效果比作开车,别想歪了,哈哈!想象一下你将车从A点开到B点:
AnimationController就像是你的脚踩油门和刹车,控制车的启动、停止、加速、减速Tween就像是导航,告诉你从A点(开始值)到B点(结束值)的路线Animation就像是车速表,实时显示当前的速度值Curve就像是道路状况,决定了你是匀速前进、先快后慢,还是有什么特殊的加速模式
1.3 核心类的职责
Animation - 值容器
Animation<double> sizeAnimation;
// 主要职责:
// - 持有当前动画值
// - 通知监听器值发生变化
// - 管理动画状态
// 添加值监听器
sizeAnimation.addListener(() {
setState(() {
// 当动画值变化时,重建Widget。。。
});
});
// 添加状态监听器
sizeAnimation.addStatusListener((status) {
switch (status) {
case AnimationStatus.dismissed:
print('动画在开始状态');
break;
case AnimationStatus.forward:
print('正向执行');
break;
case AnimationStatus.reverse:
print('反向执行');
break;
case AnimationStatus.completed:
print('执行完成');
break;
}
});
AnimationController
class _MyAnimationState extends State<MyAnimation>
with SingleTickerProviderStateMixin {
late AnimationController controller;
@override
void initState() {
super.initState();
// 创建动画控制器
controller = AnimationController(
duration: Duration(seconds: 2), // 持续2秒
vsync: this, // 垂直同步
);
}
@override
void dispose() {
controller.dispose(); // 释放
super.dispose();
}
}
这里有个重要概念:vsync
vsync的作用是防止动画在页面不可见时继续运行,造成浪费资源。通过SingleTickerProviderStateMixin,当页面被遮挡或切换到后台时,动画会自动暂停。
二、隐式动画
2.1 什么是隐式动画?
本质就是告诉Widget最终的状态是什么,Flutter会自动帮你生成过渡动画。
- 普通Widget:不加任何动画效果,widget会很生硬的直接过渡过去;
- 隐式动画Widget:从旧状态到新状态间有一个平滑的过渡,用户体验更好一些;
2.2 隐式动画组件
2.2.1 AnimatedContainer
AnimatedContainer:最常用的隐式动画组件,几乎所有的容器属性动画都可以用它来完成。
class AnimatedContainerExample extends StatefulWidget {
@override
_AnimatedContainerExampleState createState() => _AnimatedContainerExampleState();
}
class _AnimatedContainerExampleState extends State<AnimatedContainerExample> {
// 定义可变属性
double _width = 100.0;
double _height = 100.0;
Color _color = Colors.blue;
BorderRadius _borderRadius = BorderRadius.circular(8.0);
void _toggleAnimation() {
setState(() {
// 改变属性值
_width = _width == 100.0 ? 200.0 : 100.0;
_height = _height == 100.0 ? 200.0 : 100.0;
_color = _color == Colors.blue ? Colors.green : Colors.blue;
_borderRadius = _borderRadius == BorderRadius.circular(8.0)
? BorderRadius.circular(50.0) // 变成圆
: BorderRadius.circular(8.0); // 变回圆角矩形
});
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
AnimatedContainer(
width: _width,
height: _height,
duration: Duration(seconds: 1), // 动画时长
curve: Curves.easeInOut, // 动画曲线
decoration: BoxDecoration(
color: _color,
borderRadius: _borderRadius,
),
child: Center(
child: Text(
'点我',
style: TextStyle(color: Colors.white),
),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleAnimation,
child: Text('触发动画'),
),
],
);
}
}
AnimatedContainer支持的动画属性:
-
尺寸:
width、height -
颜色:
color -
边框圆角:
borderRadius -
内边距:
padding -
外边距:
margin -
阴影:
boxShadow -
变换:
transform
2.2.2 AnimatedOpacity
用来处理透明度变化的组件,非常适合实现显示/隐藏的过渡效果。
class AnimatedOpacityExample extends StatefulWidget {
@override
_AnimatedOpacityExampleState createState() => _AnimatedOpacityExampleState();
}
class _AnimatedOpacityExampleState extends State<AnimatedOpacityExample> {
double _opacity = 1.0; // 1.0 = 完全显示,0.0 = 完全隐藏
void _toggleVisibility() {
setState(() {
_opacity = _opacity == 1.0 ? 0.0 : 1.0;
});
}
@override
Widget build(BuildContext context) {
return Column(
children: [
AnimatedOpacity(
opacity: _opacity,
duration: Duration(seconds: 1),
curve: Curves.easeInOut,
child: Container(
width: 200,
height: 200,
color: Colors.blue,
child: Center(
child: Text(
'淡入淡出',
style: TextStyle(color: Colors.white, fontSize: 20),
),
),
),
),
SizedBox(height: 20),
ElevatedButton(
onPressed: _toggleVisibility,
child: Text(_opacity == 1.0 ? '隐藏' : '显示'),
),
],
);
}
}
使用场景:
- 页面元素的显示/隐藏
- 加载状态的过渡
- 错误信息的淡入淡出
2.2.3 其他隐式动画组件
// AnimatedPadding - 内边距动画
AnimatedPadding(
padding: EdgeInsets.all(_paddingValue),
duration: Duration(seconds: 1),
child: YourWidget(),
)
// AnimatedAlign - 对齐位置动画
AnimatedAlign(
alignment: _alignment,
duration: Duration(seconds: 1),
child: YourWidget(),
)
// AnimatedPositioned - 定位动画(注意:只能在Stack中使用)
AnimatedPositioned(
left: _left,
top: _top,
duration: Duration(seconds: 1),
child: YourWidget(),
)
2.3 隐式动画的工作原理
很多人会有疑问:为什么改变属性值就能产生动画?下面我们深入探讨其背后的实现原理:
sequenceDiagram
participant U as 用户交互
participant S as setState()
participant W as 隐式动画Widget
participant E as 动画引擎
participant R as 渲染层
U->>S: 触发状态变化
S->>W: 重建Widget树
W->>E: 检测到属性变化
E->>E: 计算动画补间值
loop 每一帧
E->>W: 更新动画值
W->>R: 重绘界面
end
E->>W: 动画完成
下面来看一段代码,来辅助理解隐式动画:
// 隐式动画的内部逻辑
class AnimatedContainer extends ImplicitlyAnimatedWidget {
@override
void didUpdateWidget(AnimatedContainer oldWidget) {
super.didUpdateWidget(oldWidget);
// 当属性发生变化时
if (oldWidget.width != widget.width) {
// 创建动画控制器
// 创建补间动画
// 启动动画
}
}
}
简单来说,当setState()被调用时:
- Widget树重建
-
AnimatedContainer检测到属性值变化 - 自动创建动画控制器和补间动画
- 启动动画,在指定时间内平滑过渡到新值
三、补间动画
3.1 什么是补间动画?
"补间"这个词来源于动画制作领域,意思是在起始状态和结束状态之间过渡帧。
在Flutter中,Tween就是做这个工作的:
- 你告诉它起始值(begin)和结束值(end)
- 它负责计算中间的所有值
// 创建一个从50到200的尺寸补间动画
Tween<double> sizeTween = Tween<double>(
begin: 50.0, // 开始值
end: 200.0, // 结束值
);
// 创建一个从红色到蓝色的颜色补间动画
ColorTween colorTween = ColorTween(
begin: Colors.red,
end: Colors.blue,
);
3.2 Tween的完整使用流程
class TweenAnimationExample extends StatefulWidget {
@override
_TweenAnimationExampleState createState() => _TweenAnimationExampleState();
}
class _TweenAnimationExampleState extends State<TweenAnimationExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<Color?> _colorAnimation;
late Animation<double> _rotationAnimation;
@override
void initState() {
super.initState();
// 1. 创建动画控制器
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
// 2. 创建补间动画
_sizeAnimation = Tween<double>(
begin: 50.0,
end: 200.0,
).animate(_controller);
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.red,
).animate(_controller);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * 3.14159, // 2π弧度 = 360度
).animate(_controller);
// 3. 启动动画
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
color: _colorAnimation.value,
child: Center(
child: Text(
'动画中...',
style: TextStyle(color: Colors.white),
),
),
),
);
},
);
}
}
3.3 Tween成员
Flutter提供了多种专门的Tween类:
// 尺寸
SizeTween(
begin: Size(50, 50),
end: Size(200, 200),
)
// 矩形区域
RectTween(
begin: Rect.fromLTRB(0, 0, 50, 50),
end: Rect.fromLTRB(0, 0, 200, 200),
)
// 整数
IntTween(
begin: 0,
end: 100,
)
// 步进
StepTween(
begin: 0,
end: 100,
)
// 可以为自定义类型创建补间动画
Tween<YourCustomType>(
begin: CustomType(),
end: CustomType(),
)
四、动画曲线与控制器
4.1 动画曲线 - Curves
动画曲线本质就是模拟现实生活中的自然运动规律。
// 使用不同的动画曲线
Animation<double> animation = CurvedAnimation(
parent: controller,
curve: Curves.easeInOut, // 先加速再减速
reverseCurve: Curves.easeIn, // 反向动画
);
常用的动画曲线:
// 线性
Curves.linear
// 曲线
Curves.easeIn // 先慢后快
Curves.easeOut // 先快后慢
Curves.easeInOut // 先慢,中间快,后慢
// 弹性效果
Curves.bounceOut // 回弹效果
Curves.elasticOut // 弹簧效果
// 回弹效果
Curves.decelerate // 先快后慢
4.2 不同曲线效果
下面创建一个不同曲线的例子:
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.bounceOut,
Curves.elasticOut,
];
final List<String> _curveNames = [
'linear',
'easeIn',
'easeOut',
'easeInOut',
'bounceOut',
'elasticOut',
];
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true); // 循环
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: _curves.length,
itemBuilder: (context, index) {
return Padding(
padding: EdgeInsets.all(8.0),
child: Column(
children: [
Text(_curveNames[index]),
SizedBox(height: 10),
Container(
height: 50,
child: AnimatedBuilder(
animation: _controller,
builder: (context, child) {
final animation = CurvedAnimation(
parent: _controller,
curve: _curves[index],
);
return Container(
width: 50 + animation.value * 200, // 宽度从50到250
color: Colors.blue,
child: Center(
child: Text('${(animation.value * 100).round()}%'),
),
);
},
),
),
],
),
);
},
);
}
}
五、AnimationController
5.1 基本操作
AnimationController:控制动画的整个生命周期。
class AnimationControllerExample extends StatefulWidget {
@override
_AnimationControllerExampleState createState() => _AnimationControllerExampleState();
}
class _AnimationControllerExampleState extends State<AnimationControllerExample>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 2),
vsync: this,
);
_animation = Tween<double>(begin: 0, end: 300).animate(_controller);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Container(
height: _animation.value,
width: _animation.value,
color: Colors.blue,
),
SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _controller.forward, // 正向
child: Text('播放'),
),
ElevatedButton(
onPressed: _controller.reverse, // 反向
child: Text('倒放'),
),
ElevatedButton(
onPressed: _controller.stop, // 暂停
child: Text('暂停'),
),
ElevatedButton(
onPressed: () {
_controller.reset(); // 重置
_controller.forward();
},
child: Text('重置并播放'),
),
],
),
SizedBox(height: 20),
Text('当前值: ${_animation.value.toStringAsFixed(1)}'),
Text('当前状态: ${_controller.status.toString()}'),
],
);
}
}
5.2 使用技巧
// 动画控制的高级方法
// 1. 从特定值开始
_controller.animateTo(0.5); // 从当前值动画到0.5
// 2. 相对动画
_controller.forward(from: 0.0); // 从0.0开始正向动画
// 3. 重复动画
_controller.repeat( // 无限重复
min: 0.2, // 最小值
max: 0.8, // 最大值
reverse: true, // 往返执行
period: Duration(seconds: 1), // 循环周期
);
// 4. 获取动画信息
print('是否动画中: ${_controller.isAnimating}');
print('是否完成: ${_controller.isCompleted}');
print('是否停止: ${_controller.isDismissed}');
六、自定义显式动画与性能优化
6.1 自定义显式动画
有时候隐式动画不能满足我们的需求,这时候就需要自定义显式动画。
class CustomExplicitAnimation extends StatefulWidget {
@override
_CustomExplicitAnimationState createState() => _CustomExplicitAnimationState();
}
class _CustomExplicitAnimationState extends State<CustomExplicitAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _sizeAnimation;
late Animation<double> _opacityAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 3),
vsync: this,
);
// 创建交错动画效果
_sizeAnimation = Tween<double>(
begin: 50.0,
end: 200.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.0, 0.6, curve: Curves.easeInOut), // 只在前60%时间执行
));
_opacityAnimation = Tween<double>(
begin: 0.3,
end: 1.0,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.2, 0.8, curve: Curves.easeIn), // 从20%到80%时间执行
));
_colorAnimation = ColorTween(
begin: Colors.blue,
end: Colors.purple,
).animate(CurvedAnimation(
parent: _controller,
curve: Interval(0.5, 1.0, curve: Curves.easeOut), // 从50%时间开始执行
));
_controller.forward();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Opacity(
opacity: _opacityAnimation.value,
child: Container(
width: _sizeAnimation.value,
height: _sizeAnimation.value,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.3),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: Center(
child: Text(
'自定义动画',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
),
);
},
);
}
}
二者之间有什么区别呢?隐式动画 vs 显式动画
| 维度 | 隐式动画 | 显式动画 |
|---|---|---|
| 控制 | 自动控制 | 完全控制 |
| 代码量 | 简单 | 相对复杂 |
| 灵活性 | 有限 | 非常高 |
| 适用场景 | 简单属性变化 | 复杂动画序列 |
6.2 性能优化
动画性能很重要,不好的动画效果会让用户觉得应用卡顿。这里有几个优化技巧:
6.2.1 使用AnimatedBuilder局部重建
// 不推荐:整个页面重建
@override
Widget build(BuildContext context) {
return Scaffold(
body: Opacity(
opacity: _animation.value,
child: TestWidget(), // 组件被重复重建
),
);
}
// 推荐:只重建动画部分
@override
Widget build(BuildContext context) {
return Scaffold(
body: AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Opacity(
opacity: _animation.value,
child: child, // 使用缓存的child
);
},
child: TestWidget(), // 组件只构建一次
),
);
}
6.2.2 使用Transform代替布局属性
// 不推荐:改变尺寸会触发布局重新计算
Container(
width: _animation.value,
height: _animation.value,
)
// 推荐:Transform只影响绘制,不触发布局
Transform.scale(
scale: _animation.value,
child: Container(
width: 100, // 固定尺寸
height: 100,
),
)
6.2.3 避免在动画中使用Opacity
// 不推荐:Opacity会导致整个子树重绘
Opacity(
opacity: _animation.value,
child: TestComplexWidget(),
)
// 推荐:使用颜色透明度
Container(
color: Colors.blue.withOpacity(_animation.value),
child: TestComplexWidget(),
)
// 推荐:使用FadeTransition
FadeTransition(
opacity: _animation,
child: TestComplexWidget(),
)
6.3 创建一个加载动画
下面用一个加载动画效果串一下上面所讲内容:
class SmoothLoadingAnimation extends StatefulWidget {
@override
_SmoothLoadingAnimationState createState() => _SmoothLoadingAnimationState();
}
class _SmoothLoadingAnimationState extends State<SmoothLoadingAnimation>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _rotationAnimation;
late Animation<double> _scaleAnimation;
late Animation<Color?> _colorAnimation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(milliseconds: 1500),
vsync: this,
)..repeat(reverse: true);
_rotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * 3.14159,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_scaleAnimation = Tween<double>(
begin: 0.8,
end: 1.2,
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
_colorAnimation = ColorTween(
begin: Colors.blue[300],
end: Colors.blue[700],
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: Transform.scale(
scale: _scaleAnimation.value,
child: Container(
width: 60,
height: 60,
decoration: BoxDecoration(
color: _colorAnimation.value,
borderRadius: BorderRadius.circular(30),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 10,
offset: Offset(0, 5),
),
],
),
child: Icon(
Icons.refresh,
color: Colors.white,
size: 30,
),
),
),
);
},
);
}
}
总结
核心知识点
- 理解了Animation、AnimationController、Tween、Curve的作用
- 掌握了动画状态的生命周期管理
- 理解了隐式动画的自动管理机制
- 学会了创建各种类型的补间动画
- 学会了使用Curves让动画更自然
- 学会了创建复杂的自定义动画
- 学会了使用AnimatedBuilder优化性能
一个好的应用整体动画效果风格要一致,同时要保持流畅度,增强用户体验。通过本节学习,相信大家已经掌握了Flutter动画的核心概念和实战技巧。多动手实现各种动画效果,不断调试优化吧~~~ 有任何问题欢迎在评论区留言,看到会第一时间回复!