本篇將帶你深入理解 Flutter 開發過程中關於字型和文字渲染的“冷”知識,幫助你理解和增加關於 Flutter 中字型繪製的“無用”知識點。
畢竟此類相關的內容太少了
首先從一個簡單的文字顯示開始,如下程式碼所示,執行後可以看到介面內出現了一個 H 字母,它的 fontSize
是 100,Text
被放在一個高度為 200 的 Container
中,然後如果這時候有人問你: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,
)
],
),
)
),
),
);
}
複製程式碼
一、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
的高度,它的大小又是如何組成的呢?
事實上,前面的藍色區域是字型的行高,也就是 line height ,關於這個行高,首先需要解釋的就是 TextStyle
中的 height
引數。
預設情況下 height
引數是 null
,當我們把它設定為 1
之後,如下圖所示,可以看到藍色區域的高度和紅色小方塊對齊,變成了 100 的高度,也就是行高變成了 100 ,而 H 字母完整的顯示在藍色區域內。
那 height
是什麼呢?根據文件可知,首先 TextStyle
中的 height
引數值在設定後,其效果值是 fontSize
的倍數:
- 當
height
為空時,行高預設是使用字型的量度(這個量度後面會有解釋); - 當
height
不是空時,行高為height
*fontSize
的大小;
如下圖所示,藍色區域和紅色區域的對比就是 height
為 null
和 1
的對比高度。
另外上圖的 BaseLine
也解釋了:為什麼 fontSize
為 100 的 H 字母,不是充滿高度為 100 的藍色區域。
根據上圖的示意效果,在 height
為 1 的紅色區域內,H 字母也應該是顯示在基線之上,而基線的底部區域是為了如 g 和 j 等字母預留,所以如下圖所示,在 Text
內加入 g 字母並開啟 Flutter 除錯的文字基線顯示,由 Flutter 渲染的綠色基線也可以看到符合我們預期的效果。
忘記截圖由 g 的了,腦補吧。
接著如下程式碼所示,當我們把 height
設定為 2
,並且把上層的高度為 200 的 Container
新增一個紫色背景,結果如下圖所示,可以看到藍色塊剛好充滿紫色方塊,因為 fontSize
為 100 的文字在 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,
)
],
),
)
),
),
);
}
複製程式碼
不過這裡的
Hg
是往下偏移的,為什麼這樣偏移在後面會介紹,還會有新的對比。
最後如下圖所示,是官方提供的在不同 TextStyle
的 height
引數下, Text
所佔高度的對比情況。
二、StrutStyle
那再回顧下前面所說的預設字型的量度,這個預設字型的量度又是如何組成的呢?這就不得不說到 StrutStyle
。
如下程式碼所示,在之前的程式碼中新增 StrutStyle
:
- 設定了
forceStrutHeight
為 true ,這是因為只有forceStrutHeight
才能強制重置Text
的height
屬性; - 設定了
StrutStyle
的height
設定為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,
)
],
),
)
),
),
);
}
複製程式碼
效果如下圖所示,雖然 TextStyle
的 height
是 2
,但是顯示出現是以 StrutStyle
中 height
為 1
的效果為準。
然後檢視文件對於 StrutStyle
中 height
的描述,可以看到:height
的效果依然是 fontSize
的倍數,但是不同的是這裡的對 fontSize
進行了補充說明 : ascent + descent = fontSize
,其中:
-
ascent
代表的是基線上方部分; -
descent
代表的是基線的半部分 -
其組合效果如下圖所示:
Flutter 中
ascent
和descent
是不能用程式碼單獨設定。
除此之外,StrutStyle
的 fontSize
和 TextStyle
的 fontSize
作用並不一樣:當我們把 StrutStyle
的 fontSize
設定為 50 ,而 TextStyle
的 fontSize
依然是 100 時,如下圖所示,可以看到黑色的字型大小沒有發生變化,而藍色部分的大小變為了 50 的大小。
有人就要說那 StrutStyle
這樣的 fontSize
有什麼用?
這時候,如果在上面條件不變的情況下,把 Text
中的文字變成 "Hg\nHg"
這樣的兩行文字,可以看到換行後的文字重疊在了一起,所以 StrutStyle
的 fontSize
也是會影響行高。
另外,在 StrutStyle
中還有另外一個引數也會影響行高,那就是 leading
。
如下圖所示,加上了 leading
後才是 Flutter 中對字型行高完全的控制組合,leading
預設為 null
,同時它的效果也是 fontSize
的倍數,並且分佈是上下均分。
所以如下程式碼所示,當 StrutStyle
的 fontSize
為 100 ,height
為 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
是根據ascent
和descent
的部分放大,明顯ascent
比ascent
大得多,所以前面的TextStyle
的height
為 2 時,充滿後整體往下偏移。
三、backgroundColor
那麼到這裡應該對於 Flutter 中關於文字大小、度量和行高等有了基本的認知,接著再介紹一個屬性:TextStyle
的 backgroundColor
。
介紹這個屬性是為了和前面的內容產生一個對比,並且解除一些誤解。
如下程式碼所示,可以看到 StrutStyle
的 fontSize
為 100 ,height
為 1
,按照前面的介紹,藍色的區域大小應該是和紅色小方塊一樣大。
然後我們設定了 TextStyle
的 backgroundColor
為具有透明度的綠色,結果如下圖所示,可以看到 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,
)
],
),
)
),
),
);
}
複製程式碼
這是不是很有意思,事實上也可以反應出,字型的度量其實一直都是預設的 ascent + descent = fontSize
,我們可以改變 TextStyle
的 height
或者 StrutStyle
來改變行高效果,但是本質上的 fontSize
其實並沒有變。
如果把輸入內容換成 "H\ng"
,如下圖所示可以看到更有意思的效果。
四、TextBaseline
最後再介紹一個屬性 :TextStyle
的 TextBaseline
,因為這個屬性一直讓人產生“誤解”。
關於 TextBaseline
有兩個屬性,分別是 alphabetic
和 ideographic
,為了更方便解釋他們的效果,如下程式碼所示,我們通過 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 下對齊的位置應該在哪裡。
但是事實上 baseLine 的作用並不會直接影響 TextStyle
中文字的對齊方式,Flutter 中預設顯示的文字只會通過 TextBaseline.alphabetic
對齊的,如下圖所示官方人員也對這個問題有過描述 #47512。
這也是為什麼要用
CustomPaint
展示的原因,因為用預設Text
展示不出來。
舉個典型的例子,如下程式碼所示,雖然在 Row
和 Text
上都是用了 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 中的字型相關的“冷”知識介紹完了,不知道你“無用”的知識有沒有增多呢?