帶你深入理解 Flutter 中的字型“冷”知識

戀貓de小郭發表於2020-05-29

本篇將帶你深入理解 Flutter 開發過程中關於字型和文字渲染的“冷”知識,幫助你理解和增加關於 Flutter 中字型繪製的“無用”知識點。

畢竟此類相關的內容太少了

首先從一個簡單的文字顯示開始,如下程式碼所示,執行後可以看到介面內出現了一個 H 字母,它的 fontSize100Text 被放在一個高度為 200Container 中,然後如果這時候有人問你:Text 顯示 H 字母需要佔據多大的高度,你知道嗎?


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            alignment: Alignment.center,
            child: new Row(
              children: <Widget>[
                Container(
                  child: new Text(
                    "H",
                    style: TextStyle(
                      fontSize: 100,
                    ),
                  ),
                ),
                Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                )
              ],
            ),
          )

        ),
      ),
    );
  }
複製程式碼

帶你深入理解 Flutter 中的字型“冷”知識

一、TextStyle

如下程式碼所示,為了解答這個問題,首先我們給 Text 所在的 Container 增加了一個藍色背景,並增加一個 100 * 100 大小的紅色小方塊做對比。

@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            alignment: Alignment.center,
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  color: Colors.blue,
                  child: new Text(
                    "H",
                    style: TextStyle(
                      fontSize: 100,
                    ),
                  ),

                ),
                Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                )
              ],
            ),
          )

        ),
      ),
    );
  }
複製程式碼

結果如下圖所示,可以看到 H 字母的上下有著一定的 padding 區域,藍色Container 的大小明顯超過了 100 ,但是黑色的 H 字母本身並沒有超過紅色小方塊,那藍色區域的高度是不是 Text 的高度,它的大小又是如何組成的呢?

帶你深入理解 Flutter 中的字型“冷”知識

事實上,前面的藍色區域是字型的行高,也就是 line height ,關於這個行高,首先需要解釋的就是 TextStyle 中的 height 引數。

預設情況下 height 引數是 null,當我們把它設定為 1 之後,如下圖所示,可以看到藍色區域的高度和紅色小方塊對齊,變成了 100 的高度,也就是行高變成了 100 ,而 H 字母完整的顯示在藍色區域內。

帶你深入理解 Flutter 中的字型“冷”知識

height 是什麼呢?根據文件可知,首先 TextStyle 中的 height 引數值在設定後,其效果值是 fontSize 的倍數:

  • height 為空時,行高預設是使用字型的量度(這個量度後面會有解釋);
  • height 不是空時,行高為 height * fontSize 的大小;

如下圖所示,藍色區域和紅色區域的對比就是 heightnull1 的對比高度。

帶你深入理解 Flutter 中的字型“冷”知識

另外上圖的 BaseLine 也解釋了:為什麼 fontSize 為 100 的 H 字母,不是充滿高度為 100 的藍色區域。

根據上圖的示意效果,在 height 為 1 的紅色區域內,H 字母也應該是顯示在基線之上,而基線的底部區域是為了如 g 和 j 等字母預留,所以如下圖所示,在 Text 內加入 g 字母並開啟 Flutter 除錯的文字基線顯示,由 Flutter 渲染的綠色基線也可以看到符合我們預期的效果。

忘記截圖由 g 的了,腦補吧。

帶你深入理解 Flutter 中的字型“冷”知識

接著如下程式碼所示,當我們把 height 設定為 2 ,並且把上層的高度為 200Container 新增一個紫色背景,結果如下圖所示,可以看到藍色塊剛好充滿紫色方塊,因為 fontSize100 的文字在 x2 之後恰好高度就是 200

@override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            color: Colors.purple,
            alignment: Alignment.center,
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  color: Colors.blue,
                  child: new Text(
                    "Hg",
                    style: TextStyle(
                      fontSize: 100,
                      height: 2,
                    ),
                  ),

                ),
                Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                )
              ],
            ),
          )

        ),
      ),
    );
  }
複製程式碼

帶你深入理解 Flutter 中的字型“冷”知識

不過這裡的 Hg 是往下偏移的,為什麼這樣偏移在後面會介紹,還會有新的對比。

最後如下圖所示,是官方提供的在不同 TextStyleheight 引數下, Text 所佔高度的對比情況。

帶你深入理解 Flutter 中的字型“冷”知識

二、StrutStyle

那再回顧下前面所說的預設字型的量度,這個預設字型的量度又是如何組成的呢?這就不得不說到 StrutStyle

