本篇将带你深入理解 Flutter 开发过程中关于字体和文本渲染的“冷”知识,帮助你理解和增加关于 Flutter 中字体绘制的“无用”知识点。

毕竟此类相关的内容太少了

首先从一个简单的文本显示开始,如下代码所示,运行后可以看到界面内出现了一个 H 字母,它的 fontSize100Text 被放在一个高度为 200Container 中,然后如果这时候有人问你:Text 显示 H 字母需要占据多大的高度,你知道吗?

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. alignment: Alignment.center,
  13. child: new Row(
  14. children: <Widget>[
  15. Container(
  16. child: new Text(
  17. "H",
  18. style: TextStyle(
  19. fontSize: 100,
  20. ),
  21. ),
  22. ),
  23. Container(
  24. height: 100,
  25. width: 100,
  26. color: Colors.red,
  27. )
  28. ],
  29. ),
  30. )
  31. ),
  32. ),
  33. );
  34. }

带你深入理解 Flutter 中的字体“冷”知识 - 图1

一、TextStyle

如下代码所示,为了解答这个问题,首先我们给 Text 所在的 Container 增加了一个蓝色背景,并增加一个 100 * 100 大小的红色小方块做对比。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. alignment: Alignment.center,
  13. child: new Row(
  14. mainAxisAlignment: MainAxisAlignment.center,
  15. children: <Widget>[
  16. Container(
  17. color: Colors.blue,
  18. child: new Text(
  19. "H",
  20. style: TextStyle(
  21. fontSize: 100,
  22. ),
  23. ),
  24. ),
  25. Container(
  26. height: 100,
  27. width: 100,
  28. color: Colors.red,
  29. )
  30. ],
  31. ),
  32. )
  33. ),
  34. ),
  35. );
  36. }

结果如下图所示,可以看到 H 字母的上下有着一定的 padding 区域,蓝色Container 的大小明显超过了 100 ,但是黑色的 H 字母本身并没有超过红色小方块,那蓝色区域的高度是不是 Text 的高度,它的大小又是如何组成的呢?

带你深入理解 Flutter 中的字体“冷”知识 - 图2

事实上,前面的蓝色区域是字体的行高,也就是 line height ,关于这个行高,首先需要解释的就是 TextStyle 中的 height 参数。

默认情况下 height 参数是 null,当我们把它设置为 1 之后,如下图所示,可以看到蓝色区域的高度和红色小方块对齐,变成了 100 的高度,也就是行高变成了 100 ,而 H 字母完整的显示在蓝色区域内。

带你深入理解 Flutter 中的字体“冷”知识 - 图3

height 是什么呢?根据文档可知,首先 TextStyle 中的 height 参数值在设置后,其效果值是 fontSize 的倍数:

  • height 为空时,行高默认是使用字体的量度(这个量度后面会有解释);
  • height 不是空时,行高为 height * fontSize 的大小;

如下图所示,蓝色区域和红色区域的对比就是 heightnull1 的对比高度。

带你深入理解 Flutter 中的字体“冷”知识 - 图4

另外上图的 BaseLine 也解释了:为什么 fontSize 为 100 的 H 字母,不是充满高度为 100 的蓝色区域。

根据上图的示意效果,在 height 为 1 的红色区域内,H 字母也应该是显示在基线之上,而基线的底部区域是为了如 g 和 j 等字母预留,所以如下图所示,在 Text 内加入 g 字母并打开 Flutter 调试的文本基线显示,由 Flutter 渲染的绿色基线也可以看到符合我们预期的效果。

忘记截图由 g 的了,脑补吧。

带你深入理解 Flutter 中的字体“冷”知识 - 图5

接着如下代码所示,当我们把 height 设置为 2 ,并且把上层的高度为 200Container 添加一个紫色背景,结果如下图所示,可以看到蓝色块刚好充满紫色方块,因为 fontSize100 的文本在 x2 之后恰好高度就是 200

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. color: Colors.purple,
  13. alignment: Alignment.center,
  14. child: new Row(
  15. mainAxisAlignment: MainAxisAlignment.center,
  16. children: <Widget>[
  17. Container(
  18. color: Colors.blue,
  19. child: new Text(
  20. "Hg",
  21. style: TextStyle(
  22. fontSize: 100,
  23. height: 2,
  24. ),
  25. ),
  26. ),
  27. Container(
  28. height: 100,
  29. width: 100,
  30. color: Colors.red,
  31. )
  32. ],
  33. ),
  34. )
  35. ),
  36. ),
  37. );
  38. }

带你深入理解 Flutter 中的字体“冷”知识 - 图6

不过这里的 Hg 是往下偏移的,为什么这样偏移在后面会介绍,还会有新的对比。

最后如下图所示,是官方提供的在不同 TextStyleheight 参数下, Text 所占高度的对比情况。

带你深入理解 Flutter 中的字体“冷”知识 - 图7

二、StrutStyle

那再回顾下前面所说的默认字体的量度,这个默认字体的量度又是如何组成的呢?这就不得不说到 StrutStyle

