本文首发于公众号:技术最TOP
周末发表了一篇文章《这个我的项目也太屌了吧》,给大家举荐了一个炫酷的Flutter粒子时钟我的项目,不过没有将具体实现思路和代码,所幸,作者本人写了一篇博客将这个我的项目的背景、实现思路、和所遇到的问题,我感觉对十分有用,因而翻译进去,整顿给大家!原文题目《我是如何创立粒子时钟,并博得了#FlutterClock挑战的》
。
背景
Google在2019年11月18日发动了The Flutter Clock Challenge
挑战流动,内容很简略:应用Flutter UI工具包设计时钟
。Google专家小组将依据四个次要规范对参赛作品进行评判:视觉美感
,创意新颖性
,代码品质
和整体执行力
。
在这之前,我只应用过Flutter一两次,这对于我来说是一个潜在的机会。
最后的想法
挑战开始了大概两周后,我才有了一些想法,但还没有编写任何代码,解决此类问题时,我通常的办法是首先寻找现有的解决方案来找灵感。但这次不行。相同,我在Figma
上新建了一个文档,并列出了一些想法。它们都是非常简单数字时钟设计
和艰涩的单色组合
。
很快地,我就对这个设计感到厌倦了,所以我敞开了Figma,就去下载了Flutter Clock GitHub
(https://github.com/flutter/fl...。
这个库蕴含了2个我的项目,一个是基于模仿时钟的演示,另一个是基于数字时钟的演示,我在Figma上设计都是数字的,所以自然而然地,我启动了根本的数字时钟我的项目。再次不足灵感,在示例我的项目的帮忙下,我搁置了挑战,开始去做其余事儿。
几天之后,在一次晨跑中,我又开始认真的思考这个挑战,一个一般的成年人每天看几次手表?对我来说,让时钟变得乏味起来是真正的挑战。是否能够使“报工夫”成为主动体验?比方:即便您对工夫不感兴趣,看着手表也很乏味。这不仅须要视觉上令人惊叹的设计
或新鲜的动画计划
。
- 1、如果每次您看钟时都有不同的外观会怎么?
- 2、咱们是否激发好奇心,使您渴望下一次设计迭代?或者,当您喜爱的设计永恒隐没时,您会感到惆怅吗?
- 3、咱们是否能够将背景形态、色彩、动画随机扭转以使它:a. 看起来很酷,b. 足够满足1和2的随机性,c. 不会扩散你看工夫的注意力?
我以前从未实现过任何艺术,或者基本没有做过Flutter,所以我着手建造这样的时钟。
粒子与随机性
第一次迭代只是一个时钟。如上所述,我从示例数字时钟我的项目
开始,而不是从头开始。创立的第一个Widget是一个CustomPainter
,仅绘制了一个圆圈。很不错,然而从久远来看不是很乏味。
随机性减少了,从色彩开始,而后确定地位和大小。所有逻辑依然在单个CustomPainter
的paint()
办法中,这简直使动画变得不可能,因而须要将一堆逻辑重形成一个简略的粒子系统。我看了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.minute
和time.second
,以使时针在数小时之间平滑地动画。
而后,从弧度取得2D静止矢量就很简略了。
// Particle movement vector.p.vx = sin(-angle);p.vy = cos(-angle);
当初,p.vx
和p.vy
领有 无关粒子在每个动画滴答声中
应该挪动多远的信息,同时放弃在时针的角度里。
除了钟针外,粒子还可能作为乐音产生。而后它将从核心沿随机方向发射。在收回时,还将为所有粒子调配随机的速度
,色彩
,大小
和绘画款式
(填充或笔划)。这使时钟看起来更乏味。
时钟的晚期版本中。这有四分之一标记,有些粒子有速度标记。时钟的第一个版本只是粒子,这里就不花工夫截图演示了。
应用Flutter Widgets 增加图层
到当初为止,所有内容都应用单个CustomPainter
小部件(即widget,下文也一样)绘制。时钟看起来不错,然而很难通知你工夫。而且,背景是单色,看上去很无聊。
Flutter非常适合构建简单的布局。毕竟,它是一个用于用户界面的工具包。将一堆小部件彼此重叠,您只需将它们包装在Stack widget中。粒子时钟的最初一个场景小部件负责构建3个次要层:
- 1、
Background
:一个带有CustomPaint
小部件的堆栈,该小部件绘制不同色彩和绘画款式的随机形态,以及一个利用了含糊成果的BackdropFilter
。 2、
Clock Face
: 带有2个CustomPaint
小部件的堆栈,- a.
时钟标记
-绘制时钟标记。每分钟标记,每5分钟标记具备额定的可见性。 - b.
秒针
-绘制两秒的针弧。
- a.
- 3、
Particle FX
: 一个CustomPaint
小部件,用于绘制所有粒子。
@overrideWidget 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仍能够通过小部件组合来治理布局。
时钟绘图层和覆盖层。
这是与上述雷同的图片,但没有覆盖层。
与工夫同步的动画
我很早就想到,如果动画与时钟的滴答声同步产生,那就太酷了。最终版本中的解决方案非常简单。没有到达到设想中的指标。最后,我把它变成了一个非常复杂的问题,并尝试了各种怪异的技巧使它起作用。
@overridevoid 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...
原文链接:
https://ultimatemachine.se/ar...