动画在我们日常开发中其实使用的不是特别频繁,大部分应用主要还是以功能为主,不会做特别复杂的动画效果,即使复杂的动画效果一般也是通过使用第三方库或者lottie动画。但是,作为一名开发者,不可能完全不了解动画。因此这里就通过一些demo对Flutter基础动画做个了解,以便在需要的时候,我们有能力能够完成一些基础动画的开发。
一、动画核心要素
Flutter 动画系统基于四个核心组件:
- Animation:动画的当前值(如 0.0~1.0)
- AnimationController:控制动画播放(时长、方向等)
- Tween:定义动画的值域范围
- CurvedAnimation:控制动画的速度曲线
作为初学者,这个UML图扫一眼就行了,不用太纠结,知道一下他们的继承关系就行。
二、基础动画实现
通过一个简单的缩放动画来了解一下动画的实现步骤。
GIF的效果显得有点卡顿,但实际上在手机上运行效果还是比较流畅的。
代码示例:
import 'package:flutter/material.dart';
void main() => runApp(const AnimatedApp());
class AnimatedApp extends StatelessWidget {
const AnimatedApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(title: const Text('Flutter 基础动画')),
body: const Center(child: ScalingBox()),
),
);
}
}
class ScalingBox extends StatefulWidget {
const ScalingBox({super.key});
@override
State<ScalingBox> createState() => _ScalingBoxState();
}
class _ScalingBoxState extends State<ScalingBox> with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
// 1. 创建动画控制器(1秒周期)
_controller = AnimationController(
duration: const Duration(seconds: 1),
vsync: this,
)..repeat(reverse: true); // 循环播放
// 2. 配置动画值域和曲线
_animation = Tween(begin: 100.0, end: 200.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut, // 使用缓动曲线
),
);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _animation,
builder: (context, child) {
return Container(
width: _animation.value,
height: _animation.value,
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(12),
);
},
);
}
@override
void dispose() {
_controller.dispose(); // 释放资源
super.dispose();
}
}
代码解析
1. 在state里创建一个controller
late AnimationController _controller;
2. 在initState里初始化Controller
// 1. 创建动画控制器(1秒周期)
_controller = AnimationController(
duration: const Duration(seconds: 1),// 设置动画时间为1秒
vsync: this,
);
_controller.repeat(reverse: true); // 设置循环。动画效果为:小->大->小->大这样一直反反复复循环
- duration:控制动画时长和播放状态
vsync: 防止后台动画消耗资源- reverse: true: 这个属性可以让动画反向执行,比如你设置的是放大的动画,则在动画执行完之后会继续缩小回去。
3. 配置动画值域和曲线
_animation = Tween(begin: 100.0, end: 200.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut, // 使用缓动曲线【开始和结束的时候比较慢,中间会快一点;场景的自然运动效果】
),
);
- 将 0.0 ~ 1.0 的进度映射到 100 ~ 200 的实际值
- Curves的动画曲线效果:开始和结束的时候比较慢,中间会快一点;场景的自然运动效果。
Curves支持的运动曲线可以查看这篇介绍[Flutter 进阶] 动画曲线(Curve)深度解析
4. 提高渲染效率
AnimatedBuilder(
animation: _animation,
builder: (context, child) {
// 仅重建动画部分
}
)
三、常见动画实现
通过前面的示例,我们可以了解到动画的基本实现方式,接下来我们再看看我们常见的一些动画是怎么实现的。
- 淡入淡出动画(Fade):
关键点:
- 通过更改透明度的方式实现显示和隐藏的效果。
- 通过设置
Curves.easeInOut使淡入淡出变得更加自然一点。
关键代码:
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_opacityAnimation = Tween(begin: 0.1, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: FadeTransition(
opacity: _opacityAnimation,
child: Container(
width: 200,
height: 200,
decoration: BoxDecoration(
color: Colors.deepPurple,
borderRadius: BorderRadius.circular(20),
),
),
),
)
);
}
2. 旋转动画:
关键点:
- 通过
Transform.rotate随着时间的变化不断设置更改旋转角度。 - 设置变化的值范围为0 ~ 2Π(约等于2 x 3.14159)之间,换个角度理解就是从0 ~ 360度的变化。
*关键代码:
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 700),
vsync: this,
)..repeat();
_rotationAnimation = Tween(
begin: 0.0,
end: 2 * 3.14159,
).animate(_controller);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: AnimatedBuilder(
animation: _rotationAnimation,
builder: (context, child) {
return Transform.rotate(
angle: _rotationAnimation.value,
child: const Icon(Icons.refresh, size: 50),
);
},
),
),
);
}
3. 平移动画:
关键点:
- 设置平移的起始位置和结束位置
- 设置
elasticOut,让小球在开始和结束时保持一点弹性 - 通过动画改变
SlideTransition的position的值达到小球平移的效果。
关键代码:
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
)..repeat(reverse: true);
_positionAnimation = Tween<Offset>(
begin: const Offset(-1.0, 0.0),
end: const Offset(1.0, 0.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.elasticOut,
));
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: SlideTransition(
position: _positionAnimation,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(50),
),
),
),
),
);
}
4. 物理动画:
物理动画与常规动画的核心区别在于:物理动画基于物理定律模拟真实世界的运动规律,而常规动画则基于预设的数学曲线。如果我们要做一些类似小球下落之后自然弹起的效果,可以通过物理动画做的更真实一点。在游戏开发里使用的应该会比较多,比如碰撞效果等。
- 设置模拟物体的三个重要参数
- 质量mass: 1 // 质量 (kg) - 影响惯性
- stiffness: 200 // 刚度 (N/m) - 影响弹性
- damping: 10 // 阻尼 (N·s/m) - 影响阻力
- 设置初始值:
final spring = SpringSimulation(
SpringDescription(
mass: 1,
stiffness: 200,
damping: 10,
),
0, // 起始位置
300, // 目标位置
1000, // 初始速度
);
这个初始值主要是用来确定小球的初始速度,有了初始速度之后就可以通过物理方法计算整个下落过程。
核心代码:
final spring = SpringSimulation(
SpringDescription(
mass: 1,
stiffness: 200,
damping: 10,
),
0, // 起始位置
300, // 目标位置
1000, // 初始速度
);
@override
void initState() {
super.initState();
_controller = AnimationController.unbounded(vsync: this);
_springAnimation = _controller.drive(
Tween(begin: 0.0, end: 1.0),
);
// _controller.animateWith(spring);
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
SizedBox(height: 200,),
ElevatedButton(onPressed: (){
_controller.animateWith(spring);
}, child: Text('开始')),
SizedBox(height: 100,),
Center(
child: AnimatedBuilder(
animation: _springAnimation,
builder: (context, child) {
return Transform.translate(
offset: Offset(0, _controller.value),
child: Container(
width: 80,
height: 80,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(10),
),
),
);
},
),
),
],
)
);
}
5. 组合动画: 这个其实就是针对同一个view设置多个动画,这些动画可能同时执行,按顺序执行,主要取决于设置的时间点是怎么样的。 其实很多看似酷炫的动画都是通过组合动画来实现的,只要把那些复杂的动画拆接下来,也就是一个个单独的简单动画。
关键解析:
- 组合动画时间片:
- 0 ~ 0.5 执行淡入淡出动画
- 0.3 ~ 1.0 执行缩放动画
- 0.5 ~ 1.0 执行颜色变化动画
也就是在0.3 ~ 0.5这段时间同时执行了淡入淡出和缩放动画;
在0.5 ~ 1.0之间,同时执行了缩放和颜色变化动画。
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(seconds: 2),
vsync: this,
);
// 定义组合时间区间
_opacity = Tween(begin: 0.0, end: 1.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.0, 0.5, curve: Curves.easeIn),
),
);
_width = Tween(begin: 50.0, end: 200.0).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.3, 1.0, curve: Curves.easeOut),
),
);
_color = ColorTween(begin: Colors.blue, end: Colors.purple).animate(
CurvedAnimation(
parent: _controller,
curve: const Interval(0.5, 1.0),
),
);
_controller.repeat(reverse: true);
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _controller,
builder: (context, child) {
return Container(
height: 100,
width: _width.value,
decoration: BoxDecoration(
color: _color.value,
borderRadius: BorderRadius.circular(8),
),
child: Opacity(
opacity: _opacity.value,
child: const Center(child: Text('交错动画', style: TextStyle(color: Colors.white))),
),
);
},
);
}
四、动画的生命周期
其实基础动画的实现还是非常简单的,而且我们还通过repeat方法可以让动画重复执行,通过reverse参数可以让动画反向执行。那整体这个循环的生命周期大概是怎么样的呢?看图
解析:
-
大黑点是开始,空心小圆点带空心圆的是结束
-
dismissed (未启动状态) :
- 动画的初始状态
- 动画值为初始值(通常是0.0)
- 动画未开始播放
-
forward (正向播放状态) :
- 动画从开始值向结束值播放
- 通过调用
forward()方法进入此状态 - 动画值从0.0 → 1.0变化
-
reverse (反向播放状态) :
- 动画从结束值向开始值播放
- 通过调用
reverse()方法进入此状态 - 动画值从1.0 → 0.0变化
-
completed (完成状态) :
- 动画播放完成的状态
- 正向播放完成后值为1.0
- 可在此状态重新开始动画
五、动画开发心得
- 使用
const修饰静态组件,避免组件的频繁刷新 - 避免在动画builder中构建复杂子树,降低动画执行时的性能消耗
- 对位移动画使用
Transform代替修改布局属性 - 使用
Opacity组件实现透明度渐变的动画
总结
Flutter 的动画系统通过分层架构实现了灵活的组合方式。核心要点:
- 使用
AnimationController管理动画生命周期 - 通过
Tween实现值域映射 - 使用
Curves控制动画节奏 - 通过
AnimatedBuilder优化重建范围
这些示例覆盖了Flutter动画开发中的常见需求,可以根据实际场景选择合适的动画实现方式。Flutter的动画系统既强大又灵活,掌握这些基础模式后,可以创造出丰富多样的动效体验。