Flutter實現(xiàn)"劍氣"加載???

前言:前幾天在掘金上看到一篇文章,用html+css編寫了一個劍氣加載的動效竟痰。前端能做的東西郊愧,我Flutter大前端豈能罷休?于是小弟班門弄斧蟆融,用Flutter編寫了這個劍氣動效草巡。
掘金文章:https://juejin.cn/post/7001779766852321287

效果圖

劍氣加載.gif

知識點(diǎn)

  • Animation【動效】
  • Clipper/Canvas【路徑裁剪/畫布】
  • Matrix4【矩陣轉(zhuǎn)化】

劍氣形狀

我們仔細(xì)看一道劍氣,它的形狀是一輪非常細(xì)小的彎彎的月牙型酥;在Flutter中山憨,我們可以通過Clipper路徑來裁剪出來,或者也可以通過canvas繪制出來冕末。

  1. 先看canvas如何進(jìn)行繪制的
class MyPainter extends CustomPainter {
  Color paintColor;

  MyPainter(this.paintColor);

  Paint _paint = Paint()
    ..strokeCap = StrokeCap.round
    ..isAntiAlias = true
    ..strokeJoin = StrokeJoin.bevel
    ..strokeWidth = 1.0;

  @override
  void paint(Canvas canvas, Size size) {
    _paint..color = this.paintColor;
    Path path = new Path();
    // 獲取視圖的大小
    double w = size.width;
    double h = size.height;
    // 月牙上邊界的高度
    double topH = h * 0.92;
    // 以區(qū)域中點(diǎn)開始繪制
    path.moveTo(0, h / 2);
    // 貝塞爾曲線連接path
    path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
    path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
    path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
    path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);

    canvas.drawPath(path, _paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) => false; // 一次性畫好萍歉,不需要更新侣颂,返回false
}
  1. Clipper也上代碼档桃,跟canvas兩種選其一即可,我用的是canvas
class SwordPath extends CustomClipper<Path> {
  @override
  getClip(Size size) {
    print(size);
    // 獲取視圖的大小
    double w = size.width;
    double h = size.height;
    // 月牙上邊界的高度
    double topH = h * 0.92;
    Path path = new Path();
    // 以區(qū)域中點(diǎn)開始繪制
    path.moveTo(0, h / 2);
    // 貝塞爾曲線連接path
    path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
    path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
    path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
    path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);
    return path;
  }

  @override
  bool shouldReclip(covariant CustomClipper oldClipper) => false;
}
  1. 生成月牙控件
CustomPaint(
    painter: MyPainter(widget.loadColor),
    size: Size(200, 200),
),

讓劍氣旋轉(zhuǎn)起來

我們需要劍氣一直不停的循環(huán)轉(zhuǎn)動憔晒,所以需要用到動畫藻肄,讓劍氣圍繞中心的轉(zhuǎn)動起來蔑舞。注意這里只是單純的平面旋轉(zhuǎn),也就是我們說的2D變換嘹屯。這里我們用到的是Transform.rotate控件攻询,通過animation.value傳入旋轉(zhuǎn)的角度,從而實現(xiàn)360度的旋轉(zhuǎn)州弟。

class _SwordLoadingState extends State<SwordLoading>
    with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  double angle = 0;

  @override
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 800));
    // pi * 2:360°旋轉(zhuǎn)
    _animation = Tween(begin: 0.0, end: pi * 2).animate(_controller);
    _controller.repeat(); // 循環(huán)播放動畫
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Transform.rotate(
      alignment: Alignment.center,
      angle: _animation.value,
      child: CustomPaint(
        painter: MyPainter(widget.loadColor),
        size: Size(widget.size, widget.size),
      ),
    );
   }
}
轉(zhuǎn)起來啦钧栖!

讓劍氣有角度的、更犀利的轉(zhuǎn)動

  • 我們仔細(xì)看單獨(dú)一條劍氣婆翔,其實是在一個三維的模型中拯杠,把與Z軸垂直的劍氣 向Y軸、X軸進(jìn)行了一定角度的偏移啃奴。
  • 相當(dāng)于在這個3D空間內(nèi)潭陪,劍氣不在某一個平面了,而是斜在這個空間內(nèi)最蕾,然后 再繞著圓心去旋轉(zhuǎn)依溯。
  • 而觀者的視圖,永遠(yuǎn)與Z軸垂直【或者說:X軸和Y軸共同組成的平面上】瘟则,所以就會產(chǎn)生劍氣 從外到里進(jìn)行旋轉(zhuǎn) 的感覺黎炉。
    純手工繪制

    不要笑我

