共计 5288 个字符,预计需要花费 14 分钟才能阅读完成。
本文首发于公众号:技术最 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
小部件,用于绘制所有粒子。
@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…
原文链接:
https://ultimatemachine.se/ar…