Flutter】动画学习(二) Animation
文章目录
介绍
在任何系统的UI框架中,动画实现的原理都是相同的,即:在一段时间内,快速地多次改变UI外观,由于人眼会产生视觉暂留,最终看到的就是一个“连续”的动画,这和电影的原理是一样的,而UI的一次改变称为一个动画帧,对应一次屏幕刷新,而决定动画流畅度的一个重要指标就是帧率FPS(Frame Per Second),指每秒的动画帧数。很明显,帧率越高则动画就会越流畅。
一般情况下,对于人眼来说,动画帧率超过16FPS,就比较流畅了,超过32FPS就会非常的细腻平滑,而超过32FPS基本就感受不到差别了。由于动画的每一帧都是要改变UI输出,所以在一个时间段内连续的改变UI输出是比较耗资源的,对设备的软硬件系统要求都较高,所以在UI系统中,动画的平均帧率是重要的性能指标,而在Flutter中,理想情况下是可以实现60FPS的,这和原生应用动画基本是持平的。
为了方便开发者创建动画,不同的UI系统对动画都进行了一些抽象,比如在Android中可以通过XML来描述一个动画然后设置给View。Flutter中也对动画进行了抽象,主要涉及Tween、Animation、Curve、Controller这些角色。
基础动画
Animation对象是Flutter动画库中的一个核心类,它生成指导动画的值。
Flutter 提供了两种动画,分别是 Tween Animation 补间动画和 Physics-based Animation 基于物理动画;
Animation
Animation可以生成动画过程中的值,生成的值并非单一的 double 也可以是 Size/Color 等;Animation 可以获取状态但无法获取屏幕显示内容。
AnimationController
AnimationController 小菜理解为 Animation 控制器,实际也是一个特殊的 Animation,在屏幕刷新时会生成一个新的值;使用时需要传递 vsync 值,用来防止屏幕外动画,vsync 值可以继承 TickerProviderStateMixin,若当前页面只有一个 controller 也可以用 SingleTickerProviderStateMixin;
AnimationController 有两个常用方法:
- forward() 方法用来开始动画,即从无到有;
- reverse() 方法用来反向开始动画,即从有到无;
TweenAnimation
Tween 动画是无状态的,只是在固定时间内均匀生成 begin 和 end 的值,通过 animation.value 来获取;
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 300), vsync: this);
Animation<double> animation =
Tween(begin: 0.0, end: 1.0).animate(controller);
animation.addListener(() {
setState(() {});
});
CurvedAnimation
CurvedAnimation 的动画过程是非线性的,curve 种类很多,比较符合日常生活的物理过程,例如先快后慢或先增长到一个峰值再降低等;curve 的动画过程也可以自定义函数等;
AnimationController controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
CurvedAnimation curve =
CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn);
AnimatedWidget
Flutter 很贴心的提供了自带动画属性的 Widget 极大的方便我们使用简单的动画,涵盖 透明度/旋转/缩放/平移 等常用的动画属性,使用时非常方便;但是缺点也相对明显,这些 Widget 属性相对专一,若需要多种动画属性不太适合;
简单介绍几个小菜日常使用的动画组件;
FadeTransition 显隐性
FadeTransition(opacity: animation, child: FlutterLogo(size: 100.0))
ScaleTransition 缩放
ScaleTransition(scale: animation, child: FlutterLogo(size: 100.0))
RotationTransition 旋转
RotationTransition(turns: animation, child: FlutterLogo(size: 100.0))
Transform.scale 缩放
Transform.scale(scale: curve.value, child: FlutterLogo(size: 100.0))
Transform.rotate 旋转
Transform.rotate(angle: curve.value * pi, child: FlutterLogo(size: 100.0))
Transform.translate 平移
Transform.translate(offset: Offset(100.0 * curve.value, 0.0), child: FlutterLogo(size: 100.0))
AnimatedOpacity 透明度
AnimatedOpacity(opacity: animation.value, duration: Duration(milliseconds: 2000), child: FlutterLogo(size: 100.0))
核心代码
class _AnimationPageState extends State<AnimationPage03> with TickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
CurvedAnimation curve;
bool isForward = false;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
curve = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn);
animation = Tween(begin: 0.0, end: 1.0).animate(controller);
animation.addListener(() {
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return new Scaffold(
appBar: AppBar(
title: Text('Animation Demo'),
),
body: Stack(children: <Widget>[
ListView(children: <Widget>[
Padding(
padding: EdgeInsets.all(5.0),
child: Row(children: <Widget>[
Expanded(
flex: 1, child: Center(child: Text('FadeTransition'))),
Expanded(
flex: 1, child: Center(child: Text('ScaleTransition')))
])),
Row(children: <Widget>[
Expanded(
flex: 1,
child: FadeTransition(
opacity: animation, child: FlutterLogo(size: 100.0))),
Expanded(
flex: 1,
child: ScaleTransition(
scale: animation, child: FlutterLogo(size: 100.0)))
]),
Padding(
padding: EdgeInsets.all(5.0),
child: Row(children: <Widget>[
Expanded(
flex: 1,
child: Center(child: Text('RotationTransition'))),
Expanded(
flex: 1, child: Center(child: Text('AnimatedOpacity')))
])),
Row(children: <Widget>[
Expanded(
flex: 1,
child: RotationTransition(
turns: animation, child: FlutterLogo(size: 100.0))),
Expanded(
flex: 1,
child: AnimatedOpacity(
opacity: animation.value,
duration: Duration(milliseconds: 2000),
child: FlutterLogo(size: 100.0)))
]),
Padding(
padding: EdgeInsets.all(5.0),
child: Row(children: <Widget>[
Expanded(
flex: 1,
child: Center(child: Text('Transform.translate'))),
Expanded(
flex: 1, child: Center(child: Text('Transform.rotate')))
])),
Row(children: <Widget>[
Expanded(
flex: 1,
child: Transform.translate(
offset: Offset(100.0 * curve.value, 0.0),
child: FlutterLogo(size: 100.0))),
Expanded(
flex: 1,
child: Transform.rotate(
angle: curve.value * pi, child: FlutterLogo(size: 100.0)))
]),
Padding(
padding: EdgeInsets.all(5.0),
child: Row(children: <Widget>[
Expanded(
flex: 1, child: Center(child: Text('Transform.scale'))),
Expanded(flex: 1, child: Center(child: Text('Position')))
])),
Row(children: <Widget>[
Expanded(
flex: 1,
child: Transform.scale(
scale: curve.value, child: FlutterLogo(size: 100.0))),
])
]),
posWid()
]),
floatingActionButton: new FloatingActionButton(
tooltip: 'Animation',
child: new Icon(Icons.lightbulb_outline),
onPressed: () {
isForward ? controller.reverse() : controller.forward();
isForward = !isForward;
}));
}
Widget posWid() {
return Positioned(
bottom: 16 + 314 * animation.value,
right: 16 + 84 * animation.value,
child: Container(child: FlutterLogo(size: 100.0)));
}
}
复合动画
隐性和缩放
- addStatusListener 用来监听当前动画状态,即开始或结束;
- addListener 用来坚挺动画过程,可获取实时 value 值;
AnimationController controller;
Animation<double> animation, sizeAnim;
@override
void initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween(begin: 0.0, end: 1.0).animate(controller);
sizeAnim = Tween(begin: 0.0, end: 180.0).animate(controller);
animation.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
sizeAnim.addStatusListener((status) {
if (status == AnimationStatus.completed) {
controller.reverse();
} else if (status == AnimationStatus.dismissed) {
controller.forward();
}
});
}
Widget bodyWid() {
return Center(
child: Opacity(
opacity: animation.value,
child: FlutterLogo(size: sizeAnim.value)));
}
时间段动画
既然可以监听动画过程和动画状态,整体的动画便可以灵活掌握;小菜接下来尝试一下分时间段动画,前 50% 显隐性处理,后 50% 缩放处理;
AnimationController controller;
Animation<double> animation, sizeAnim;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: controller, curve: Interval(0.0, 0.5, curve: Curves.ease)));
sizeAnim = Tween<double>(
begin: 100.0,
end: 180.0,
).animate(CurvedAnimation(
parent: controller,
curve: Interval(0.5, 1.0, curve: Curves.fastOutSlowIn)));
}
Widget bodyWid() {
return Center(
child: Opacity(
opacity: animation.value,
child: FlutterLogo(size: sizeAnim.value)));
}
自定义动画
动画是灵活的,我们可以根据自己的需求自定义动画效果,小菜尝试一个圆环绕一个圆转圈;
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
controller = AnimationController(duration: const Duration(milliseconds: 2000), vsync: this);
animation = Tween(begin: -1.0, end: 1.0).animate(controller);
}
class AnimationCanvas extends CustomPainter {
Animation<double> animation;
AnimationCanvas(this.animation);
Paint _paint = Paint()
..color = Colors.blue
..strokeWidth = 4.0
..style = PaintingStyle.stroke;
@override
void paint(Canvas canvas, Size size) {
canvas.save();
canvas.drawCircle(Offset(300, 300.0), 150.0, _paint);
canvas.restore();
canvas.save();
canvas.translate(150 * sin(pi * animation.value), 150 * cos(pi * animation.value));
canvas.drawCircle(Offset(300, 300.0), 10.0, _paint..color = Colors.red);
canvas.restore();
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
Hero 动画
Hero 动画是 Flutter 提供的飞入式动画,主要用在页面间切换时动画,且返回时动画按原路返回;使用时设置两个页面间 tag 一致即可,方便简洁;
Widget bodyWid04() {
return Container(
child: Padding(
padding: EdgeInsets.all(10.0),
child: GestureDetector(
child: Row(children: <Widget>[
Hero(tag: 'user_header', child: FlutterLogo(size: 50.0)),
Padding(
padding: EdgeInsets.only(left: 12.0),
child: Text('Flutter Ptoto'))
]),
onTap: () {
Navigator.pushNamed(context, 'animateRoute01');
})));
}
推荐阅读
-
Android开发之图形图像与动画(二)Animation实现图像的渐变/缩放/位移/旋转
-
Android开发之图形图像与动画(二)Animation实现图像的渐变/缩放/位移/旋转
-
Flutter学习(二)-FlutterGo学习
-
《Flutter 动画系列二》Google工程师带你选择Flutter动画控件
-
Android基础夯实--重温动画(二)之Frame Animation
-
安卓开发之Animation学习(帧、补间、属性动画)
-
Flutter学习总结(二、初识Dart)
-
Android Animation动画详解(二): 组合动画特效_html/css_WEB-ITnose
-
Flutter学习之路(二)-Dart介绍 环境搭建
-
自定义View时,用到Paint Canvas的一些温故,简单的View Animation(动画二,“大大姐”的简单变动)_html/css_WEB-ITnose