如下代码所示,在之前的代码中添加 StrutStyle

  • 设置了 forceStrutHeight 为 true ,这是因为只有 forceStrutHeight 才能强制重置 Textheight 属性;
  • 设置了StrutStyleheight 设置为 1 ,这样 TextStyle 中的 height 等于 2 就没有了效果。
  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. color: Colors.purple,
  13. alignment: Alignment.center,
  14. child: new Row(
  15. mainAxisAlignment: MainAxisAlignment.center,
  16. children: <Widget>[
  17. Container(
  18. color: Colors.blue,
  19. child: new Text(
  20. "Hg",
  21. style: TextStyle(
  22. fontSize: 100,
  23. height: 2,
  24. ),
  25. strutStyle: StrutStyle(
  26. forceStrutHeight: true,
  27. fontSize: 100,
  28. height: 1
  29. ),
  30. ),
  31. ),
  32. Container(
  33. height: 100,
  34. width: 100,
  35. color: Colors.red,
  36. )
  37. ],
  38. ),
  39. )
  40. ),
  41. ),
  42. );
  43. }

效果如下图所示,虽然 TextStyleheight2 ,但是显示出现是以 StrutStyleheight1 的效果为准。

带你深入理解 Flutter 中的字体“冷”知识 - 图8

然后查看文档对于 StrutStyleheight 的描述,可以看到:height 的效果依然是 fontSize 的倍数,但是不同的是这里的对 fontSize 进行了补充说明 : ascent + descent = fontSize,其中:

  • ascent 代表的是基线上方部分;
  • descent 代表的是基线的半部分

  • 其组合效果如下图所示:

带你深入理解 Flutter 中的字体“冷”知识 - 图9

Flutter 中 ascentdescent 是不能用代码单独设置。

除此之外,StrutStylefontSizeTextStylefontSize 作用并不一样:当我们把 StrutStylefontSize 设置为 50 ,而 TextStylefontSize 依然是 100 时,如下图所示,可以看到黑色的字体大小没有发生变化,而蓝色部分的大小变为了 50 的大小。

带你深入理解 Flutter 中的字体“冷”知识 - 图10

有人就要说那 StrutStyle 这样的 fontSize 有什么用?

这时候,如果在上面条件不变的情况下,把 Text 中的文本变成 "Hg\nHg" 这样的两行文本,可以看到换行后的文本重叠在了一起,所以 StrutStylefontSize 也是会影响行高

带你深入理解 Flutter 中的字体“冷”知识 - 图11

另外,在 StrutStyle 中还有另外一个参数也会影响行高,那就是 leading

如下图所示,加上了 leading 后才是 Flutter 中对字体行高完全的控制组合,leading 默认为 null ,同时它的效果也是 fontSize 的倍数,并且分布是上下均分。

带你深入理解 Flutter 中的字体“冷”知识 - 图12

所以如下代码所示,当 StrutStylefontSize100height 为 1,leading 为 1 时,可以看到 leading 的大小让蓝色区域变为了 200,从而 和紫色区域高度又重叠了,不同的对比之前的 Hg 在这次充满显示是居中。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. color: Colors.purple,
  13. alignment: Alignment.center,
  14. child: new Row(
  15. mainAxisAlignment: MainAxisAlignment.center,
  16. children: <Widget>[
  17. Container(
  18. color: Colors.blue,
  19. child: new Text(
  20. "Hg",
  21. style: TextStyle(
  22. fontSize: 100,
  23. height: 2,
  24. ),
  25. strutStyle: StrutStyle(
  26. forceStrutHeight: true,
  27. fontSize: 100,
  28. height: 1,
  29. leading: 1
  30. ),
  31. ),
  32. ),
  33. Container(
  34. height: 100,
  35. width: 100,
  36. color: Colors.red,
  37. )
  38. ],
  39. ),
  40. )
  41. ),
  42. ),
  43. );
  44. }

因为 leading 是上下均分的,而 height 是根据 ascentdescent 的部分放大,明显 ascentdescent 大得多,所以前面的 TextStyleheight 为 2 时,充满后整体往下偏移。

带你深入理解 Flutter 中的字体“冷”知识 - 图13

三、backgroundColor

那么到这里应该对于 Flutter 中关于文本大小、度量和行高等有了基本的认知,接着再介绍一个属性:TextStylebackgroundColor

介绍这个属性是为了和前面的内容产生一个对比,并且解除一些误解。

如下代码所示,可以看到 StrutStylefontSize100height1,按照前面的介绍,蓝色的区域大小应该是和红色小方块一样大。

然后我们设置了 TextStylebackgroundColor 为具有透明度的绿色,结果如下图所示,可以看到 backgroundColor 的区域超过了 StrutStyle,显示为默认情况下字体的度量

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. color: Colors.purple,
  13. alignment: Alignment.center,
  14. child: new Row(
  15. mainAxisAlignment: MainAxisAlignment.center,
  16. children: <Widget>[
  17. Container(
  18. color: Colors.blue,
  19. child: new Text(
  20. "Hg",
  21. style: TextStyle(
  22. fontSize: 100,
  23. backgroundColor: Colors.green.withAlpha(180)
  24. ),
  25. strutStyle: StrutStyle(
  26. forceStrutHeight: true,
  27. fontSize: 100,
  28. height: 1,
  29. ),
  30. ),
  31. ),
  32. Container(
  33. height: 100,
  34. width: 100,
  35. color: Colors.red,
  36. )
  37. ],
  38. ),
  39. )
  40. ),
  41. ),
  42. );
  43. }