如下程式碼所示,在之前的程式碼中新增 StrutStyle

  • 設定了 forceStrutHeight 為 true ,這是因為只有 forceStrutHeight 才能強制重置 Textheight 屬性;
  • 設定了StrutStyleheight 設定為 1 ,這樣 TextStyle 中的 height 等於 2 就沒有了效果。
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            color: Colors.purple,
            alignment: Alignment.center,
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  color: Colors.blue,
                  child: new Text(
                    "Hg",
                    style: TextStyle(
                      fontSize: 100,
                      height: 2,
                    ),
                    strutStyle: StrutStyle(
                      forceStrutHeight: true,
                      fontSize: 100,
                      height: 1
                    ),

                  ),

                ),
                Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                )
              ],
            ),
          )

        ),
      ),
    );
  }
複製程式碼

效果如下圖所示,雖然 TextStyleheight2 ,但是顯示出現是以 StrutStyleheight1 的效果為準。

帶你深入理解 Flutter 中的字型“冷”知識

然後檢視文件對於 StrutStyleheight 的描述,可以看到:height 的效果依然是 fontSize 的倍數,但是不同的是這裡的對 fontSize 進行了補充說明 : ascent + descent = fontSize,其中:

  • ascent 代表的是基線上方部分;

  • descent 代表的是基線的半部分

  • 其組合效果如下圖所示:

帶你深入理解 Flutter 中的字型“冷”知識

Flutter 中 ascentdescent 是不能用程式碼單獨設定。

除此之外,StrutStylefontSizeTextStylefontSize 作用並不一樣:當我們把 StrutStylefontSize 設定為 50 ,而 TextStylefontSize 依然是 100 時,如下圖所示,可以看到黑色的字型大小沒有發生變化,而藍色部分的大小變為了 50 的大小。

帶你深入理解 Flutter 中的字型“冷”知識

有人就要說那 StrutStyle 這樣的 fontSize 有什麼用?

這時候,如果在上面條件不變的情況下,把 Text 中的文字變成 "Hg\nHg" 這樣的兩行文字,可以看到換行後的文字重疊在了一起,所以 StrutStylefontSize 也是會影響行高

帶你深入理解 Flutter 中的字型“冷”知識

另外,在 StrutStyle 中還有另外一個引數也會影響行高,那就是 leading

如下圖所示,加上了 leading 後才是 Flutter 中對字型行高完全的控制組合,leading 預設為 null ,同時它的效果也是 fontSize 的倍數,並且分佈是上下均分。

帶你深入理解 Flutter 中的字型“冷”知識

所以如下程式碼所示,當 StrutStylefontSize100height 為 1,leading 為 1 時,可以看到 leading 的大小讓藍色區域變為了 200,從而 和紫色區域高度又重疊了,不同的對比之前的 Hg 在這次充滿顯示是居中。


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            color: Colors.purple,
            alignment: Alignment.center,
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  color: Colors.blue,
                  child: new Text(
                    "Hg",
                    style: TextStyle(
                      fontSize: 100,
                      height: 2,
                    ),
                    strutStyle: StrutStyle(
                      forceStrutHeight: true,
                      fontSize: 100,
                      height: 1,
                      leading: 1
                    ),

                  ),

                ),
                Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                )
              ],
            ),
          )

        ),
      ),
    );
  }
複製程式碼

因為 leading 是上下均分的,而 height 是根據 ascentdescent 的部分放大,明顯 ascentascent 大得多,所以前面的 TextStyleheight 為 2 時,充滿後整體往下偏移。

帶你深入理解 Flutter 中的字型“冷”知識

三、backgroundColor

那麼到這裡應該對於 Flutter 中關於文字大小、度量和行高等有了基本的認知,接著再介紹一個屬性:TextStylebackgroundColor

介紹這個屬性是為了和前面的內容產生一個對比,並且解除一些誤解。

如下程式碼所示,可以看到 StrutStylefontSize100height1,按照前面的介紹,藍色的區域大小應該是和紅色小方塊一樣大。

然後我們設定了 TextStylebackgroundColor 為具有透明度的綠色,結果如下圖所示,可以看到 backgroundColor 的區域超過了 StrutStyle,顯示為預設情況下字型的度量


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            color: Colors.purple,
            alignment: Alignment.center,
            child: new Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: <Widget>[
                Container(
                  color: Colors.blue,
                  child: new Text(
                    "Hg",
                    style: TextStyle(
                      fontSize: 100,
                      backgroundColor: Colors.green.withAlpha(180)
                    ),
                    strutStyle: StrutStyle(
                      forceStrutHeight: true,
                      fontSize: 100,
                      height: 1,
                    ),

                  ),

                ),
                Container(
                  height: 100,
                  width: 100,
                  color: Colors.red,
                )
              ],
            ),
          )

        ),
      ),
    );
  }
