Flutter 動畫之 Animation

1.前言

1.1:Flutter動畫中:

首先要看的是Flutter中動畫的幾個類之間的關係:git

主角固然是咱們的Animation類了,它能夠藉助Animatable進行強化
Animatable經過animate函數接收一個Animation對象,再返回Animation對象,這不就是包裝嗎?
經過Animation對象回調便可獲取規律變畫的值,進行渲染。這是動畫的基本。github


1.2:Animation和Animation體系一覽

整個Flutter的Animation相比Android仍是比較簡單的canvas


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+代碼生成器=懶人必備api

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使用圖:bash


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()方法app

animation = Tween(begin: 25.0, end: 150.0).animate(controller)
    ..addListener(() {
      print(animation.value);
    });
複製代碼

看一下控制檯打印結果:從25~150變化的一羣數字dom

---->[控制檯打印]----
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

相關文章
相關標籤/搜索