带你深入理解 Flutter 中的字体“冷”知识 - 图14

这是不是很有意思,事实上也可以反应出,字体的度量其实一直都是默认的 ascent + descent = fontSize,我们可以改变 TextStyleheight 或者 StrutStyle 来改变行高效果,但是本质上的 fontSize 其实并没有变。

如果把输入内容换成 "H\ng" ,如下图所示可以看到更有意思的效果。

带你深入理解 Flutter 中的字体“冷”知识 - 图15

四、TextBaseline

最后再介绍一个属性 :TextStyleTextBaseline,因为这个属性一直让人产生“误解”。

关于 TextBaseline 有两个属性,分别是 alphabeticideographic ,为了更方便解释他们的效果,如下代码所示,我们通过 CustomPaint 把不同的基线位置绘制出来。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Container(
  11. height: 200,
  12. width: 400,
  13. color: Colors.purple,
  14. child: CustomPaint(
  15. painter: Text2Painter(),
  16. ),
  17. )
  18. ),
  19. ),
  20. );
  21. }
  22. class Text2Painter extends CustomPainter {
  23. @override
  24. void paint(Canvas canvas, Size size) {
  25. var baseLine = TextBaseline.alphabetic;
  26. //var baseLine = TextBaseline.ideographic;
  27. final textStyle =
  28. TextStyle(color: Colors.white, fontSize: 100, textBaseline: baseLine);
  29. final textSpan = TextSpan(
  30. text: 'My文字',
  31. style: textStyle,
  32. );
  33. final textPainter = TextPainter(
  34. text: textSpan,
  35. textDirection: TextDirection.ltr,
  36. );
  37. textPainter.layout(
  38. minWidth: 0,
  39. maxWidth: size.width,
  40. );
  41. final left = 0.0;
  42. final top = 0.0;
  43. final right = textPainter.width;
  44. final bottom = textPainter.height;
  45. final rect = Rect.fromLTRB(left, top, right, bottom);
  46. final paint = Paint()
  47. ..color = Colors.red
  48. ..style = PaintingStyle.stroke
  49. ..strokeWidth = 1;
  50. canvas.drawRect(rect, paint);
  51. // draw the baseline
  52. final distanceToBaseline =
  53. textPainter.computeDistanceToActualBaseline(baseLine);
  54. canvas.drawLine(
  55. Offset(0, distanceToBaseline),
  56. Offset(textPainter.width, distanceToBaseline),
  57. paint..color = Colors.blue..strokeWidth = 5,
  58. );
  59. // draw the text
  60. final offset = Offset(0, 0);
  61. textPainter.paint(canvas, offset);
  62. }
  63. @override
  64. bool shouldRepaint(CustomPainter oldDelegate) => true;
  65. }

如下图所示,蓝色的线就是 baseLine,从效果可以直观看到不同 baseLine 下对齐的位置应该在哪里。

带你深入理解 Flutter 中的字体“冷”知识 - 图16

但是事实上 baseLine 的作用并不会直接影响 TextStyle 中文本的对齐方式,Flutter 中默认显示的文本只会通过 TextBaseline.alphabetic 对齐的,如下图所示官方人员也对这个问题有过描述 #47512

带你深入理解 Flutter 中的字体“冷”知识 - 图17

这也是为什么要用 CustomPaint 展示的原因,因为用默认 Text 展示不出来。

举个典型的例子,如下代码所示,虽然在 RowText 上都是用了 ideographic ,但是其实并没有达到我们想要的效果。

  1. @override
  2. Widget build(BuildContext context) {
  3. return Scaffold(
  4. backgroundColor: Colors.black,
  5. body: Container(
  6. color: Colors.lime,
  7. alignment: Alignment.center,
  8. child: Container(
  9. alignment: Alignment.center,
  10. child: Row(
  11. crossAxisAlignment: CrossAxisAlignment.baseline,
  12. textBaseline: TextBaseline.ideographic,
  13. mainAxisSize: MainAxisSize.max,
  14. children: [
  15. Text(
  16. '我是中文',
  17. style: TextStyle(
  18. fontSize: 55,
  19. textBaseline: TextBaseline.ideographic,
  20. ),
  21. ),
  22. Spacer(),
  23. Text('123y56',
  24. style: TextStyle(
  25. fontSize: 55,
  26. textBaseline: TextBaseline.ideographic,
  27. )),
  28. ])),
  29. ),
  30. );
  31. }

关键就算 Row 设置了 center ,这段文本看起来还是不是特别“对齐”。

带你深入理解 Flutter 中的字体“冷”知识 - 图18

自从,关于 Flutter 中的字体相关的“冷”知识介绍完了,不知道你“无用”的知识有没有增多呢?

带你深入理解 Flutter 中的字体“冷”知识 - 图19