天天看點

Flutter 小技巧之玩轉字型渲染和問題修複

這次的 Flutter 小技巧是字型渲染,雖然是小技巧但是内容略長,可能大家在日常開發中不會特别關心字型相關的部分,而這将是一篇你平時可能用不到 ,但是遇到問題就會翻出來的文章。

本篇将快速普及一些字型渲染相關的基礎,解決一些因為字型而導緻的異常問題,并穿插一些實用小技巧,内容篇幅可能略長,建議先 Mark 後看。

一、字型庫

首先,問一個我經常問的面試題:Flutter 在 Android 和 iOS 上使用了哪些字型?

如果你恰好看過 ​

​typography.dart​

​ 的源碼和解釋,你可以會有初步結論:

  • Android 上使用的是​

    ​Roboto​

    ​ 字型;
  • iOS 上使用的是​

    ​.SF UI Display​

    ​​ 或者​

    ​.SF UI Text​

    ​ 字型;
Flutter 小技巧之玩轉字型渲染和問題修複

但是,如果你再進一步去了解就會發現,在加上中文顯示之後,結論應該是:

  • 預設在 iOS 上:
  • 中文字型:​

    ​PingFang SC​

    ​​ (繁體還有​

    ​PingFang TC​

    ​​ 、​

    ​PingFang HK​

    ​ )
  • 英文字型:​

    ​.SF UI Text​

    ​​ /​

    ​.SF UI Display​

  • 預設在 Android 上:
  • 中文字型:​

    ​Source Han Sans​

    ​​ /​

    ​Noto​

  • 英文字型:​

    ​Roboto​

那這時候你可能會問:.SF 沒有中文,那可以使用 ​

​PingFang​

​ 顯示英文嗎? 答案是可以的,但是字形和字重會有微妙差別, 例如下圖裡的 G 就有很明顯的不同。

Flutter 小技巧之玩轉字型渲染和問題修複

那如果加上韓文呢?這時候 iOS 上的 ​

​PingFang​

​​ 和 ​

​.SF​

​​ 就不夠用了,需要調用如 ​

​Apple SD Gothic Neo​

​ 這樣的超集字型庫,而說到這裡就需要介紹一個 Flutter 上你可能會遇到的 Bug。

如下圖所示,當在使用 ​

​Apple SD Gothic Neo​

​ 字型出現中文和韓文同時顯示時,你可能會察覺一些字形很奇怪,比如【推廣】這兩個字,其中【廣】這個字元在超集上是不存在的,是以會變成了中文的【廣】,但是【推】字用的還是超集裡的字形。

Flutter 小技巧之玩轉字型渲染和問題修複

這種情況下,最終渲染的結果會如下圖所示,解決的思路也很簡單,小技巧就是給 ​

​TextStyle​

​ 或者 ​

​Theme​

​ 的 ​

​fontFamilyFallback​

​ 配置上 ​

​["PingFang SC" , "Heiti SC"]​

​ 。

Flutter 小技巧之玩轉字型渲染和問題修複

另外,如果你還對英文下 ​

​.SF UI Display​

