由點匯聚成字的動效炫極了

語言: CN / TW / HK

theme: v-green highlight: atom-one-dark


我正在參與掘金技術社群創作者簽約計劃招募活動,點選連結報名投稿

前言

在引入 fl_chart 繪製圖表的時候,看到外掛有下面這樣的動效,隨機散亂的圓點最後組合成了 Flutter 的 Logo,挺酷炫的。本篇我們來探討類似的效果怎麼實現。

logo 動畫.gif

點陣

在講解程式碼實現之前,我們先科普一個知識,即點陣。點陣在日常生活中很常見,比如廣告屏,停車系統的顯示,行業內稱之為 LED 顯示屏。

image.png

LED 顯示屏實際上就是由很多 LED 燈組合成的一個顯示面板,然後通過顯示驅動某些燈亮,某些燈滅就可以實現文字、圖形的顯示。LED 顯示屏的點距足夠小時,色彩足夠豐富時其實就形成了我們日常的顯示屏,比如 OLED 顯示屏其實原理也是類似的。之前報道過的大學宿舍樓通過控制每個房間的燈亮燈滅來形成文字的原理也是一樣的。

image.png

現在來看看 LED顯示文字是怎麼回事,比如我們要 顯示島上碼農的“島”字,在16x16的點陣上,通過排布得到的就是下面的結果(不同字型的排佈會有些差別)。

因為每一行是16個點,我們可以對應為16位二進位制數,把黑色的標記為1,灰色的標記為0,每一行就可以得到一個二進位制數。比如上面的第一行第8列為1,其他都是0,對應的二進位制數就是0000000100000000,對應的16進位制數就是0x0100。把其他行也按這種方式計算出來,最終得到的“島”字對應的是16個16進位制數,如下所示。 dart [ 0x0100, 0x0200, 0x1FF0, 0x1010, 0x1210, 0x1150, 0x1020, 0x1000, 0x1FFC, 0x0204, 0x2224, 0x2224, 0x3FE4, 0x0004, 0x0028, 0x0010 ]; 又了這個基礎,我們就可以用 Flutter 繪製點陣圖形。

點陣圖形繪製

首先我們繪製一個“LED 面板”,也就是繪製一個有若干個點構成的矩陣,這個比較簡單,保持相同的間距,逐行繪製相同的圓即可,比如我們繪製一個16x16的點陣,實現程式碼如下所示。 dart var paint = Paint()..color = Colors.grey; final dotCount = 16; final fontSize = 100.0; var radius = fontSize / dotCount; var startPos = Offset(size.width / 2 - fontSize, size.height / 2 - 2 * fontSize); for (int i = 0; i < dotCount; ++i) { var position = startPos + Offset(0.0, radius * i * 2); for (int j = 0; j < dotCount; ++j) { var dotPosition = startPos + Offset(radius * 2 * j, position.dy); canvas.drawCircle(dotPosition, radius, paint); } } 繪製出來的效果如下:

image.png

