Flutter 动画之 Animation

1.前言

1.1:Flutter动画中:

首先要看的是Flutter中动画的几个类之间的关系:

image

主角当然是我们的Animation类了,它可以借助Animatable进行强化
Animatable通过animate函数接收一个Animation对象,再返回Animation对象,这不就是包装吗?
通过Animation对象回调即可获取规律变画的值,进行渲染。这是动画的基本。


1.2:Animation和Animation体系一览

整个Flutter的Animation相比Android还是比较简单的

image

1.3:介绍今天的主角nStarPath

我们通过变动这个函数中的参数让路径动态变化实现动画

image
/// 可以创建一个外接圆半径为[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+代码生成器=懒人必备

image
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使用图:

image

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补间的动画是匀速的

image
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变化

image
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不断变化形成动画

image
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的颜色,这样在刷新时就会呈现颜色渐变

image
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,点开可以调试曲线,获取四个值

image

用刚才的画点方法看了一下数据的变动情况

image

3.3:代码操作

根据包装设设计模式的思想,CurveTween可以强化Animation拥有从0~1的曲线,
然后再送到Tween中进行补间,让其在两个数的范围内具有曲线补间能力

image
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对象

image
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,拥有颤动效果的文字

image
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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,265评论 6 490
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,078评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,852评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,408评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,445评论 5 384
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,772评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,921评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,688评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,130评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,467评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,617评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,276评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,882评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,740评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,967评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,315评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,486评论 2 348

推荐阅读更多精彩内容