政採雲 Flutter 動態 iconfont 實踐探索

語言: CN / TW / HK

政採雲技術團隊.png

以北1.png

前言

動態修改 iconfont?有可能嗎?

目前越來越多的團隊開始使用 Flutter 來開發 App ,在 Flutter 中我們可以像前端一樣很方便地使用 iconfont 而不是圖片來顯示圖示 (icon):將 iconfont 字型檔案放到工程目錄,在 Flutter 程式碼中使用檔案裡的 icon 。可這種方式難以滿足動態修改 icon 的需求,如果產品經理突然想要更換專案裡的 icon (比如節日更換 icon) ,我們通常只能通過發版來解決,但是發版的步驟繁瑣且對老版本無效。

接下來讓我們一起來探索一套基於 Flutter 的 iconfont 動態載入方案。

iconfont 原理

iconfont 即“字型圖示”,它是將圖示做成字型檔案,然後通過指定不同的字元而顯示不同的icon。在字型檔案中,每一個字元都對應一個 Unicode 編碼,而每一個 Unicode 碼對應一個顯示字形,不同的字型就是指字形不同,即字元對應的字形是不同的。而在 iconfont 中,只是將 Unicode 碼對應的字形做成了圖示,所以不同的字元最終就會渲染成不同的圖示。

在Flutter開發中,iconfont 和圖片相比有如下優勢:

  • 體積小:可以減小安裝包大小。
  • 向量的:iconfont 都是向量圖示,放大不會影響其清晰度
  • 可以應用文字樣式:可以像文字一樣改變字型圖示的顏色、大小對齊等。 正是有了上述優勢,所以我們會在Flutter專案中優先考慮使用 iconfont,而不是圖片。

iconfont 動態化之前的使用方式