綜上,可以確定這個過程是一個3D的變換醋拧,很明顯我們Transform.rotate這種2D的widget已經(jīng)不滿足需求了拜隧,這個時候Matrix4大佬上場了,我們通過Matrix4.identity()..rotate的方法趁仙,傳入我們的3D轉(zhuǎn)化洪添,在通過rotateZ進(jìn)行旋轉(zhuǎn),簡直完美雀费。代碼如下

 AnimatedBuilder(
    animation: _animation,
    builder: (context, _) => Transform(
      transform: Matrix4.identity()
              ..rotate(v.Vector3(0, -8, 12), pi)
              ..rotateZ(_animation.value),
      alignment: Alignment.center,
      child: CustomPaint(
              painter: MyPainter(widget.loadColor),
              size: Size(widget.size, widget.size),
      ),
   ),
),

這里多說一句干奢,要完整矩陣變換,Matrix4必不可少盏袄,可以著重學(xué)習(xí)下忿峻。

讓劍氣一起動起來

完成一個劍氣的旋轉(zhuǎn)之后,我們回到預(yù)覽效果辕羽,無非就是3個劍氣堆疊在一起逛尚,通過偏移角度去區(qū)分。Flutter堆疊效果直接用Stack實現(xiàn)刁愿,完整代碼如下:

import 'package:flutter/material.dart';
import 'dart:math';
import 'package:vector_math/vector_math_64.dart' as v;

class SwordLoading extends StatefulWidget {
  const SwordLoading({Key? key, this.loadColor = Colors.black, this.size = 88})
      : super(key: key);

  final Color loadColor;
  final double size;

  @override
  _SwordLoadingState createState() => _SwordLoadingState();
}

class _SwordLoadingState extends State<SwordLoading>
    with TickerProviderStateMixin {
  late AnimationController _controller;
  late Animation<double> _animation;
  double angle = 0;

  @override
  void initState() {
    _controller =
        AnimationController(vsync: this, duration: Duration(milliseconds: 800));
    _animation = Tween(begin: 0.0, end: pi * 2).animate(_controller);
    _controller.repeat();
    super.initState();
  }

  @override
  Widget build(BuildContext context) {
    return Stack(
      children: [
        AnimatedBuilder(
          animation: _animation,
          builder: (context, _) => Transform(
            transform: Matrix4.identity()
              ..rotate(v.Vector3(0, -8, 12), pi)
              ..rotateZ(_animation.value),
            alignment: Alignment.center,
            child: CustomPaint(
              painter: MyPainter(widget.loadColor),
              size: Size(widget.size, widget.size),
            ),
          ),
        ),
        AnimatedBuilder(
          animation: _animation,
          builder: (context, _) => Transform(
            transform: Matrix4.identity()
              ..rotate(v.Vector3(-12, 8, 8), pi)
              ..rotateZ(_animation.value),
            alignment: Alignment.center,
            child: CustomPaint(
              painter: MyPainter(widget.loadColor),
              size: Size(widget.size, widget.size),
            ),
          ),
        ),
        AnimatedBuilder(
          animation: _animation,
          builder: (context, _) => Transform(
            transform: Matrix4.identity()
              ..rotate(v.Vector3(-8, -8, 6), pi)
              ..rotateZ(_animation.value),
            alignment: Alignment.center,
            child: CustomPaint(
              painter: MyPainter(widget.loadColor),
              size: Size(widget.size, widget.size),
            ),
          ),
        ),
      ],
    );
  }
}

class MyPainter extends CustomPainter {
  Color paintColor;

  MyPainter(this.paintColor);

  Paint _paint = Paint()
    ..strokeCap = StrokeCap.round
    ..isAntiAlias = true
    ..strokeJoin = StrokeJoin.bevel
    ..strokeWidth = 1.0;

