Flutter -如何建立炫酷粒子時鐘效果!

本文首發於公衆號:技術最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上設計都是數字的,因此天然而然地,我啓動了基本的數字時鐘項目。再次缺少靈感,在示例項目的幫助下,我擱置了挑戰,開始去作其餘事兒。函數

幾天以後,在一次晨跑中,我又開始認真的思考這個挑戰,一個普通的成年人天天看幾回手錶?對我來講,讓時鐘變得有趣起來是真正的挑戰。是否可使「報時間」成爲自動體驗?好比:即便您對時間不感興趣,看着手錶也頗有趣。這不只須要視覺上使人驚歎的設計新穎的動畫方案工具

  • 一、若是每次您看鐘時都有不一樣的外觀會怎樣?
  • 二、咱們可否激發好奇心,使您渴望下一次設計迭代?或者,當您喜歡的設計永久消失時,您會感到難過嗎?
  • 三、咱們是否能夠將背景形狀、顏色、動畫隨機改變以使它:a. 看起來很酷,b. 足夠知足1和2的隨機性,c. 不會分散你看時間的注意力?

我之前從未完成過任何藝術,或者根本沒有作過Flutter,因此我着手建造這樣的時鐘。佈局

粒子與隨機性

第一次迭代只是一個時鐘。如上所述,我從示例數字時鐘項目開始,而不是從頭開始。建立的第一個Widget是一個CustomPainter,僅繪製了一個圓圈。很不錯,可是從長遠來看不是頗有趣。動畫

隨機性增長了,從顏色開始,而後肯定位置和大小。全部邏輯仍然在單個CustomPainterpaint()方法中,這幾乎使動畫變得不可能,所以須要將一堆邏輯重構成一個簡單的粒子系統。我看了Flutter Vignettes項目,以尋求啓發。ui

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.minutetime.second,以使時針在數小時之間平滑地動畫。

而後,從弧度得到2D運動矢量就很簡單了。

// Particle movement vector.
p.vx = sin(-angle);
p.vy = cos(-angle);

如今,p.vxp.vy擁有 有關粒子在每一個動畫滴答聲中應該移動多遠的信息,同時保持在時針的角度裏。

除了鍾針外,粒子還可能做爲噪音產生。而後它將從中心沿隨機方向發射。在發出時,還將爲全部粒子分配隨機的速度顏色大小繪畫樣式(填充或筆劃)。這使時鐘看起來更有趣。

時鐘的早期版本中。這有四分之一標記,有些粒子有速度標記。時鐘的第一個版本只是粒子,這裏就不花時間截圖演示了。

使用Flutter Widgets 添加圖層

到如今爲止,全部內容都使用單個CustomPainter小部件(即widget,下文也同樣)繪製。時鐘看起來不錯,可是很難告訴你時間。並且,背景是單色,看上去很無聊。

Flutter很是適合構建複雜的佈局。畢竟,它是一個用於用戶界面的工具包。將一堆小部件彼此堆疊,您只需將它們包裝在Stack widget中。粒子時鐘的最後一個場景小部件負責構建3個主要層:

  • 一、Background:一個帶有CustomPaint小部件的堆棧,該小部件繪製不一樣顏色和繪畫樣式的隨機形狀,以及一個應用了模糊效果的BackdropFilter
  • 二、Clock Face: 帶有2個CustomPaint 小部件的堆棧,

    • a. 時鐘標記-繪製時鐘標記。每分鐘標記,每5分鐘標記具備額外的可見性。
    • b. 秒針-繪製兩秒的針弧。
  • 三、 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,咱們獲得一個介於01之間的值。max函數確保該數字保持在0.2以上,以始終保持粒子隨着每一個刻度移動。

而後,在計算粒子的新xy位置時,將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...

相關文章
相關標籤/搜索