plain 在我們現有的 Flutter 專案中,關於 iconfont 的使用,都是通過 [icontfont 官網](https://www.iconfont.cn)下載 ttf 字型檔案至專案中的 assets 資料夾下,然後在 pubsepc.yaml 檔案中配置來實現 ttf 字型檔案的靜態載入。 fonts: - family: IconFont fonts: - asset: assets/fonts/iconfont.ttf

然後定義一個類( ZcyIcons )來管理iconfont檔案中的所有 IconData:

可以通過編寫指令碼自動生成這個類的程式碼,這樣每次更新iconfont檔案後只需要執行一下指令碼即可生成最新的程式碼。 ```dart class _MyIcon { static const font_name = 'iconfont'; static const package_name = 'flutter_common'; const _MyIcon(int codePoint) : super(codePoint, fontFamily: font_name, fontPackage: package_name,); }

class ZcyIcons { static const IconData tongzhi = const MyIcon(0xe784);   static Map _map = Map();   ZcyIcons.();

static IconData from(String name) {     if(_map.isEmpty) {       initialization();     }     return _map[name];   }

static void initialization() { _map["tongzhi"] = tongzhi; } }

```

在使用的時候,有兩種呼叫方式

dart /// 方法1:直接載入 Icon(ZcyIcons.arrow) /// 方法2:通過name的值去取map中對應的IconData Icon(ZcyIcons.from(name)) 雖然第二種方法能通過改變 key 的值來動態的從 map 中載入對應的 IconData ,但是僅侷限於所有的 IconData 都已經在 map 中配置好且不再更改。

既然 iconfont 是字型檔案,那麼如果系統能動態載入字型檔案,那麼一定也能用同樣的方式去動態載入 iconfont。

iconfont 動態化方案

步驟1: 載入遠端下發的 ttf 檔案

Flutter SDK 提供了 FontLoader 類來實現字型的動態載入。而我們解決這個問題的核心就是這個 FontLoader 類。

它有一個 addFont 方法,支援將 ByteData 格式資料轉化為字型包並載入到應用字型資源庫:

```dart class FontLoader{ ...

void addFont(Future bytes) { if (_loaded) throw StateError('FontLoader is already loaded'); _fontFutures.add(bytes.then( (ByteData data) => Uint8List.view(data.buffer, data.offsetInBytes, data.lengthInBytes) )); }

Future load() async { if (_loaded) throw StateError('FontLoader is already loaded'); _loaded = true; final Iterable> loadFutures = _fontFutures.map( (Future f) => f.then( (Uint8List list) => loadFont(list, family) ) ); return Future.wait(loadFutures.toList()); } } ```

我們可以建立一個介面來下發 iconfont 字型檔案遠端地址及該檔案的 hash 值,每次啟動 APP 將本地字型檔案的 hash 值與介面中的值對比,當存在差異時將遠端的字型檔案下載到本地並以 ByteData 的資料格式供 FontLoader 載入即可。附上部分關鍵程式碼:

```dart /// 下載遠端的字型檔案 static Future httpFetchFontAndSaveToDevice(Uri fontUri) { return () async { http.Response response; try { response = await _httpClient.get(uri); } catch (e) { throw Exception('Failed to get font with url: ${fontUrl.path}'); } if (response.statusCode == 200) { return ByteData.view(response.bodyBytes.buffer); } else { /// 如果執行失敗, 丟擲異常. throw Exception('Failed to download font with url: ${fontUrl.path}'); } }; }

/// 載入字型,先從本地檔案載入,如果不存在,則使用[loader]載入 static Future loadFontIfNecessary(ByteData loader, String fontFamilyToLoad) async { assert(fontFamilyToLoad != null && loader != null);

if (_loadedFonts.contains(fontFamilyToLoad)) { return; } else { _loadedFonts.add(fontFamilyToLoad); }

try { Future byteData; byteData = file_io.loadFontFromDeviceFileSystem(fontFamilyToLoad); if (await byteData != null) { return _loadFontByteData(fontFamilyToLoad, byteData); }

byteData = loader();
if (await byteData != null) {
  /// 通過 FontLoader 載入下載好的字型檔案
  final fontLoader = FontLoader(familyWithVariantString);
  fontLoader.addFont(byteData);
  await fontLoader.load();
  successLoadedFonts.add(familyWithVariantString);
}

} catch (e) { _loadedFonts.remove(fontFamilyToLoad); print('Error: unable to load font $fontFamilyToLoad because the following exception occured:\n$e'); } } ```

步驟2: 通過 icon 的名稱獲取需要載入的 unicode 值

在實際使用時我們發現需要指定 icon 對應字型檔案的 codePoint ,也就是 unicode 值:

程式碼中通過iconfont 的 unicde 值獲取 icon 的用法如下:

plain /// StringToInt 方法是定義的將 "" 從 String 型別的16進位制值轉為 int 型別方法 MyIcons.from(StringToInt(''));

這樣的用法對於我們開發來說不是很友好,每次都需要去查詢這個 unicde 值對應的是哪個圖示,因此我們可以在之前下載 ttf 檔案的介面建立一個對映關係表,然後在 iconfont 初始化的時候通過程式碼將動態下發的 icon 名稱和 Unicode 進行關聯。

介面返回資料格式:

更改介面格式後代碼中 icon 的用法:

plain /// _aliasMap 是將介面下發的nameList儲存起來的 Map MyIcons.from(StringToInt(_aliasMap['tongzhi']);

假設我們有這麼一個場景:APP進入首頁,下載最新的 iconfont.ttf 檔案並載入,但是Icon已經載入完成,此時怎麼做到動態重新整理當前Icon裡面的內容呢?

步驟3:動態載入非同步優化

之前的步驟已經可以完成 APP 啟動後本地字型檔案的更新,但是無法解決 icon 已經載入完成後的資料更新,因此我們的動態化方案需要依賴於 FutureBuilder。

FutureBuilder 會依賴一個 Future,它會根據所依賴的 Future 的狀態來動態構建自身。

我們可以擴充套件一個 Icon 的 dynamic 方法去返回一個依賴於 FutureBuilder 的 Icon,當我們的 iconfont 字型檔案更新成功後讓 FutureBuilder 強制去重新整理這個 Icon。

主要程式碼如下:

```plain /// Icon的擴充套件方法,主要實現Icon元件的動態重新整理 /// [dynamic] 方法主要通過[FutureBuilder]實現動態載入的核心原理 extension DynamicIconExtension on Icon { /// 用來監聽新icon字型載入成功後的回撥及時重新整理icon, Widget get dynamic { /// 沒有使用動態iconfont的情況下直接返回 if (this.icon is! DynamicIconDataMixin) return this; final mix = this.icon as DynamicIconDataMixin; final loadedKey = UniqueKey(); return FutureBuilder( future: mix.dynamicIconFont.loadedAsync, builder: (context, snapshot) { /// 由於icon的配置未發生變化但實際上其使用的字型已經發生了變化,所以這裡通過使用不同的key讓其強制重新整理 return KeyedSubtree( key: snapshot.hasData ? loadedKey : null, child: this, ); }, ); } } 

/// 呼叫程式碼如下: Icon(MyIcons.from('')).dynamic ```

至此,我們的動態化方案支援的能力如下:

  • 可動態修改專案中已有的 icon
  • 通過 name/code 的形式動態設定 icon
  • 可在專案中使用新增的 icon 整個方案的流程圖如下:

總結

總體來說,整個方案的核心原理就是通過 FontLoader 來實現字型檔案的動態載入。但是其中涉及到一些動態化的處理和 iconfont 的原理探究,涉及到多點多面的知識,需要融會貫通並組合在一起使用。

參考資料

Flutter中文網

推薦閱讀

政採雲Flutter低成本螢幕適配方案探索

Redis系列之Bitmaps

MySQL 之 InnoDB 鎖系統原始碼分析

招賢納士

政採雲技術團隊(Zero),一個富有激情、創造力和執行力的團隊,Base 在風景如畫的杭州。團隊現有300多名研發小夥伴,既有來自阿里、華為、網易的“老”兵,也有來自浙大、中科大、杭電等校的新人。團隊在日常業務開發之外,還分別在雲原生、區塊鏈、人工智慧、低程式碼平臺、中介軟體、大資料、物料體系、工程平臺、效能體驗、視覺化等領域進行技術探索和實踐,推動並落地了一系列的內部技術產品,持續探索技術的新邊界。此外,團隊還紛紛投身社群建設,目前已經是 google flutter、scikit-learn、Apache Dubbo、Apache Rocketmq、Apache Pulsar、CNCF Dapr、Apache DolphinScheduler、alibaba Seata 等眾多優秀開源社群的貢獻者。如果你想改變一直被事折騰,希望開始折騰事;如果你想改變一直被告誡需要多些想法,卻無從破局;如果你想改變你有能力去做成那個結果,卻不需要你;如果你想改變你想做成的事需要一個團隊去支撐,但沒你帶人的位置;如果你想改變本來悟性不錯,但總是有那一層窗戶紙的模糊……如果你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的自己。如果你希望參與到隨著業務騰飛的過程,親手推動一個有著深入的業務理解、完善的技術體系、技術創造價值、影響力外溢的技術團隊的成長過程,我覺得我們該聊聊。任何時間,等著你寫點什麼,發給 [email protected]

微信公眾號

文章同步釋出,政採雲技術團隊公眾號,歡迎關注

政採雲技術團隊.png