接下來是點亮對應的位置來繪製文字了。上面我們講過了,每一行是一個16進位制數,那麼我們只需要判斷每一行的16進位制數的第幾個 bit是1就可以了,如果是1就點亮,否則不點亮。點亮的效果用不同的顏色就可以了。 怎麼判斷16進位制數的第幾個 bit 是不是1呢,這個就要用到位運算技巧了。實際上,我們可以用一個第 N 個 bit 是1,其他 bit 都是0的數與要判斷的數進行“位與”運算,如果結果不為0,說明要判斷的數的第 N 個 bit 是1,否則就是0。聽著有點繞,看個例子,我們以0x0100為例,按從第0位到第15位逐個判斷第0位和第15位是不是1,程式碼如下: dart for (i = 0 ; i < 16; ++i) { if ((0x0100 & (1 << i)) > 0) { // 第 i 位為1 } } 這裡有兩個位操作,1 << i是將1左移 i 位,為什麼是這樣呢,因為這樣可以構成0x0001,0x0002,0x0004,...,0x8000等數字,這些數字依次從第0位,第1位,第2位,...,第15位為1,其他位都是0。然後我們用這樣的數與另外一個數做位與運算時,就可以依次判斷這個數的第0位,第1位,第2位,...,第15位是否為1了,下面是一個計算示例,第11位為1,其他位都是0,從而可以 判斷另一個數的第11位是不是0。

位與運算

通過這樣的邏輯我們就可以判斷一行的 LED 中第幾列應該點亮,然後實現文字的“顯示”了,實現程式碼如下。wordHex是對應字的16個16進位制數的陣列。dotCount的值是16,用於控制繪製16x16大小的點陣。每隔一行我們向下移動一段直徑距離,每隔一列,我們向右移動一段直徑距離。然後如果當前繪製位置的數值對應的 bit位為1,就用藍色繪製,否則就用灰色繪製。這裡說一下為什麼左移的時候要用dotCount - j - 1,這是因為繪製是從左到右的,而16進位制數的左邊是高位,而數字j是從小到大遞增的,因此要通過這種方式保證判斷的順序是從高位(第15位)到低位(第0位),和繪製的順序保持一致。 ```dart for (int i = 0; i < dotCount; ++i) { var position = startPos + Offset(0.0, radius * i * 2); for (int j = 0; j < dotCount; ++j) { var dotPosition = startPos + Offset(radius * 2 * j, position.dy);

if ((wordHex[i] & ((1 << dotCount - j - 1))) != 0) {
  paint.color = Colors.blue[600]!;
  canvas.drawCircle(dotPosition, radius, paint);
} else {
  paint.color = Colors.grey;
  canvas.drawCircle(dotPosition, radius, paint);
}

} } ``` 繪製的結果如下所示。

image.png

由點聚整合字的動畫實現

接下來我們來考慮如何實現開篇說的類似的動畫效果。實際上方法也很簡單,就是先按照文字應該“點亮”的 LED 的數量,先在隨機的位置繪製這麼多數量的 LED,然後通過動畫控制這些 LED 移動到目標位置——也就是文字本該繪製的位置。這個移動的計算公式如下,其中 t 是動畫值,取值範圍為0-1.

移動公式

需要注意的是,隨機點不能在繪圖過程生成,那樣會導致每次繪製產生新的隨機位置,也就是初始位置會變化,導致上面的公式實際不成立,就達不到預期的效果。另外,也不能在 build 方法中生成,因為每次重新整理 build 方法就會被呼叫,同樣會導致初始位置發生變化。所以,生成隨機位置應該在 initState方法完成。但是又遇到一個新問題,那就是 initState方法裡沒有 context,拿不到螢幕寬高,所以不能直接生成位置,我們只需要生成一個0-1的隨機係數就可以了,然後在繪製的時候在乘以螢幕寬高就得到實際的初始位置了。初始位置係數生成程式碼如下: dart @override void initState() { super.initState(); var wordBitCount = 0; for (var hex in dao) { wordBitCount += _countBitOne(hex); } startPositions = List.generate(wordBitCount, (index) { return Offset( Random().nextDouble(), Random().nextDouble(), ); }); ... } wordBitCount是計算一個字中有多少 bit 是1的,以便知道要繪製的 “LED” 數量。接下來是繪製程式碼了,我們這次對於不亮的直接不繪製,然後要點亮的位置通過上面的位置計算公式計算,這樣保證了一開始繪製的是隨機位置,隨著動畫的過程,逐步移動到目標位置,最終匯聚成一個字,就實現了預期的動畫效果,程式碼如下。 ```dart void paint(Canvas canvas, Size size) { final dotCount = 16; final fontSize = 100.0; var radius = fontSize / dotCount; var startPos = Offset(size.width / 2 - fontSize, size.height / 2 - fontSize); var paint = Paint()..color = Colors.blue[600]!;

var paintIndex = 0; for (int i = 0; i < dotCount; ++i) { var position = startPos + Offset(0.0, radius * i * 2); for (int j = 0; j < dotCount; ++j) { // 判斷第 i 行第幾位不為0,不為0則繪製,否則不繪製 if ((wordHex[i] & ((1 << dotCount - j))) != 0) { var startX = startPositions[paintIndex].dx * size.width; var startY = startPositions[paintIndex].dy * size.height; var endX = startPos.dx + radius * j * 2; var endY = position.dy; var animationPos = Offset(startX + (endX - startX) * animationValue, startY + (endY - startY) * animationValue); canvas.drawCircle(animationPos, radius, paint); paintIndex++; } } } } `` 來看看實現效果吧,是不是很酷炫?完整原始碼已提交至:[繪圖相關原始碼](http://gitee.com/island-coder/flutter-beginner/tree/master/custom_paint),檔名為:dot_font.dart`。

點陣匯聚文字動畫.gif

總結

本篇介紹了點陣的概念,以及基於點陣如何繪製文字、圖形,最後通過先繪製隨機點,再匯聚成文字的動畫效果。可以看到,化整為零,再聚零為整的動畫效果還是蠻酷炫的。實際上,基於這種方式,可以構建更多有趣的動畫效果。