Flutter -如何创建炫酷粒子时钟效果!

文章首发于公众号:技术最TOP

周末发表了一篇文章《这个项目也太屌了吧》,给大家推荐了一个炫酷的Flutter粒子时钟项目,不过没有将具体实现思路和代码,所幸,作者自己写了一篇博客将这个项目的背景、实现思路、和所遇到的问题,我觉得对非常有用,因此翻译出来,整理给大家!原文题目《我是如何创建粒子时钟,并赢得了#FlutterClock挑战的》

背景

Google在2019年11月18日发起了The Flutter Clock Challenge挑战活动,内容很简单:使用Flutter UI工具包设计时钟。Google专家小组将根据四个主要标准对参赛作品进行评判:视觉美感创意新颖性代码质量整体执行力

在这之前,我只使用过Flutter一两次,这对于我来说是一个潜在的机会。

最初的想法

挑战开始了大约两周后,我才有了一些想法,但还没有编写任何代码,解决此类问题时,我通常的方法是首先寻找现有的解决方案来找灵感。但这次不行。相反,我在Figma上新建了一个文档,并列出了一些想法。它们都是非常简单数字时钟设计晦涩的单色组合

image

很快地,我就对这个设计感到厌倦了,所以我关闭了Figma,就去下载了Flutter Clock GitHub(https://github.com/flutter/flutter_clock)库,以查看一些示例代码。

这个库包含了2个项目,一个是基于模拟时钟的演示,另一个是基于数字时钟的演示,我在Figma上设计都是数字的,所以自然而然地,我启动了基本的数字时钟项目。再次缺乏灵感,在示例项目的帮助下,我搁置了挑战,开始去做其他事儿。

几天之后,在一次晨跑中,我又开始认真的思考这个挑战,一个普通的成年人每天看几次手表?对我来说,让时钟变得有趣起来是真正的挑战。是否可以使“报时间”成为自动体验?比如:即使您对时间不感兴趣,看着手表也很有趣。这不仅需要视觉上令人惊叹的设计新颖的动画方案

  • 1、如果每次您看钟时都有不同的外观会怎样?
  • 2、我们能否激发好奇心,使您渴望下一次设计迭代?或者,当您喜欢的设计永久消失时,您会感到难过吗?
  • 3、我们是否可以将背景形状、颜色、动画随机改变以使它:a. 看起来很酷,b. 足够满足1和2的随机性,c. 不会分散你看时间的注意力?

我以前从未完成过任何艺术,或者根本没有做过Flutter,所以我着手建造这样的时钟。

粒子与随机性

第一次迭代只是一个时钟。如上所述,我从示例数字时钟项目开始,而不是从头开始。创建的第一个Widget是一个CustomPainter,仅绘制了一个圆圈。很不错,但是从长远来看不是很有趣。

随机性增加了,从颜色开始,然后确定位置和大小。所有逻辑仍然在单个CustomPainterpaint()方法中,这几乎使动画变得不可能,因此需要将一堆逻辑重构成一个简单的粒子系统。我看了Flutter Vignettes项目,以寻求启发。

https://flutter.gskinner.com/

这时候,制作模拟粒子时钟的想法变得更加明显了。

将粒子变成模拟时钟

提出想法后,我要做的就是编写代码以实现所有目标。数学部分花费了我最多的时间才最终完成,大多是我多年以前学过的数学,不过好多我都忘记了,角度,弧度,PI和类似的东西,网上又许多解决方案,但是你将不得不做一些修改以适应您的用例。

以下是获取时针弧度的方法:

/// Gets the radians of the hour hand.
double _getHourRadians() =>
    (time.hour * pi / 6) +
    (time.minute * pi / (6 * 60)) +
    (time.second * pi / (360 * 60));

我在计算中包括了time.minutetime.second,以使时针在数小时之间平滑地动画。

然后,从弧度获得2D运动矢量就很简单了。

// Particle movement vector.
p.vx = sin(-angle);
p.vy = cos(-angle);

现在,p.vxp.vy拥有 有关粒子在每个动画滴答声中应该移动多远的信息,同时保持在时针的角度里。

除了钟针外,粒子还可能作为噪音产生。然后它将从中心沿随机方向发射。在发出时,还将为所有粒子分配随机的速度颜色大小绘画样式(填充或笔划)。这使时钟看起来更有趣。

image

时钟的早期版本中。这有四分之一标记,有些粒子有速度标记。时钟的第一个版本只是粒子,这里就不花时间截图演示了。

使用Flutter Widgets 添加图层

到现在为止,所有内容都使用单个CustomPainter小部件(即widget,下文也一样)绘制。时钟看起来不错,但是很难告诉你时间。而且,背景是单色,看上去很无聊。

Flutter非常适合构建复杂的布局。毕竟,它是一个用于用户界面的工具包。将一堆小部件彼此堆叠,您只需将它们包装在Stack widget中。粒子时钟的最后一个场景小部件负责构建3个主要层:

  • 1、Background:一个带有CustomPaint小部件的堆栈,该小部件绘制不同颜色和绘画样式的随机形状,以及一个应用了模糊效果的BackdropFilter

  • 2、Clock Face: 带有2个CustomPaint 小部件的堆栈,

    • a. 时钟标记-绘制时钟标记。每分钟标记,每5分钟标记具有额外的可见性。
    • b. 秒针-绘制两秒的针弧。
  • 3、 Particle FX: 一个CustomPaint小部件,用于绘制所有粒子。

@override
Widget build(BuildContext context) {
  return AnimatedContainer(
    duration: Duration(milliseconds: 1500),
    curve: Curves.easeOut,
    color: _bgColor,
    child: ClipRect(
      child: Stack(
        children: <Widget>[
          _buildBgBlurFx(),
          _buildClockFace(),
          CustomPaint(
            painter: ClockFxPainter(fx: _fx),
            child: Container(),
          ),
        ],
      ),
    ),
  );
}

即使底层代码很复杂,Flutter仍可以通过小部件组合来管理布局。

image

时钟绘图层和覆盖层。

image

这是与上述相同的图片,但没有覆盖层。

与时间同步的动画

我很早就想到,如果动画与时钟的滴答声同步发生,那就太酷了。最终版本中的解决方案非常简单。没有到达到想象中的目标。最初,我把它变成了一个非常复杂的问题,并尝试了各种怪异的技巧使它起作用。

@override
void tick(Duration duration) {
  var secFrac = DateTime.now().millisecond / 1000;

  var vecSpeed = duration.compareTo(easingDelayDuration) > 0
      ? max(.2, Curves.easeInOutSine.transform(1 - secFrac))
      : 1;

  particles.asMap().forEach((i, p) {
    // Movement
    p.x -= p.vx * vecSpeed;
    p.y -= p.vy * vecSpeed;
    // etc...
  }
}

以上代码在每个动画刻度上运行。通过结合使用DateTime.now()(以毫秒为单位)和Curves,我们得到一个介于01之间的值。max函数确保该数字保持在0.2以上,以始终保持粒子随着每个刻度移动。

然后,在计算粒子的新xy位置时,将vecSpeed编号与运动矢量结合使用。

调色板和清晰度

在图形用户界面中随机分配颜色时,通常会让人感到厌烦。当然,这是有充分的理由,因为它通常会使GUI的访问性降低。在保持易读性的同时将随机颜色应用于GUI并不是一个容易解决的问题。幸运的是,Flutter有一些工具可以使我们更轻松。

首先,我使用了ColourLovers API来获取其用户最喜欢的一些调色板。简而言之,许多调色板的颜色之间的对比度很差。我根据WCAG Contrast指南,创建了一个过滤调色板阵列的脚本来解决了这一问题。过滤后,该列表仅包含调色板,其中至少存在一种对比度大于或等于4.5的颜色组合。

然后,在Flutter中,我们仅需使用Color类的computeLuminance方法即可找到良好的匹配项。

/// Gets a random palette from a list of palettes and sorts its'
/// colors by luminance.
///
/// Given if [dark] or not, this method makes sure the luminance
/// of the background color is valid.
static Palette getPalette(List<Palette> palettes, bool dark) {
  Palette result;

  while (result == null) {
    Palette palette = Rnd.getItem(palettes);
    List<Color> colors = Rnd.shuffle(palette.components);

    var luminance = colors[0].computeLuminance();

    if (dark ? luminance <= .1 : luminance >= .1) {
      var lumDiff = colors
          .sublist(1)
          .asMap()
          .map(
            (i, color) => MapEntry(
              i,
              [i, (luminance - color.computeLuminance()).abs()],
            ),
          )
          .values
          .toList();

      lumDiff.sort((List<num> a, List<num> b) {
        return a[1].compareTo(b[1]);
      });

      List<Color> sortedColors =
          lumDiff.map((d) => colors[d[0] + 1]).toList();

      result = Palette(
        components: [colors[0]] + sortedColors,
      );
    }
  }
  return result;
}

该代码返回一个Palette,仅包含一个颜色列表。通过颜色之间的亮度差异调色板进行排序。

然后,此方法的调用者可以确保组件的第一项最后一项具有足够好的对比度。

image

一小部分,可能有许多不同颜色变化。请注意,就亮度而言,强调色始终总是与背景色最远的一种。

最后的润色

大部分魔术发生在编写此代码的最后几个小时。使发出的粒子从中心淡入,而不是从无处突然弹出。这使整体外观更加平滑。我对弧/速度标记进行了相同的操作,并一次将它们限制为仅几个粒子,以减少视觉复杂性。

最初,我不确定如何避免沿钟针方向发射噪声粒子,但知道必须这样做。在放弃寻找数学解之后,我用了一些蛮力的代码解决了这个问题(当然,数学解方案是有的,只是我没有耐心寻找到它)。

// Find a random angle while avoiding clutter at the hour & minute hands.
var am = _getMinuteRadians();
var ah = _getHourRadians() % (pi * 2);
var d = pi / 18;

// Probably not the most efficient solution right here.
do {
  angle = Rnd.ratio * pi * 2;
} while (_isBetween(angle, am - d, am + d) || _isBetween(angle, ah - d, ah + d));

有效!这样一来,所有噪音颗粒都从针中移开,就更容易分辨时间了。

最后

有时令人沮丧(谢谢,数学!😅)。但最后,我对结果感到满意。我特别喜欢不断变化的色彩和有机的、不可预测的动画。

Flutter非常适合此类事情。创造力需要实验,这就是Flutter令人瞩目的地方。在这个项目的开始,我不知道它最终会变成这样。记住,我最初的想法是建立一个数字时钟。但是由于一些幸运的错误,以及对不同想法的成千上万次小迭代,它的变化比最初想象的要好。

最终演示视频地址:https://youtu.be/VPbcVhKIzIo

Google Flutter全球市场总监 Martin Aguinis发推文说,他们在86个国家/地区收到了850份独特的作品。在所有这些中,Google专家评审团选出了我的获得大奖(一台装有Apple iMac Pro的苹果,价值约10,000美元)。我从来没有认为自己是一个优秀的程序员,所以当Martin向我伸手时,我真的很惊讶!我现在仍然不敢相信自己赢了。

感谢Google和Flutter团队使这一挑战得以实现,也感谢所有为我加油并在Twitter上对我表示支持的人

项目Github地址:https://github.com/miickel/flutter_particle_clock

原文链接:https://ultimatemachine.se/articles/how-i-created-a-particle-clock-and-won-the-flutterclock-challenge/