『Flutter-绘制篇』实现炫酷的雨雪特效

前言

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

下图为主页效果:

图1

开始

项目中很多自定义 widget,今天的主角是 背景层,不同的天气气象有不一样的呈现效果,一共实现了 12 种类别,其中有 晴、多云、阴天、小中大雨、小中大雪、雾、霾、浮尘,而背景层又分为三层:

  • 背景颜色层。从上到下的渐变效果
  • 云层。只有一种图片,对其位移、数量、染色做不同变化达到不同效果
  • 雨雪层。为雨雪天气单独做了动画,很炫酷。

好,真正的主角就是这个雨雪层,为了更好的预览效果,在关于页面有上角添加切换天气类型的入口,实时查看不同气象下不同的背景效果。如下图,为雨雪的最终效果(gif 效果看起来会失真,请下载 apk 自行体验):

图5
图6

不得不说,如此复杂的动画(复杂并不是指多难实现,而是不停的绘制很多图片下),Flutter 还能有不错的性能表现,媲美原生效果。

效果实现

这里不赘述绘制和动画相关知识,网上已经有很多文章介绍,本篇只针对项目中用到的实现方式和相关知识进行讲述,具有一定的局限性,适合简单的绘制动画逻辑。

创建绘制类

因为 Flutter 处处是 widget,自定义 View 需要用到的是 CustomPaint,而成员变量中需要传入实现 CustomPainter 的类,那咱们先创建此类。

class RainSnowPainter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) {
    return true;
  }
}

有没有很熟悉,看到了 Canvas 的类,自然也有 Paint 类,有了画笔和画板,剩下就好办了。

构造雨雪对象

对需要实现的效果进行分析,首先雨雪效果是由一张图片不同属性拼接而成,每个雨滴和雪花落实在屏幕上,必须有 x,y 的坐标属性。为了营造远近的效果,需要加上 scale 值,由于更加还原真实的视觉效果,雨滴的远近,必然速度上和清晰度上会有差异,因此加上 speed 和 alpha 属性,再加上其他计算用的属性,最后类的声明如下:

class RainSnowParams {
  double x;
  double y;
  double speed;
  double scale;
  double width;
  double height;
  double alpha;
  WeatherType weatherType;

  RainSnowParams(this.width, this.height, this.weatherType);
}

属性初始化

有了属性后,接下来就是对属性进行赋值,为了保证效果更加的还原,所有属性既要有规则,又要随机。怎么解释规则性和随机性都要同时拥有,就拿雨速而言,小雨相对于大雨,雨的速度稍慢,但是不能很慢,并且每滴雨滴的速度不一样,这就致使小雨的速度必然在一个区间下随机,同样雪也一样。

初始化又分成两步,第一次的初始化和雨滴下落结束后的数据重置,实际上两者的区别只在于 y。第一次初始化 y 在屏幕高度中随机放置,而雨滴下落结束后,y 值置为0。那么就可以把重置逻辑封装统一的方法。

void reset() {
  double initScale = 0.1;
  double gapScale = 0.2;
  double initSpeed = 40;
  double gapSpeed = 40;
  if (weatherType == WeatherType.lightRainy) {
    initScale = 1.05;
    gapScale = 0.1;
    initSpeed = 15;
    gapSpeed = 10;
  } else if(){
    ...// 其他雨雪情况
  }
  double random = Random().nextDouble();
  this.scale = initScale + gapScale * random;
  this.speed = initSpeed + gapSpeed * (1 - random);
  this.alpha = 0.1 + 0.9 * random;
  x = Random().nextInt(width * 1.2 ~/ scale).toDouble() - width * 0.1 ~/ scale;
}

其中 init 代表这初始值,gap 代表浮动值,这两个根据雨雪量大小而做不同区分。通过 Random().nextDouble()获取随机 [0.0, 1.0] 的值,random * gap + init 就是最终的值。

x 的属性控制在 [-0.1*width, 1.1 width]的区间内随机,y 值上面已提到。

