Flutter中那些你需要知道的文本知识!
Posted 编程的平行世界
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Flutter中那些你需要知道的文本知识!相关的知识,希望对你有一定的参考价值。
通过阅读本文,您将了解到
- 文本的组成部分;
- Flutter对于文本&段落是如何绘制的;
- 明白Flutter Text 背后的逻辑;
- 在业务中碰到一些文本显示的问题时,知道从哪些地方去尝试修改。
前言
文字是记录语言的书写符号系统,是形、音、义的统一体,是人类最重要的辅助性 交际工具。作为一个Flutter开发者,我们都知道可以通过Text()
这个文本组件将文字显示出来。但是这其中的Flutter的字体是怎么组成的?Flutter文本是怎么构建的?Render Tree
是怎样绘制文本的…作为本专栏(整个专栏都在与文本打交道)的第一篇文章,让我们从这些原理细节讲起。希望能对你认识Flutter的文本渲染有所帮助。
注:本文的目的在于让大家了解Flutter中的基本文本知识,快速的带大家了解渲染流程,但并未很深入的分析Flutter文本渲染的原理。
字体基础理论通用部分
在整个网络世界中,大家可以将字体理解为一个数字文件,它是一个包含特定大小、粗细和样式的文件。它定义了每个字的形状、大小和图形。
例如Bariol_Regular.otf。.otf
是字体文件格式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SLGvJ6LK-1669544494694)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6b43356354404639bd7b54526a2970da~tplv-k3u1fbpfcp-watermark.image?)]
有了字体格式后,我们会碰到相同的字体大小却有不同的显示布局这个问题。因为每一个字体格式都定义了它自己的参考大小,每一个字符都是基于这个大小设计的。所以即使设置同样的字体大小,也会有不同的布局。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M19SgzcN-1669544494696)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/541c8653d5964a3b9358d941b2ff3661~tplv-k3u1fbpfcp-watermark.image?)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-flIXYZLi-1669544494697)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/06896234fa3846469fb647a380bc607b~tplv-k3u1fbpfcp-watermark.image?)]
在Flutter中文本由哪些部分组成?
Baseline
- 在Flutter中,每一个字符都会在
Baseline
(基线)上。有了这个基线后,就算是不同大小的文字也可以处于同一水平线上。Baseline
是非常重要的,因为可以通过它测量文本和元素之间的垂直距离。其他还有Middleline
、Bottomline
、Topline
。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VZO6UXAO-1669544494698)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/2853b80fe819407b928a6fd93e29a859~tplv-k3u1fbpfcp-watermark.image?)]
- Baseline的算法公式推导有兴趣的朋友可以自行搜索。
Text Spacing
- 文字间距是指一段文本中每个文字之间插入的空间。
- 在Flutter中可以通过
TextStyle
下的wordSpacing
设置单词与单词之间的间距,通过letterSpacing
设置字符与字符之间的间距。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uyChiBG1-1669544494698)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/28a543c5d32840ee9f4a48d2ea333688~tplv-k3u1fbpfcp-watermark.image?)]
Weight
-
Weight是指字体笔画的粗细,在Flutter中通过
fontWeight
设置。常见的有:
normal
、bold
,其他还有FontWeight.w100
…等粗细值
TextStyle(
fontWeight: FontWeight.bold
),
TextSpan
在Flutter中,我们经常会使用Text()
这个组件,但是我们通过阅读Text()
的源码后就可以知道,它的build
方法返回的就是RichText
组件。所以它会呈现为TextSpan
。Span指的是字符之间的行距。
@override
Widget build(BuildContext context)
...
Widget result = RichText(
...
text: TextSpan(
style: effectiveTextStyle,
text: data,
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
),
);
...
return result;
Height
在Flutter中,定义了一个TextStyle.height
,用于给呈现文本的TextSpan
一个准确的行高。
TextStyle(height: 1)
但是我们需要注意,每一种字体格式都定义了自己的字体度量默认高度,这也是为什么即使设置了相同的字体高度,也会有不同的TextSpan
的高度。
让我们来看下这个例子:
红色是Flutter默认的字体,蓝色是Bariol_Regular字体,绿色是Bellota-Regular字体,看看他们在相同height
下不同的框高度。
- 默认height
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ikr2c80A-1669544494699)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/6f47d0f38c7443b8bbaad4ac3e15cc04~tplv-k3u1fbpfcp-watermark.image?)]
- height: 1.0
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qe9aiK52-1669544494700)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fa54e9cf2b28475ea39155ede0583499~tplv-k3u1fbpfcp-watermark.image?)]
- height:0.8
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tbGTBpwJ-1669544494701)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/1733f3d05cb2482fb868d92c7e14d8dc~tplv-k3u1fbpfcp-watermark.image?)]
这个例子也很好的验证了:
- 即使使用一样的fontSize,每种字体也都有不同的高度
- 每一种字体都有不同的基线。
那么关于Flutter的字体组成我们也可以得到一个结论:使用多种字体大概率会因为基线的不同导致布局不协调!
Flutter中是如何绘制文本的?
通过Paragraph
,Flutter
最后绘制文本时都是通过Paragraph
完成的!
// Paragraph paragraph:文本对象
// Offset offset:文本绘制的位置
void drawParagraph(Paragraph paragraph, Offset offset)
举个例子: 通过drawParagraph
绘制一段文字
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-C7TkjSos-1669544494702)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/fc53245c2bbd4a9c86207b2cf58afbfd~tplv-k3u1fbpfcp-watermark.image?)]
import 'dart:ui' as ui;
class TextPainter extends CustomPainter
//创建段落构建器
ParagraphBuilder paragraphBuilder = ParagraphBuilder(
ParagraphStyle(fontWeight: FontWeight.bold, fontSize: 16))
..pushStyle(ui.TextStyle(color: Colors.black))
..addText('通过drawParagraph绘制的 Hello Taxze');
@override
void paint(Canvas canvas, Size size)
//设置段落宽度
ParagraphConstraints paragraphConstraints =
ParagraphConstraints(width: size.width);
//计算绘制的文本位置及尺寸
Paragraph paragraph = paragraphBuilder.build()
..layout(paragraphConstraints);
//绘制
canvas.drawParagraph(paragraph, const Offset(40.0, 50.0));
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
使用:
@override
Widget build(BuildContext context)
return Scaffold(
...
body: SizedBox.expand(child: CustomPaint(painter: TextPainter())),
);
SizedBox.expand
包裹CustomPaint
是为了给ParagraphConstraints(width: size.width)
一个size
。你也可以用其他的组件包裹它。
关于Flutter使用CustomPaint
绘制文字的实践较为复杂,若要讲清楚绘制的主要知识点,则需要另开一篇文章来讲述。若对这个部分感兴趣的朋友可以阅读下这篇文章:Flutter学习:使用CustomPaint绘制文字 — @菠萝橙子丶
Flutter是如何把一段长文字转变成段落的?
你有没有想过,Flutter是如何把一段长文字生成下面的这样一个段落的呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K8zsfS9R-1669544494703)(https://p9-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a6aa369fa78f4762b66bfa5f64026cee~tplv-k3u1fbpfcp-watermark.image?)]
这张效果图的代码:
Container(
color: Colors.red,
width: 200,
height: 100,
margin: EdgeInsets.all(30),
child: Text(
"通过drawParagraph绘制的 Taxze Hello....")),
那么其中的自动换行是怎么实现的呢?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hQ2ezYSG-1669544494704)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/a4fe3f2943234c1ca5372dc902c53b57~tplv-k3u1fbpfcp-watermark.image?)]
我们知道,段落指的就是一段文本,我们要给每个字符一个合适的大小和位置。那么Flutter是如何计算这些参数的呢?
在前文说到过,Flutter最后绘制文本时都是通过Paragraph
完成的。Flutter就是通过Paragraph.layout
来计算这些参数,而且ParagraphBuilder
给每个字符都在渲染前分配了一个偏移量。通过Paragraph
可以知道所有占位符的位置和尺寸大小。
class TextPosition
//创建一个表示字符串中特定位置的对象。
const TextPosition(
required this.offset,
this.affinity = TextAffinity.downstream,
) : assert(offset != null),
assert(affinity != null);
//举个例子:有一个“Hello”字符,offset = 0表示光标在字符H之前,offset = 5表示光标在字符o之后。
final int offset;
final TextAffinity affinity;
@override
bool operator ==(Object other)
if (other.runtimeType != runtimeType)
return false;
return other is TextPosition
&& other.offset == offset
&& other.affinity == affinity;
@override
int get hashCode => Object.hash(offset, affinity);
@override
String toString()
return 'TextPosition(offset: $offset, affinity: $affinity)';
Text()背后的大哥有哪些?
–文本的渲染流程
从之前讲述的知识点,Text()
组件它的build
方法返回的就是RichText
,但是Flutter
最后绘制文本时又都是通过Paragraph
完成的!那么其中的完整的一个流程是怎么样的呢?话不多说,先上图!
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dDMUcy7t-1669544494704)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/dbce4fff785640e78c6c743b6ed6c409~tplv-k3u1fbpfcp-watermark.image?)]
组件层
如图所示,每当我们使用Text
组件时,它实际上创建的是RichText
组件。但是RichText
和Text
不同的是,Text
将String
作为参数,而RichText
将InlinSpan
作为参数(或者说是TextSpan
)。
const Text(String this.data)
//通过Text.rich构造函数传给RichText
const Text.rich(InlineSpan this.textSpan)
RichText(
...
text: TextSpan(
style: effectiveTextStyle,
text: data,
children: textSpan != null ? <InlineSpan>[textSpan!] : null,
),
)
//TextSpan继承于InlineSpan
class TextSpan extends InlineSpan implements HitTestTarget, MouseTrackerAnnotation
因RichText
接收TextSpan
,而每一个TextSpan
都有更多的子TextSpan
,这些子TextSpan
会 继承父TextSpan
的样式。例如:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PF2spzd5-1669544494705)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/233e496530c94552aca073ef9b90a60c~tplv-k3u1fbpfcp-watermark.image?)]
RichText(
text: TextSpan(
style: Theme.of(context)
.textTheme
.bodyText1
?.copyWith(fontSize: 24),
children: [
TextSpan(
text: 'Taxze ',
),
TextSpan(text: 'blog', style: TextStyle(color: Colors.blue)),
TextSpan(
text: ' Flutter',
),
TextSpan(text: '稀土掘金', style: TextStyle(color: Colors.blue)),
]))
不过,RichText
本身是MultiChildRenderObjectWidget
的子类。它们之间有这样的继承关系:
class RichText extends MultiChildRenderObjectWidget
abstract class MultiChildRenderObjectWidget extends RenderObjectWidget
而MultiChildRenderObjectWidget
产生的MultiChildRenderObjectElement
则是这样的关系:
class MultiChildRenderObjectElement extends RenderObjectElement
abstract class RenderObjectElement extends Element
RichText
实际上是需要一个InlineSpan
,而InlineSpan
可以是TextSpan
或者是WidgetSpan
。对WidgetSpan有兴趣的朋友,可以参考官方的文档WidgetSpan。
到这里为止,我们可以将RichText
(包括RichText)之前的所有划分为组件层,那么我们现在就要进入渲染层了。
渲染层
我们已经知道了RichText
会创建一个渲染对象— RenderParagraph
,那么RenderParagraph
是干什么的呢?
RichText
是MultiChildRenderObjectWidget
的子类,它会把MultiChildRenderObjectElement
往下传递,但是此时MultiChildRenderObjectElement
没有渲染,它还没有什么作用。这个时候RichText
会给它一个RenderParagraph
,RenderParagraph
会收到RenderPadding
的指令,这个时候MultiChildRenderObjectElement
就准备好了一切,就可以开始工作了。
这样解释可能有点抽象,那么我们来看下这个例子:
body: Container(
alignment: Alignment.center,
child: Text("Taxze Hello"), ,
)
很简单的一个小例子,它的结构也很清晰:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yhsYCYUC-1669544494705)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/ef477b4af1b849f3a3cf5aa3f68c8466~tplv-k3u1fbpfcp-watermark.image?)]
当Flutter把三棵树都构建完后:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zinkstt2-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/58bc0cb3d88a4dc1b08305026eef423a~tplv-k3u1fbpfcp-watermark.image?)]
那么当我们改变文本时,又会发生什么呢?
最先改变的当然是组件层:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-94R0jawX-1669544494706)(https://p1-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/313481d32f85479c8eb3dd8ffe47b545~tplv-k3u1fbpfcp-watermark.image?)]
我们会有一个 “新” 的组件树。不过你真的认为都是新的吗?Flutter会充分利用现有的元素,让我们来看下这个名为canUpdate
的方法吧。
static bool canUpdate(Widget oldWidget, Widget newWidget)
return oldWidget.runtimeType == newWidget.runtimeType
&& oldWidget.key == newWidget.key;
通过这个方法,Flutter可以检查一个老的组件的Type
和key
,并把它和新的组件进行比较。如果它们都相同的话,就不需要更新。
所以就算更新后,Container
更新之后它还是存在的,而且我们没有给它一个Key,所以OldContainer
和NewContainer
是完全相同的。Align、Text、以及RichText它们的Type和Key都没有变化,重新构建它们没有什么意义,所以它们都不会有更新。
到这里,我猜你肯定会问,都没有更新,那么文本是如何改变的呢?
那么我们就要讲到组件中的属性了。组件除了具有Type和Key之外,还有属性。属性的改变会使RenderParagraph
显示新的文本。
不过关于文本的更改渲染到现在我们都是在纸上谈兵,那么我们现在就来用一个简单的例子去验证之前的结论。
bool _isFirst = true;
@override
Widget build(BuildContext context)
return Scaffold(
floatingActionButton: FloatingActionButton(
child: const Icon(Icons.swap_horiz),
onPressed: ()
setState(()
_isFirst = !_isFirst;
);
,
),
body: _isFirst ? first() : second());
Widget first() => Container(
alignment: Alignment.center,
child: const Text("Taxze First"),
);
Widget second() => Container(
alignment: Alignment.center,
child: const Text("Taxze Second"),
);
非常简单的一个例子,点击按钮更改显示文字。当我们点下按钮时,文本改变后,所有的组件都会重用,Flutter只会重建RenderPadding
。
绘制层
在渲染层中,我们最后发生文本变化都在RenderParagraph
上,不过RenderParagraph
并不会直接的绘制文本,而是会创建一TextPainter
来管理绘制的工作。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JgUMZp4Z-1669544494706)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/f358e123221c48ef8891ea7a5ef49620~tplv-k3u1fbpfcp-watermark.image?)]
不过,TextPainter
做的事和它的名字完全不一样,你以为就是它来绘制文本的吗?No~
实际上,它只是负责管理绘制的事,但它自己不会去绘制(当老板)。
基础层
到现在为止,你会发现,讲了那么多,但是还是没有那个ta去真正的绘制文本,就好像之前的所有的组件都在当中间商,把活外包了出去。
到了Flutter的最底层,你会发现有一个ParagraphBuilder
和Paragraph
,在前面关于Flutter如何绘制文本中,我们也提到了Flutter
最后绘制文本时都是通过Paragraph
完成的,而TextPainter
是负责创建ParagraphBuilder
的,但是当你翻看Paragraph
类的源码时,你会发现,大部分的函数都是空函数,原来这哥们也没干活啊!
@pragma('vm:entry-point')
class Paragraph extends NativeFieldWrapperClass1
@pragma('vm:entry-point')
Paragraph._();
bool _needsLayout = true;
double get width native 'Paragraph_width';
double get height native 'Paragraph_height';
double get longestLine native 'Paragraph_longestLine';
double get minIntrinsicWidth native 'Paragraph_minIntrinsicWidth';
double get maxIntrinsicWidth native 'Paragraph_maxIntrinsicWidth';
double get alphabeticBaseline native 'Paragraph_alphabeticBaseline';
...
引擎层
当Paragraph
和ParagraphBuilder
这两个类都将绘制的工作交给了Flutter Engine
后,我们也要将视线放到SkParagraph
上了,在以前Flutter Engine
处理文本绘制的库是LibText
。后面切换成了SkParagraph
,但是也实现了和Libtext
相同的API。对于Flutter引擎在这篇文章中只做一个简单的说明,若对引擎感兴趣的朋友可以自己编译FlutterEngine进行学习,或者在线阅读。
–更详细更深入的Flutter文本渲染原理有兴趣的朋友可以阅读这篇文章
解决Flutter文本基线不对齐的问题
经常在各大Flutter交流群中看到有哥们问这样的问题:Row中,两个文本没有对齐,这怎么处理呀?
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jLL0YCuo-1669544494707)(https://p6-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/327583bd15264122a5b1e9c4bc2c0aca~tplv-k3u1fbpfcp-watermark.image?)]
展示图代码:
Center(
child: Row(
children: [
ColoredBox(
color: Colors.amber,
child: Text.rich(TextSpan(children: [
TextSpan(text: "¥999", style: TextStyle(fontSize: 28)),
TextSpan(text: ".9", style: TextStyle(fontSize: 14)),
])),
),
ColoredBox(
color: Colors.red,
child: Text.rich(TextSpan(children: [
TextSpan(text: "123", style: TextStyle(fontSize: 12)),
])),
),
],
),
)
其实处理这个问题很简单,只需要给Row加上:
textBaseline: TextBaseline.alphabetic,
crossAxisAlignment: CrossAxisAlignment.baseline,
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-JFwhozn4-1669544494707)(https://p3-juejin.byteimg.com/tos-cn-i-k3u1fbpfcp/add602d2e218444e8ccf7c3f157b108b~tplv-k3u1fbpfcp-watermark.image?)]
关于更多有关文本的布局问题大家可以查看官方这篇文档。
尾述
在这篇文章中,我们知道了文本是由什么组成的,Flutter是怎样将文本显示到屏幕上的。但是这也只是Flutter关于文本的一小部分,关于文本的编辑…等内容将会在后续的文章中继续探索。希望这篇文章能对你有所帮助,有问题欢迎在评论区留言讨论~
参考&推荐阅读
Flutter Text Rendering — @Jonathan Sande
书后拓展:Flutter 中一行文字到屏幕上,渲染全过程! — @MeandNi
Flutter 小技巧之玩转字体渲染和问题修复 — @恋猫de小郭
Flutter学习:使用CustomPaint绘制文字 — @菠萝橙子丶
关于我
Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里
如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章——万一哪天我进步了呢?😝
— 文字是人类用符号记录表达信息以传之久远的方式和工具。
以上是关于Flutter中那些你需要知道的文本知识!的主要内容,如果未能解决你的问题,请参考以下文章