複製程式碼

帶你深入理解 Flutter 中的字型“冷”知識

這是不是很有意思,事實上也可以反應出,字型的度量其實一直都是預設的 ascent + descent = fontSize,我們可以改變 TextStyleheight 或者 StrutStyle 來改變行高效果,但是本質上的 fontSize 其實並沒有變。

如果把輸入內容換成 "H\ng" ,如下圖所示可以看到更有意思的效果。

帶你深入理解 Flutter 中的字型“冷”知識

四、TextBaseline

最後再介紹一個屬性 :TextStyleTextBaseline,因為這個屬性一直讓人產生“誤解”。

關於 TextBaseline 有兩個屬性,分別是 alphabeticideographic ,為了更方便解釋他們的效果,如下程式碼所示,我們通過 CustomPaint 把不同的基線位置繪製出來。


  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
          alignment: Alignment.center,
          child: Container(
            height: 200,
            width: 400,
            color: Colors.purple,
            child: CustomPaint(
              painter: Text2Painter(),
            ),
          )

        ),
      ),
    );
  }
  
class Text2Painter extends CustomPainter {
  @override
  void paint(Canvas canvas, Size size) {
    var baseLine = TextBaseline.alphabetic;
    //var baseLine = TextBaseline.ideographic;

    final textStyle =
        TextStyle(color: Colors.white, fontSize: 100, textBaseline: baseLine);
    final textSpan = TextSpan(
      text: 'My文字',
      style: textStyle,
    );
    final textPainter = TextPainter(
      text: textSpan,
      textDirection: TextDirection.ltr,
    );
    textPainter.layout(
      minWidth: 0,
      maxWidth: size.width,
    );

    final left = 0.0;
    final top = 0.0;
    final right = textPainter.width;
    final bottom = textPainter.height;
    final rect = Rect.fromLTRB(left, top, right, bottom);
    final paint = Paint()
      ..color = Colors.red
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1;
    canvas.drawRect(rect, paint);

    // draw the baseline
    final distanceToBaseline =
        textPainter.computeDistanceToActualBaseline(baseLine);

    canvas.drawLine(
      Offset(0, distanceToBaseline),
      Offset(textPainter.width, distanceToBaseline),
      paint..color = Colors.blue..strokeWidth = 5,
    );

    // draw the text
    final offset = Offset(0, 0);
    textPainter.paint(canvas, offset);
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => true;
}
複製程式碼

如下圖所示,藍色的線就是 baseLine,從效果可以直觀看到不同 baseLine 下對齊的位置應該在哪裡。

帶你深入理解 Flutter 中的字型“冷”知識

但是事實上 baseLine 的作用並不會直接影響 TextStyle 中文字的對齊方式,Flutter 中預設顯示的文字只會通過 TextBaseline.alphabetic 對齊的,如下圖所示官方人員也對這個問題有過描述 #47512

帶你深入理解 Flutter 中的字型“冷”知識

這也是為什麼要用 CustomPaint 展示的原因,因為用預設 Text 展示不出來。

舉個典型的例子,如下程式碼所示,雖然在 RowText 上都是用了 ideographic ,但是其實並沒有達到我們想要的效果。

 @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: Container(
        color: Colors.lime,
        alignment: Alignment.center,
        child: Container(
            alignment: Alignment.center,
            child: Row(
                crossAxisAlignment: CrossAxisAlignment.baseline,
                textBaseline: TextBaseline.ideographic,
                mainAxisSize: MainAxisSize.max,
                children: [
                  Text(
                    '我是中文',
                    style: TextStyle(
                      fontSize: 55,
                      textBaseline: TextBaseline.ideographic,
                    ),
                  ),
                  Spacer(),
                  Text('123y56',
                      style: TextStyle(
                        fontSize: 55,
                        textBaseline: TextBaseline.ideographic,
                      )),
                ])),
      ),
    );
  }
複製程式碼

關鍵就算 Row 設定了 center ,這段文字看起來還是不是特別“對齊”。

帶你深入理解 Flutter 中的字型“冷”知識

自從,關於 Flutter 中的字型相關的“冷”知識介紹完了,不知道你“無用”的知識有沒有增多呢?

帶你深入理解 Flutter 中的字型“冷”知識

相關文章