使用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: [
_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 palettes, bool dark) {
Palette result;
while (result == null) {
Palette palette = Rnd.getItem(palettes);
List 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 a, List b) {
return a[1].compareTo(b[1]);
});
List 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上对我表示支持的人
!
结语
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
再附一部分Android架构面试视频讲解:
的程序员,所以当Martin向我伸手时,我真的很惊讶!我现在仍然不敢相信自己赢了。
感谢Google和Flutter团队使这一挑战得以实现,也感谢所有为我加油并在Twitter上对我表示支持的人
!
结语
由于篇幅限制,文档的详解资料太全面,细节内容太多,所以只把部分知识点截图出来粗略的介绍,每个小节点里面都有更细化的内容!以下是目录截图:
[外链图片转存中…(img-Ny7ez7n8-1718870977082)]
由于整个文档比较全面,内容比较多,篇幅不允许,下面以截图方式展示 。
再附一部分Android架构面试视频讲解:
[外链图片转存中…(img-iYDIQ1wb-1718870977083)]