本文首發於公衆號:技術最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...