『Flutter-绘制篇』实现炫酷的雷电特效

前言

前不久,利用周末时间学习并完成一个简单的 Flutter 项目 - 简悦天气简约不简单,丰富不复杂,这是一款简约风格的 flutter 天气项目,提供实时、多日、24 小时、台风路径、语音播报以及生活指数等服务,支持定位、删除、搜索等操作。

下图为主页效果,点击下载 进行体验:

图1

项目中运用了大量的自定义绘制 widget,首页丰富的 自定义 chart 效果和炫酷的天气背景动效。天气背景动效在不同的天气气象下展示不同的效果。目前一共实现了 15 种类别,其中有,晴、晴晚、多云、多云晚、阴天、小中大雨、小中大雪、雾、霾、浮尘以及雷暴。背景动效一共分为三层:

  • 背景颜色层。从上到下的渐变效果
  • 云层。只有一种图片,对其位移、数量、染色做不同变化达到不同效果
  • 信息层。包括雨雪、雷暴和晴晚流星效果

之前分别用两篇文章介绍雨雪和晴晚流星效果的实现细节:

今天我们介绍背景动画的最后一篇,如何实现炫酷的雷电特效,先看一下最终效果:

thunder

准备

根据实现效果进行分析,雨滴效果在之前文章有介绍过不多赘述,仔细观察,其实就是对闪电图片在绘制时控制其 alpha 以营造出这种霹雳的效果。

首先准备几张闪电的素材,UI 网站找了很长时间没有找到满意的效果,关键费时费钱。后来发现 oppo 最新版的天气的雷暴效果停酷炫的,于是对其反编译,找到他的资源目录。其实 oppo 雷暴的动画效果是通过 视频+openGL 的方式实现,里面有闪电的静态资源、视频资源和 openGL 代码文件。我们只需提取他的静态资源文件即可。随即在 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}");
  }

有了图片后,开始构建对象和参数列表。由上面分析可知,除了基本的坐标 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() 方法用于在当前雷暴结束时,重新初始化参数信息。

绘制

参数配置好后,绘制很简单。有了图片有了位置信息和 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。但是你可能发现,出现的速度要比消失的速度要快。我们可以借助 TweenSequence 类来实现这个效果。

TweenSequence 是一个动画序列,支持配置权重,以及对应的动画 Tween。这样,我们可以给 alpha 在 [0,1] 区间做动画时权重设置低一点,[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),
    ]);

实现后,效果如下:

image

然后,我们用三个随机的闪电作为一组,做循环动画,控制其序列帧,完成连续&不同&随机的删掉效果。

    _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,
      )

配置该动画执行序列帧的开始和结束,以及插值器。

通过在 addStatusListener 中监听动画的执行状态,在触发 AnimationStatus.completed 时随机等待一定时间后,重新开始。

到此,一个炫酷的雷电特效就完成了,是不是很简单,如果觉得还不错,后面考虑把天气动画背景做成插件供有需要的小伙伴使用

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