​ 和 ``SF UI Text` 之間的關系困惑的話,那其實你不用太過糾結,因為從 SF 設計上大概意思上了解的話:

.SF Text 适用于更小的字型;.SF Display 則适用于偏大的字型,分水嶺大概是 20pt 左右,不過 SF(San Francisco) 屬于動态字型,系統會動态比對。

二、Flutter Text

雖然上面介紹字型的一些相關内容,但是在 Flutter 上和原生還是有一些差異,在 Flutter 中的文本呈現邏輯是有分層的,其中:

  • 衍生自 Minikin 的 libtxt 庫用于字型選擇,分隔行等;
  • HartBuzz 用于字形選擇和成型;
  • Skia作為 渲染 / GPU後端;
  • 在 Android / Fuchsia 上使用 FreeType 渲染,在 iOS 上使用CoreGraphics 來渲染字型。

Text Height

那如果這時候我問你一個問題: 一個 ​

​ fontSize: 100​

​ 的 H 字母需要占據多大的高度 ?你會回答多少?

首先,我們用一個 100 的紅色 ​

​Container​

​​ 和 ​

​ fontSize: 100​

​ 的 H 文本做個對比,可以看到 H 文本所在的藍色區域其實是需要大于 100 的紅色區域的。

Flutter 小技巧之玩轉字型渲染和問題修複

事實上,前面的藍色區域是字型的行高,也就是 line height,關于這個行高,首先需要解釋的就是 ​

​TextStyle​

​​ 中的 ​

​height​

​ 參數。

預設情況下 ​

​height​

​​ 參數是 ​

​null​

​​,當我們把它設定為 ​

​1​

​ 之後,如下圖所示,可以看到藍色區域的高度和紅色小方塊對齊,變成了 100 的高度,也就是行高變成了 100 ,而 H 字母完整地顯示在了藍色區域内。

Flutter 小技巧之玩轉字型渲染和問題修複

那 ​

​height​

​​ 是什麼呢?首先 ​

​TextStyle​

​​ 中的 ​

​height​

​​ 參數值在設定後,其效果值是 ​

​fontSize​

​ 的倍數:

  • 當​

    ​height​

    ​ 為空時,行高預設是使用字型的量度(這個量度後面會有解釋);
  • 當​

    ​height​

    ​​ 不是空時,行高為​

    ​height​

    ​​ *​

    ​fontSize​

    ​ 的大小;

如下圖所示,藍色區域和紅色區域的對比就是 ​

​height​

​​ 為 ​

​null​

​​ 和 ​

​1​

​ 的對比高度。

Flutter 小技巧之玩轉字型渲染和問題修複

是以,看到這裡你又知道了一個小技巧:當文字在 ​

​Container​

​ “有限高度” 内容内無法居中時,可以考慮調整 ​

​TextStyle​

​ 中的 ​

​height​

​ 來實作 。

Flutter 小技巧之玩轉字型渲染和問題修複
當然,這時候如果你把 ​

​Container​

​​ 的 ​

​height:50​

​ 去掉,又會是另外一個效果。

是以 height 參數和文本渲染的高度之間是成倍數關系,具體如下圖所示,同時最需要注意的點就是:文本内容在 height 裡并不是居中,這裡的 height 可以類比于調整行高。

Flutter 小技巧之玩轉字型渲染和問題修複

另外,文本中的除了 ​

​TextStyle​

​ 下的 ​

​height​

​ 之外,還是有 ​

​StrutStyle​

​ 參數下的 ​

​height​

​ ,它影響的是字型的整體量度,也就是如下圖所示,影響的是 ascent - descent 的高度。

Flutter 小技巧之玩轉字型渲染和問題修複

那你說它和 ​

​TextStyle​

​ 下的 ​

​height​

​ 有什麼差別? 如下圖所示例子:

  • ​StrutStyle​

    ​​ 的​

    ​froceStrutHeight​

    ​​ 開啟後,​

    ​TextStyle​

    ​​ 的​

    ​height​

    ​ 不會生效;
  • ​StrutStyle​

    ​​ 設定​

    ​fontSize:50​

    ​​ 影響的内容和​

    ​TextStyle​

    ​​ 的​

    ​fontSize:100​

    ​ 影響的内容不一樣;
Flutter 小技巧之玩轉字型渲染和問題修複

另外在 ​

​StrutStyle​

​​ 裡還有一個叫 ​

​leading​

​​ 的 參數,加上了 ​

​leading​

​​ 後才是 Flutter 中對字型行高完全的控制組合,​

​leading​

​​ 預設為 ​

​null​

​​ ,同時它的效果也是 ​

​fontSize​

​ 的倍數,并且分布是上下均分。

Flutter 小技巧之玩轉字型渲染和問題修複

是以,看到這裡你又知道了一個小技巧:設定 ​

​leading​

​ 可以均分高度,是以如下圖所示,也可以用于調整行間距。

Flutter 小技巧之玩轉字型渲染和問題修複
更多行高相關可見 :​​《深入了解 Flutter 中的字型“冷”知識》​​

FontWeight

另外一個關于字型的知識點就是 ​

​FontWeight​

​​ ,相信大家對 ​

​FontWeight​

​​ 不會陌生,比如我們預設的 normal 是 w400,而常用的 bold 是 w700 ,整個 ​

​FontWeight​

​ 清單覆寫 100-900 的數值。

Flutter 小技巧之玩轉字型渲染和問題修複

那麼這裡又有個問題:這些 Weight 在字型裡都能找到對應的粗細嗎?

答案是不行的,因為正常情況下如下圖所示 ,有些字型庫在某些 Weight 下是沒有對應支援,例如

  • Roboto 沒有 w600
  • PingFang 沒有高于 w600
Flutter 小技巧之玩轉字型渲染和問題修複

那你可能好奇,為什麼這裡要特意介紹 FontWeight ?因為在 Flutter 3.0 目前它對中文有 Bug!

從下面這張圖你可以看到,在 Flutter 3.0 上中文從 100-500 的字重顯示是不正常的,肉眼可以看出在 100 - 500 都顯示同一個字重。

Flutter 小技巧之玩轉字型渲染和問題修複
這個 Bug 來自于當 ​

​SkParagraph​

​​ 調用 ​

​onMatchFamilyStyleCharacter​

​​ 時,​

​onMatchFamilyStyleCharacter​

​​ 的實作沒有選擇最接近 ​

​TextStyle​

​​ 的字型,是以在 ​

​CTFontCreateWithFontDescriptor​

​​ 時會帶上 weight 參數但是卻沒有 ​

​familyName​

​ ,是以 CTFontCreateWithFontDescriptor` 函數就會傳回 Helvetica 字型的預設 weight。

