本文首发于公众号:技术最TOP
周末发表了一篇文章《这个项目也太屌了吧》,给你们推荐了一个炫酷的Flutter粒子时钟项目,不过没有将具体实现思路和代码,所幸,做者本身写了一篇博客将这个项目的背景、实现思路、和所遇到的问题,我以为对很是有用,所以翻译出来,整理给你们!原文题目《我是如何建立粒子时钟,并赢得了#FlutterClock挑战的》
。git
Google在2019年11月18日发起了The Flutter Clock Challenge
挑战活动,内容很简单:使用Flutter UI工具包设计时钟
。Google专家小组将根据四个主要标准对参赛做品进行评判:视觉美感
,创意新颖性
,代码质量
和总体执行力
。程序员
在这以前,我只使用过Flutter一两次,这对于我来讲是一个潜在的机会。github
挑战开始了大约两周后,我才有了一些想法,但尚未编写任何代码,解决此类问题时,我一般的方法是首先寻找现有的解决方案来找灵感。但此次不行。相反,我在Figma
上新建了一个文档,并列出了一些想法。它们都是很是简单数字时钟设计
和晦涩的单色组合
。dom
很快地,我就对这个设计感到厌倦了,因此我关闭了Figma,就去下载了Flutter Clock GitHub
(https://github.com/flutter/fl...。ide
这个库包含了2个项目,一个是基于模拟时钟的演示,另外一个是基于数字时钟的演示,我在Figma上设计都是数字的,因此天然而然地,我启动了基本的数字时钟项目。再次缺少灵感,在示例项目的帮助下,我搁置了挑战,开始去作其余事儿。函数
几天以后,在一次晨跑中,我又开始认真的思考这个挑战,一个普通的成年人天天看几回手表?对我来讲,让时钟变得有趣起来是真正的挑战。是否可使“报时间”成为自动体验?好比:即便您对时间不感兴趣,看着手表也颇有趣。这不只须要视觉上使人惊叹的设计
或新颖的动画方案
。工具
我之前从未完成过任何艺术,或者根本没有作过Flutter,因此我着手建造这样的时钟。布局
第一次迭代只是一个时钟。如上所述,我从示例数字时钟项目
开始,而不是从头开始。建立的第一个Widget是一个CustomPainter
,仅绘制了一个圆圈。很不错,可是从长远来看不是颇有趣。动画
随机性增长了,从颜色开始,而后肯定位置和大小。全部逻辑仍然在单个CustomPainter
的paint()
方法中,这几乎使动画变得不可能,所以须要将一堆逻辑重构成一个简单的粒子系统。我看了Flutter Vignettes
项目,以寻求启发。ui
这时候,制做模拟粒子时钟的想法变得更加明显了。
提出想法后,我要作的就是编写代码以实现全部目标。数学部分花费了我最多的时间才最终完成,大可能是我多年之前学过的数学,不过好多我都忘记了,角度,弧度,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.minute
和time.second
,以使时针在数小时之间平滑地动画。
而后,从弧度得到2D运动矢量就很简单了。
// Particle movement vector. p.vx = sin(-angle); p.vy = cos(-angle);
如今,p.vx
和p.vy
拥有 有关粒子在每一个动画滴答声中
应该移动多远的信息,同时保持在时针的角度里。
除了钟针外,粒子还可能做为噪音产生。而后它将从中心沿随机方向发射。在发出时,还将为全部粒子分配随机的速度
,颜色
,大小
和绘画样式
(填充或笔划)。这使时钟看起来更有趣。
时钟的早期版本中。这有四分之一标记,有些粒子有速度标记。时钟的第一个版本只是粒子,这里就不花时间截图演示了。
到如今为止,全部内容都使用单个CustomPainter
小部件(即widget,下文也同样)绘制。时钟看起来不错,可是很难告诉你时间。并且,背景是单色,看上去很无聊。
Flutter很是适合构建复杂的布局。毕竟,它是一个用于用户界面的工具包。将一堆小部件彼此堆叠,您只需将它们包装在Stack widget中。粒子时钟的最后一个场景小部件负责构建3个主要层:
Background
:一个带有CustomPaint
小部件的堆栈,该小部件绘制不一样颜色和绘画样式的随机形状,以及一个应用了模糊效果的BackdropFilter
。二、Clock Face
: 带有2个CustomPaint
小部件的堆栈,
时钟标记
-绘制时钟标记。每分钟标记,每5分钟标记具备额外的可见性。秒针
-绘制两秒的针弧。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仍能够经过小部件组合来管理布局。
时钟绘图层和覆盖层。
这是与上述相同的图片,但没有覆盖层。
我很早就想到,若是动画与时钟的滴答声同步发生,那就太酷了。最终版本中的解决方案很是简单。没有到达到想象中的目标。最初,我把它变成了一个很是复杂的问题,并尝试了各类怪异的技巧使它起做用。
@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
,咱们获得一个介于0
和1
之间的值。max
函数确保该数字保持在0.2
以上,以始终保持粒子随着每一个刻度移动。
而后,在计算粒子的新x
和y
位置时,将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
,仅包含一个颜色列表。经过颜色之间的亮度差别
对调色板
进行排序。
而后,此方法的调用者能够确保组件的第一项
和最后一项
具备足够好的对比度。
一小部分,可能有许多不一样颜色变化。请注意,就亮度而言,强调色
始终老是与背景色
最远的一种。
大部分魔术发生在编写此代码的最后几个小时。使发出的粒子从中心淡入
,而不是从无处忽然弹出
。这使总体外观更加平滑。我对弧/速度
标记进行了相同的操做,并一次将它们限制为仅几个粒子,以减小视觉复杂性。
最初,我不肯定如何避免沿钟针方向发射噪声粒子
,但知道必须这样作。在放弃寻找数学解以后,我用了一些蛮力的代码解决了这个问题(固然,数学解方案是有的,只是我没有耐心寻找到它)。
// 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/fl...