『Flutter-繪制篇』實現(xiàn)炫酷的雷電特效

前言

前不久,利用周末時間學習并完成一個簡單的 Flutter 項目 - 簡悅天氣,簡約不簡單,豐富不復雜,這是一款簡約風格的 flutter 天氣項目,提供實時、多日、24 小時、臺風路徑、語音播報以及生活指數(shù)等服務,支持定位、刪除、搜索等操作。

下圖為主頁效果,點擊下載 進行體驗:

圖1

項目中運用了大量的自定義繪制 widget,首頁豐富的 自定義 chart 效果和炫酷的天氣背景動效。天氣背景動效在不同的天氣氣象下展示不同的效果。目前一共實現(xiàn)了 15 種類別,其中有,晴、晴晚、多云、多云晚、陰天、小中大雨、小中大雪、霧、霾、浮塵以及雷暴。背景動效一共分為三層:

  • 背景顏色層。從上到下的漸變效果
  • 云層。只有一種圖片,對其位移、數(shù)量、染色做不同變化達到不同效果
  • 信息層。包括雨雪、雷暴和晴晚流星效果

之前分別用兩篇文章介紹雨雪和晴晚流星效果的實現(xiàn)細節(jié):

今天我們介紹背景動畫的最后一篇,如何實現(xiàn)炫酷的雷電特效,先看一下最終效果:

thunder

準備

根據(jù)實現(xiàn)效果進行分析,雨滴效果在之前文章有介紹過不多贅述,仔細觀察,其實就是對閃電圖片在繪制時控制其 alpha 以營造出這種霹靂的效果。

首先準備幾張閃電的素材,UI 網(wǎng)站找了很長時間沒有找到滿意的效果,關鍵費時費錢。后來發(fā)現(xiàn) oppo 最新版的天氣的雷暴效果??犰诺?,于是對其反編譯,找到他的資源目錄。其實 oppo 雷暴的動畫效果是通過 視頻+openGL 的方式實現(xiàn),里面有閃電的靜態(tài)資源、視頻資源和 openGL 代碼文件。我們只需提取他的靜態(tài)資源文件即可。隨即在 initState() 方法中異步獲取加載圖片資源:

  Future<void> fetchImages() async {
    weatherPrint("開始獲取雷暴圖片");
    var image1 = await ImageUtils.getImage('assets/images/lightning/lightning0.webp');
    var image2 = await ImageUtils.getImage('assets/images/lightning/lightning1.webp');
    var image3 = await ImageUtils.getImage('assets/images/lightning/lightning2.webp');
    var image4 = await ImageUtils.getImage('assets/images/lightning/lightning3.webp');
    var image5 = await ImageUtils.getImage('assets/images/lightning/lightning4.webp');
    _images.add(image1);
    _images.add(image2);
    _images.add(image3);
    _images.add(image4);
    _images.add(image5);
    weatherPrint("獲取雷暴圖片成功: ${_images?.length}");
  }

有了圖片后,開始構建對象和參數(shù)列表。由上面分析可知,除了基本的坐標 x,y 信息,只需要額外增加 alpha 屬性來達到效果。

class ThunderParams {
  ui.Image image;
  double x;
  double y;
  double alpha;
  int get imgWidth => image.width;
  int get imgHeight => image.height;

  ThunderParams(this.image);

  void reset() {
    x = Random().nextDouble() * 0.5.wp -  1 / 3 * imgWidth;
    y = Random().nextDouble() * -0.05.hp;
    alpha = 0;
  }
}

reset() 方法用于在當前雷暴結束時,重新初始化參數(shù)信息。

繪制

參數(shù)配置好后,繪制很簡單。有了圖片有了位置信息和 alpha 信息,調用 canvas 的相關 api 進行繪制即可。

  void drawThunder(ThunderParams params, Canvas canvas, Size size) {
    if (params == null || params.image == null) {
      return;
    }
    canvas.save();
    var identity = ColorFilter.matrix(<double>[
      1, 0, 0, 0, 0,
      0, 1, 0, 0, 0,
      0, 0, 1, 0, 0,
      0, 0, 0, params.alpha, 0,
    ]);
    _paint.colorFilter = identity;
    canvas.drawImage(params.image, Offset(params.x, params.y), _paint);
    canvas.restore();
  }

繪制到屏幕中大概長這樣:

<img src="https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/e893b7064eb9463b88f07d3d79fdd4e2~tplv-k3u1fbpfcp-zoom-1.image" alt="thunder2" style="zoom: 33%;" />

動畫

離炫酷就差最后一步 動畫

首先我們把單個閃電看做一個動畫對象,從消失到展示再顯示,落實到動畫上,alpha 由0到1,然后再到0。但是你可能發(fā)現(xiàn),出現(xiàn)的速度要比消失的速度要快。我們可以借助 TweenSequence 類來實現(xiàn)這個效果。

TweenSequence 是一個動畫序列,支持配置權重,以及對應的動畫 Tween。這樣,我們可以給 alpha 在 [0,1] 區(qū)間做動畫時權重設置低一點,[1,0] 時權重高一點。

    var _animation = TweenSequence([
      TweenSequenceItem(
          tween: Tween(begin: 0.0, end: 1.0)
              .chain(CurveTween(curve: Curves.easeIn)),
          weight: 1),
      TweenSequenceItem(
          tween: Tween(begin: 1.0, end: 0.0)
              .chain(CurveTween(curve: Curves.easeIn)),
          weight: 3),
    ]);

實現(xiàn)后,效果如下:

image

然后,我們用三個隨機的閃電作為一組,做循環(huán)動畫,控制其序列幀,完成連續(xù)&不同&隨機的刪掉效果。

    _controller = AnimationController(duration: Duration(seconds: 1), vsync: this);
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.reset();
        Future.delayed(Duration(milliseconds: 10)).then((value) {
          initThunderParams();
          _controller.forward();
        });
      }
    });

    var _animation = TweenSequence([
      TweenSequenceItem(
          tween: Tween(begin: 0.0, end: 1.0)
              .chain(CurveTween(curve: Curves.easeIn)),
          weight: 1),
      TweenSequenceItem(
          tween: Tween(begin: 1.0, end: 0.0)
              .chain(CurveTween(curve: Curves.easeIn)),
          weight: 3),
    ]).animate(CurvedAnimation(
      parent: _controller,
      curve: Interval(
        0.0, 1.0,
        curve: Curves.ease,
      ),
    ));

在之前說的一個閃電動畫后面,新增 .animate() 的配置,通過控制

Interval(
        0.0, 0.3,
        curve: Curves.ease,
      )

配置該動畫執(zhí)行序列幀的開始和結束,以及插值器。

通過在 addStatusListener 中監(jiān)聽動畫的執(zhí)行狀態(tài),在觸發(fā) AnimationStatus.completed 時隨機等待一定時間后,重新開始。

到此,一個炫酷的雷電特效就完成了,是不是很簡單,如果覺得還不錯,后面考慮把天氣動畫背景做成插件供有需要的小伙伴使用

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內容