臨時解決小技巧也很簡單:全局設定 ​

​fontFamilyFallback: ["PingFang SC"]​

​ 或者 ​

​fontFamily: 'PingFang SC'​

​ 就可以解決,又是 Fallback , 這時候你就會發現,前面介紹的字型常識,可以在這裡快速被利用起來。

Flutter 小技巧之玩轉字型渲染和問題修複
因為 iOS 上中文就是 ​

​PingFang SC​

​​ ,隻要 Fallback 回 PingFang 就可以正常渲染,而這個問題在 Android 模拟器、iOS 真機、Mac 上等會出現,但是 Android 真機上卻不會,該問題我也送出在 ​​#105014​​ 下開始跟進。

添加的 Fallback 之後效果如上圖左側所示, 那 Fallback 的作用是什麼?

前面我們介紹過,系統在多語言中渲染是需要多種字型庫來支援,而當找不到字形時,就要依賴提供的 Fallback 裡的有序清單,例如:

如果在 ​​fontFamily​​​ 中找不到字形,則在 ​​fontFamilyFallback​​ 中搜尋,如果沒有找到,則會在傳回預設字型。

另外關于 ​

​FontWeight​

​​ 還有一個“小彩蛋”,在 iOS 上,當使用者在輔助設定裡開啟 Bold Text 之後,如果你使用的是 ​

​Text​

​ 控件,那麼預設情況下所有的字型都會變成 w700 的粗體。

Flutter 小技巧之玩轉字型渲染和問題修複

因為在 ​

​Text​

​​ 内使用了 ​

​MediaQuery.boldTextOverride​

​​ 判斷,Flutter 會接收到 iOS 上使用者開啟了 Bold Text ,進而強行将 ​

​fontWeight​

​​ 設定為 ​

​FontWeight.bold ​

​​ ,當然如果你直接使用 ​

​RichText​

​ 就 沒有這一行為。

Flutter 小技巧之玩轉字型渲染和問題修複

這時候小技巧就又來了:如果你不希望這些系統行為幹擾到你,那麼你可以通過嵌套 ​

​MediaQuery​

​ 來全局關閉,而類似的行為還有 ​

​textScaleFactor​

​ 和 ​

​platformBrightness​

​等 。

return MediaQuery(
  data: MediaQueryData.fromWindow(WidgetsBinding.instance!.window).copyWith(boldText: false),
  child: MaterialApp(
    useInheritedMediaQuery: true,
  ),
);      
Flutter 小技巧之玩轉字型渲染和問題修複

FontFeature

最後再介紹一個冷門參數 FontFeature 。

什麼是 ​

​FontFeature​

​? 簡單來說就是影響字型形狀的一個屬性 ,在前端的對應領域裡應該是 ​

​font-feature-settings​

​​,它有别于 ​

​FontFamily​

​ ,是用于指定字型内字的形狀參數。

如下圖所示是 ​

​frac​

​​ 分數和 ​

​tnum​

​​ 表格數字的對比渲染效果,這種效果可以在不增加字型庫時實作特殊的渲染,另外 ​

​Feature​

​ 也有特征的意思,是以也可以了解為字型特征。
Flutter 小技巧之玩轉字型渲染和問題修複

那 FontFeature 有什麼用呢? 這裡又有一個使用小技巧了:當出現數字和文本同時出現,導緻排列不對齊時,可以通過給 ​

​Text​

​ 設定 ​

​fontFeatures: [FontFeature("tnum")]​

​ 來對齊。

例如下圖左邊是沒有設定 fontFeatures 的情況,右邊是設定了 ​

​FontFeature("tnum")​

​ 的情況,對比之下還是很明顯的。

Flutter 小技巧之玩轉字型渲染和問題修複
更多關于 FontFeature 的内容可見 ​​《Flutter 上字型的另類玩法:FontFeature 》​​

三、最後

  • 字型基礎
  • Text Height
  • FontWeight
  • FontFeature