  @override
  void paint(Canvas canvas, Size size) {
    _paint..color = this.paintColor;
    Path path = new Path();
    // 獲取視圖的大小
    double w = size.width;
    double h = size.height;
    // 月牙上邊界的高度
    double topH = h * 0.92;
    // 以區(qū)域中點(diǎn)開始繪制
    path.moveTo(0, h / 2);
    // 貝塞爾曲線連接path
    path.cubicTo(0, topH * 3 / 4, w / 4, topH, w / 2, topH);
    path.cubicTo((3 * w) / 4, topH, w, topH * 3 / 4, w, h / 2);
    path.cubicTo(w, h * 3 / 4, 3 * w / 4, h, w / 2, h);
    path.cubicTo(w / 4, h, 0, h * 3 / 4, 0, h / 2);

    canvas.drawPath(path, _paint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) =>
      false; // 一次性畫好绰寞,不需要更新,返回false
}

業(yè)務(wù)端調(diào)用

SwordLoading(loadColor: Colors.black,size: 128),

寫在最后

花了我整個周六下午的時間,很開心用Flutter實現(xiàn)了加載動畫滤钱,說說感受吧觉壶。

  1. 在編寫的過程中,對比html+css的方式件缸,F(xiàn)lutter的實現(xiàn)難度其實更大铜靶,而且劍氣必須使用canvas繪制出來。
  2. 如果你也懂前端他炊,你可以深刻體會聲明式和命令式UI在編寫布局和動畫所帶來的強(qiáng)烈差異争剿,從而加深Flutter萬物皆對象的思想。*【因為萬物皆對象痊末,所以所有控件和動畫秒梅,都是可以顯示聲明的對象,而不是像前端那樣通過解析xml命令來顯示】
  3. 2D/3D變換舌胶,我建議Flutter學(xué)者們捆蜀,一定要深入學(xué)習(xí),這種空間思維對我們實現(xiàn)特效是不可獲取的能力幔嫂。

好了辆它,小弟班門弄斧,希望能一起學(xué)習(xí)進(jìn)步B亩鳌C誊浴!

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末切心,一起剝皮案震驚了整個濱河市飒筑,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌绽昏,老刑警劉巖协屡,帶你破解...
    沈念sama閱讀 219,427評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異全谤,居然都是意外死亡肤晓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評論 3 395
  • 文/潘曉璐 我一進(jìn)店門认然,熙熙樓的掌柜王于貴愁眉苦臉地迎上來补憾,“玉大人,你說我怎么就攤上這事卷员∮遥” “怎么了?”我有些...
    開封第一講書人閱讀 165,747評論 0 356
  • 文/不壞的土叔 我叫張陵毕骡,是天一觀的道長削饵。 經(jīng)常有香客問我岩瘦,道長,這世上最難降的妖魔是什么葵孤? 我笑而不...
    開封第一講書人閱讀 58,939評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮橱赠,結(jié)果婚禮上尤仍,老公的妹妹穿的比我還像新娘。我一直安慰自己狭姨,他們只是感情好宰啦,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著饼拍,像睡著了一般赡模。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上师抄,一...
    開封第一講書人閱讀 51,737評論 1 305
  • 那天漓柑,我揣著相機(jī)與錄音,去河邊找鬼叨吮。 笑死辆布,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的茶鉴。 我是一名探鬼主播锋玲,決...
    沈念sama閱讀 40,448評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼涵叮!你這毒婦竟也來了惭蹂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,352評論 0 276
  • 序言:老撾萬榮一對情侶失蹤割粮,失蹤者是張志新(化名)和其女友劉穎盾碗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體舀瓢,經(jīng)...
    沈念sama閱讀 45,834評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡置尔,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評論 3 338
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了氢伟。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片榜轿。...
    茶點(diǎn)故事閱讀 40,133評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖朵锣,靈堂內(nèi)的尸體忽然破棺而出谬盐,到底是詐尸還是另有隱情,我是刑警寧澤诚些,帶...
    沈念sama閱讀 35,815評論 5 346
  • 正文 年R本政府宣布飞傀,位于F島的核電站皇型,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏砸烦。R本人自食惡果不足惜弃鸦,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望幢痘。 院中可真熱鬧唬格,春花似錦、人聲如沸颜说。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,022評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽门粪。三九已至喊积,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間玄妈,已是汗流浹背乾吻。 一陣腳步聲響...
    開封第一講書人閱讀 33,147評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留拟蜻,地道東北人溶弟。 一個月前我還...
    沈念sama閱讀 48,398評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像瞭郑,于是被迫代替她去往敵國和親辜御。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評論 2 355

推薦閱讀更多精彩內(nèi)容