雪相对有雨有个不同,雨是垂直下落,而雪是随风摇摆,那为了营造这种感觉,此时就需要借助 sin 函数。

if (WeatherUtil.isSnow(_state.widget.weatherType)) {
    double offsetX = sin(params.y / (300 + 50 * params.alpha)) * (1 + 0.5 * params.alpha);
    params.x += offsetX;
}

开始绘制

终于到了最重要的步骤 绘制,但他并不是最难的,有了前面创建好的属性和对其初始化,剩下就只是调用 api 进行绘制即可。不过再此之前好像漏了什么没说,没错,就是 动画,一个无限循环的动画。

Flutter 中创建动画也很简单,需要在动画监听中,判断如果动画结束则重新继续执行即可。

1. 在 initState 函数中初始化 controller, animation 和 listener
    _controller =
        AnimationController(duration: Duration(minutes: 1), vsync: this);
    CurvedAnimation(parent: _controller, curve: Curves.linear);
    _controller.addListener(() {
      setState(() {});
    });
    _controller.addStatusListener((status) {
      if (status == AnimationStatus.completed) {
        _controller.repeat();
      }
    });
    _controller.forward();
2. 在 dispose 函数中释放掉动画资源
  @override
  void dispose() {
    _controller.dispose();
    super.dispose();
  }

在初始化是便让他执行并一直执行知道页面销毁,有了动画后,开始进行绘制,雨雪的绘制逻辑基本相似,只不过图片源不一样。

别看屏幕上有很多雨滴,其实只用了一直图片,通过控制 alpha、speed和scale 的属性来随机展现不同的形态。还有,根据气象大中小雨类型的区分,会直接落实到雨滴数量和雨滴形态上的变化,营造出多样的差异。

  void drawRain(Canvas canvas, Size size) {
    weatherPrint(
        "开始绘制雨层 image:${_state._images?.length}, rains:${_state._rainSnows?.length}");
    if (_state._images != null && _state._images.length > 1) {
      ui.Image image = _state._images[0];
      if (_state._rainSnows != null && _state._rainSnows.isNotEmpty) {
        _state._rainSnows.forEach((element) {
          move(element);
          ui.Offset offset = ui.Offset(element.x, element.y);
          canvas.save();
          canvas.scale(element.scale, element.scale);
          var identity = ColorFilter.matrix(<double>[
            1, 0, 0, 0, 0,
            0, 1, 0, 0, 0,
            0, 0, 1, 0, 0,
            0, 0, 0, element.alpha, 0,
          ]);
          _paint.colorFilter = identity;
          canvas.drawImage(image, offset, _paint);
          canvas.restore();
        });
      }
    }
  }

这里绘制逻辑只用到了 drawImage 的方法,参数分别为图片、位置和画笔,不像 Android 提供了 paint.setAlpha() 的方法控制图片的透明值,这里需要通过 colorFilter 修改矩阵中对应的值来控制 alpha。

move() 函数用于控制雨滴在运动过程中 x和y 值的不断变化。

  void move(RainSnowParams params) {
    params.y = params.y + params.speed;
    if (WeatherUtil.isSnow(_state.widget.weatherType)) {
      double offsetX = sin(params.y / (300 + 50 * params.alpha)) * (1 + 0.5 * params.alpha);
      params.x += offsetX;
    }
    if (params.y > 800 / params.scale) {
      params.y = 0;
      if (WeatherUtil.isRainy(_state.widget.weatherType) &&
          _state._images.isNotEmpty &&
          _state._images[0] != null) {
        params.y = -_state._images[0].height.toDouble();
      }
      params.reset();
    }
  }

该方法每次重绘时都会调用,即 y += speed 根据 speed 不断修改 y 的属性,因为雪的特殊性,x 会通过 sin 函数运算后得出。以及当雨滴超过屏幕需要重新归位并重新初始化。

到此, 雨雪的绘制和动画逻辑已经讲述结束,是不是很简单,但是效果上还是相当酷炫的,感兴趣的可以到 SimplicityWeather 下载进行查看更多效果。最后再看看大雨下的效果。

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