1.前言
1.1:Flutter动画中:
首先要看的是Flutter中动画的几个类之间的关系:
主角当然是我们的Animation类了,它可以借助Animatable进行强化
Animatable通过animate函数接收一个Animation对象,再返回Animation对象,这不就是包装吗?
通过Animation对象回调即可获取规律变画的值,进行渲染。这是动画的基本。
1.2:Animation和Animation体系一览
整个Flutter的Animation相比Android还是比较简单的
1.3:介绍今天的主角nStarPath
我们通过变动这个函数中的参数让路径动态变化实现动画
/// 可以创建一个外接圆半径为[R],内接圆半径半径为[r]的[num]角星路径
Path nStarPath(int num, double R, double r) {
Path path = new Path();
double perDeg = 360 / num;
double degA = perDeg/2/2;
double degB = (360 / (num - 1) - degA) / 2 + degA;
path.moveTo(cos(_rad(degA)) * R, (-sin(_rad(degA)) * R));
for (int i = 0; i < num; i++) {
path.lineTo(
cos(_rad(degA + perDeg * i)) * R, -sin(_rad(degA + perDeg * i)) * R);
path.lineTo(
cos(_rad(degB + perDeg * i)) * r, -sin(_rad(degB + perDeg * i)) * r);
}
path.close();
return path;
}
double _rad(double deg) {
return deg * pi / 180;
}
复制代码
1.4:动画舞台的搭建
对于动画的演示,最好的当然是绘制了,绘制中最好的当然是我的五角星了
感觉创建StatefulWidget的代码开始时基本一致,写了一篇模板解析器
玩转字符串篇--Gradle+代码生成器=懒人必备
import 'package:flutter/material.dart';
class AnimPage extends StatefulWidget {
@override
_AnimPageState createState() => _AnimPageState();
}
class _AnimPageState extends State<AnimPage>{
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter之旅"),
),
body: CustomPaint(
painter: AnimView(),
),
floatingActionButton: FloatingActionButton(
onPressed: () {
//TODO 执行动画
},
child: Icon(Icons.add),
),
);
}
}
class AnimView extends CustomPainter {
Paint mPaint;
Paint gridPaint;
AnimView() {
mPaint = new Paint();
gridPaint = Paint()
..style = PaintingStyle.stroke
..color = Colors.cyanAccent;
mPaint.color = Colors.deepOrange;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawPath(gridPath(area: Size(500, 1000)), gridPaint);//绘制网格
canvas.translate(200, 200);
canvas.drawPath(nStarPath(5, 100, 50), mPaint);//绘制五角星
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
/// 可以创建一个外接圆半径为[R],内接圆半径半径为[r]的[num]角星,
Path nStarPath(int num, double R, double r) {
Path path = new Path();
double perDeg = 360 / num;
double degA = perDeg/2/2;
double degB = (360 / (num - 1) - degA) / 2 + degA;
path.moveTo(cos(_rad(degA)) * R, (-sin(_rad(degA)) * R));
for (int i = 0; i < num; i++) {
path.lineTo(
cos(_rad(degA + perDeg * i)) * R, -sin(_rad(degA + perDeg * i)) * R);
path.lineTo(
cos(_rad(degB + perDeg * i)) * r, -sin(_rad(degB + perDeg * i)) * r);
}
path.close();
return path;
}
double _rad(double deg) {
return deg * pi / 180;
}
///创建一个区域是[area],小格边长为[step]的网格的路径
Path gridPath({double step = 20, Size area}) {
Path path = Path();
for (int i = 0; i < area.height / step + 1; i++) {
//画横线
path.moveTo(0, step * i); //移动画笔
path.lineTo(area.width, step * i); //画直线
}
for (int i = 0; i < area.width / step + 1; i++) {
//画纵线
path.moveTo(step * i, 0);
path.lineTo(step * i, area.height);
}
return path;
}
复制代码
好了,现在开始Flutter的动画之旅
2.Flutter动画基本使用
这里再贴一下这张Animation使用图:
2.1:动画的基本使用:Tween+AnimationController
1.让_AnimPageState类with一下SingleTickerProviderStateMixin
2.使用创建一个AnimationController对象(Animation族)
3.复写SingleTickerProviderStateMixin的dispose方法释放AnimationController对象
4.创建Tween对象(Animatable族)并调用animate方法,生成新的Animation对象
5.监听Animation的变化,获取每次刷新时的值。
class _AnimPageState extends State<AnimPage> with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
@override
void initState() {
super.initState();
controller = AnimationController(////创建 Animation对象
duration: const Duration(milliseconds: 2000), //时长
vsync: this);
var tween = Tween(begin: 25.0, end: 150.0); //创建从25到150变化的Animatable对象
animation = tween.animate(controller); //执行animate方法,生成
animation.addListener(() {
print(animation.value);
});
}
@override
void dispose() {
super.dispose();
controller.dispose(); // 资源释放
}
@override
Widget build(BuildContext context) {
return Scaffold(
//略同...
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.forward(); //执行动画
},
//略同...
);
}
}
复制代码
注:有时候为了方便可以连写,关于
SingleTickerProviderStateMixin
这里不做深究,
但要知道,既然是mixin就是给类附加能力的,其中之一便是dispose()
方法
animation = Tween(begin: 25.0, end: 150.0).animate(controller)
..addListener(() {
print(animation.value);
});
复制代码
看一下控制台打印结果:从25~150变化的一群数字
---->[控制台打印]----
I/flutter ( 9073): 25.0
I/flutter ( 9073): 26.1205625
I/flutter ( 9073): 27.2418125
I/flutter ( 9073): 28.363125
出处略去n行....
I/flutter ( 9073): 147.20725
I/flutter ( 9073): 148.3288125
I/flutter ( 9073): 149.4503125
I/flutter ( 9073): 150.0
复制代码
2.2:热身运动,看一下Tween下点的轨迹
也是突发奇想,数字在不断变化,这可都是白花花的资源啊,要不秀一个
这个小例子完美的阐述了Tween补间的动画是匀速的
class AnimPage extends StatefulWidget {
@override
_AnimPageState createState() => _AnimPageState();
}
class _AnimPageState extends State<AnimPage>
with SingleTickerProviderStateMixin {
AnimationController controller;
Animation<double> animation;
List<Offset> _points=[];//点集
@override
void initState() {
super.initState();
controller = AnimationController(//创建 Animation对象
duration: const Duration(milliseconds: 2000), //时长
vsync: this);
var tween = Tween(begin: 25.0, end: 150.0); //创建从25到150变化的Animatable对象
animation = tween.animate(controller); //执行animate方法,生成
animation.addListener(() {
render(_points,animation.value);
});
}
@override
void dispose() {
super.dispose();
controller.dispose(); // 资源释放
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Flutter之旅"),
),
body: CustomPaint(
painter: AnimView(_points),//入参
),
floatingActionButton: FloatingActionButton(
onPressed: () {
controller.forward(); //执行动画
},
tooltip: 'Increment',
child: Icon(Icons.add),
),
);
}
double x=0;
//核心渲染方法,将值加入集合中并渲染
void render(List<Offset> _points, double value) {
_points.add(Offset(x, -value));
x++;
setState(() {//更新组件
});
}
}
class AnimView extends CustomPainter {
List<Offset> _points;
Paint mPaint;
Paint gridPaint;
AnimView(this._points) {
mPaint = new Paint();
gridPaint = Paint()
..style = PaintingStyle.stroke
..color = Colors.cyanAccent;
mPaint..color = Colors.deepOrange..strokeWidth=3;
}
@override
void paint(Canvas canvas, Size size) {
canvas.drawPath(gridPath(area: Size(500, 1000)), gridPaint);
canvas.translate(200,200);
canvas.drawCircle(Offset(0, 0), 2.5, gridPaint..color=Colors.black..style=PaintingStyle.fill);
_drawStar(canvas,_points);
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
void _drawStar(Canvas canvas, List<Offset> pos) {
canvas.drawPoints(PointMode.lines, pos, mPaint);
}
}
///创建一个区域是[area],小格边长为[step]的网格的路径
Path gridPath({double step = 20, Size area}) {
Path path = Path();
for (int i = 0; i < area.height / step + 1; i++) {
//画横线
path.moveTo(0, step * i); //移动画笔
path.lineTo(area.width, step * i); //画直线
}
for (int i = 0; i < area.width / step + 1; i++) {
//画纵线
path.moveTo(step * i, 0);
path.lineTo(step * i, area.height);
}
return path;
}
复制代码
2.3:创建星星的描述类和绘制
三个属性,外接圆半径,内接圆半径和角数
class Star{
int num;
double R;
double r;
Star(this.num,this.R,this.r);
}
---->[AnimView类]----
class AnimView extends CustomPainter {
Star _star;
AnimView(this._star) {
//略同...
}
@override
void paint(Canvas canvas, Size size) {
//略同...
_drawStar(canvas,_star);
}
//绘制星星
void _drawStar(Canvas canvas, Star star) {
canvas.drawPath(nStarPath(star.num, star.R, star.r), mPaint);
}
}
---->[_AnimPageState类]----
class _AnimPageState extends State<AnimPage>
with SingleTickerProviderStateMixin {
Star _star;
@override
void initState() {
_star=Star(5, 100, 50);
//略同...
}
@override
Widget build(BuildContext context) {
//略同...
body: CustomPaint(
painter: AnimView(_star),
复制代码
2.3:动态更新
只需要在刷新的时候更改五角星的属性就行了,下面就是外接圆半径25~150变化
animation.addListener(() {
render(_star,animation.value);
}
//核心渲染方法
void render(Star star, double value) {
star.R=value;
setState(() {//更新组件
});
}
复制代码
2.4:int数据的动画:IntTween
Tween是两个double类型的数字在一定的时间内的均匀变化
那int该肿么办?Tween之下有二十来个孩子用于不同的对象变化
其一便是IntTween
,这里让星星的角数从5~100不断变化形成动画
class _AnimPageState extends State<AnimPage>
with SingleTickerProviderStateMixin {
Animation<int> animation;//改成int泛型
//略同...
@override
void initState() {
//略同...
var intTween = IntTween(begin: 5, end: 100);
//略同...
}
//核心渲染方法
void render(Star star, int value) {
star.num=value;
setState(() {//更新组件
});
}
}
复制代码
实现起来还是比较简单的
2.5:颜色变化: ColorTween
顾名思义,匀速改变颜色呗,思路是一致的,这里先给Star描述类价格color字段
在Canvas绘制时使用Satr的颜色,这样在刷新时就会呈现颜色渐变
class Star{
//略同...
Color color;
Star(this.num,this.R,this.r,this.color);
}
class _AnimPageState extends State<AnimPage>
with SingleTickerProviderStateMixin {
Animation<Color> animation;
//略同...
@override
void initState() {
_star=Star(5, 100, 60,Colors.red);
//略同...
var colorTween = ColorTween(begin: Colors.red, end: Colors.yellow);
//创建从红到黄变化的Animatable对象
}
//核心渲染方法
void render(Star star, Color value) {
star.color=value;
setState(() {//更新组件
});
}
}
---->[AnimView:绘制时使用颜色]----
void _drawStar(Canvas canvas, Star star) {
canvas.drawPath(nStarPath(star.num, star.R, star.r), mPaint..color=star.color);
}
复制代码
3.让动画更有动感:CurveTween
看名字是曲线补间,也就是运动不再是匀速的,可以自己设计。
3.1:看一下CurveTween的源码
需要一个curve属性,对应的是Curve对象。
Curve为抽象类,有一个四入参的子类Cubic,去吧,皮卡丘就决定是你了。
---->[CurveTween]----
class CurveTween extends Animatable<double> {
CurveTween({ @required this.curve })
: assert(curve != null);
Curve curve;
---->[Curve]----
@immutable
abstract class Curve {
---->[Curve]----
class Cubic extends Curve {
const Cubic(this.a, this.b, this.c, this.d)
复制代码
3.2:关于曲线参数的获取
记得掘金的头像可以转,Chrome浏览器里有个小功能,在调试面板里
看来一下有个lazy的样式下的translation,点开可以调试曲线,获取四个值
用刚才的画点方法看了一下数据的变动情况
3.3:代码操作
根据包装设设计模式的思想,CurveTween可以强化Animation拥有从0~1的曲线,
然后再送到Tween中进行补间,让其在两个数的范围内具有曲线补间能力
controller = AnimationController(//创建 Animation对象
duration: const Duration(milliseconds: 2000), //时长
vsync: this);
var curveTween = CurveTween(curve:Cubic(0.96, 0.13, 0.1, 1.2));//创建curveTween
var tween=Tween(begin: 50.0, end: 100.0);
animation = tween.animate(curveTween.animate(controller));
animation.addListener(() {
render(_star,animation.value);
});
复制代码
另外,Curves中也定义了41个常用的Curve,来方便使用,大家可以试试
4.动画的监听和动画序列
4.1:运动状态:AnimationStatus
相像一下,一个百米跑道标注着刻度,哨声一响,你开始跑
enum AnimationStatus {
dismissed,//在正在开始时停止了?跌倒在起跑线上
forward,//运动中
reverse,//跑到终点,再跑回来的时候
completed,//跑到终点时
}
复制代码
4.2:为Animation添加监听
通过
Animation#addStatusListener
可以回调AnimationStatus
对象
animation.addStatusListener((status){
switch(status){
case AnimationStatus.completed:
controller.reverse();//反向
break;
case AnimationStatus.forward:
break;
case AnimationStatus.reverse:
_star.color=randomRGB();
break;
case AnimationStatus.dismissed:
controller.forward();
break;
}
});
复制代码
4.3:最后说一下序列动画
找了好一会都没有发现多值的api,只有start和end两个值
然后翻译一下源码,看到还有个TweenSequence,顾名思义,序列动画
现在重新写个组件叫FlutterText,拥有颤动效果的文字
class FlutterText extends StatefulWidget {
var str;
var style;
FlutterText(this.str, this.style);
_FlutterTextState createState() => _FlutterTextState();
}
class _FlutterTextState extends State<FlutterText>
with SingleTickerProviderStateMixin {
Animation<double> animation;
AnimationController controller;
initState() {
super.initState();
controller = AnimationController(
duration: const Duration(milliseconds: 1000), vsync: this);
animation = TweenSequence<double>([//使用TweenSequence进行多组补间动画
TweenSequenceItem<double>(tween: Tween(begin: 0, end: 15), weight: 1),
TweenSequenceItem<double>(tween: Tween(begin: 15, end: 0), weight: 2),
TweenSequenceItem<double>(tween: Tween(begin: 0, end: -15), weight: 3),
TweenSequenceItem<double>(tween: Tween(begin: -15, end: 0), weight: 4),
]).animate(controller)
..addListener(() {
setState(() {});
})
..addStatusListener((s) {
if (s == AnimationStatus.completed) {
setState(() {});
}
});
controller.forward();
}
Widget build(BuildContext context) {
var result = Transform(
transform: Matrix4.rotationZ(animation.value * pi / 180),
alignment: Alignment.center,
child: Text(
widget.str,
style: widget.style,
),
);
return result;
}
dispose() {
controller.dispose();
super.dispose();
}
}
复制代码
这样,Animation基本用法就说完了,还有几个类就不一一介绍了,基本使用都差不多
关于动画效果,是一个永远也无法满足的深渊,它无法言尽。
一张经典的画作重要的不是画笔,而是握笔的人,你的动画属于你。
结语
本文到此接近尾声了,如果想快速尝鲜Flutter,《Flutter七日》会是你的必备佳品;如果想细细探究它,那就跟随我的脚步,完成一次Flutter之旅。
另外本人有一个Flutter微信交流群,欢迎小伙伴加入,共同探讨Flutter的问题,本人微信号:zdl1994328
,期待与你的交流与切磋。
本文所有源码见
github/flutter_journey