前言
这一篇文章我来带大家一起来阅读一下flutter框架中的文本组件Text相关源码,对于文本组件Text的使用大家应该是十分的熟悉,但其内部实现机制应该是很少有人去了解。当你了解完text的内部实现后,你会发现你完全可以通过自定义自己的Text来实现自己的文本内容和布局方式。话不多说,让我们一起进入flutter世界,一睹Text组件的真正容貌吧。
第一节:Text之TextSpan树的形成
阅读过Text源码的人都应该知道Text最终是由TextSpan组成的树形结构,以下为Text类中的两个数据参数:
final String data;
final InlineSpan textSpan;
以下为构建“data”数据参数的构造函数:
const Text(
this.data, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
}) :
textSpan = null,
super(key: key);
上图代码中可以看到该构造函数只能传入“data”作为必要的参数,并且“textSpan = null”。
const Text.rich(
this.textSpan, {
Key key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
this.textScaleFactor,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
}) :
data = null,
super(key: key);
而上图代码中只能传入“textSpan”作为必要参数,并且“data = null”。
这里可以大概明了类Text只有两种构造方式:
- 将参数“data”作为构造源,此时忽略了"textSpan"参数。
- 将参数“textSpan"作为构造源,此时忽略了“data”参数。
我们再来看看Text类的“build“函数的主要逻辑:
@override
Widget build(BuildContext context) {
...
Widget result = RichText(
textAlign: textAlign ?? defaultTextStyle.textAlign ?? TextAlign.start,
textDirection: textDirection, // RichText uses Directionality.of to obtain a default if this is null.
locale: locale, // RichText uses Localizations.localeOf to obtain a default if this is null
softWrap: softWrap ?? defaultTextStyle.softWrap,
overflow: overflow ?? defaultTextStyle.overflow,
textScaleFactor: textScaleFactor ?? MediaQuery.textScaleFactorOf(context),
maxLines: maxLines ?? defaultTextStyle.maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis ?? defaultTextStyle.textWidthBasis,
textHeightBehavior: textHeightBehavior ?? defaultTextStyle.textHeightBehavior ?? DefaultTextHeightBehavior.of(context),
text: TextSpan(
style: effectiveTextStyle,
text: data,
children: textSpan != null ? <InlineSpan>[textSpan] : null,
),
);
...
return result;
}
上图中我们主要观察"RichText"构造时传入的参数“text”,该text是一个"TextSpan"对象,该对象有两个主要的数据源:
- text - String类型,当类Text采用第一种传入“data”的构造方式时,该text才有值。
- children - List<InlineSpan>?类型,当类Text采用第二种传入"textSpan"的构造方式时,该children才不会为null。
从以上分析中得到这样的论述:
- 当以“data”构造Text时,树形中只会存在一个根TextSpan。
- 当以"textSpan"构造Text时,树形中会存在多个TextSpan子树结构。
我们具体看图:
1. 当以“data”构造Text时的树结构:

2. 当以"textSpan"构造Text时的树结构:

所以总结下来,不管Text是如何构建,内部总是会形成一棵TextSpan树。我们来看下面的例子:
Text.rich(
TextSpan(
text: "ABC\n",
children: [
TextSpan(
text: "DE\n",
children: [
TextSpan(
text: "F\n",
style: TextStyle(
fontSize: 12
)
)
]
),
TextSpan(
text: "HIJ\n",
style: TextStyle(
color: Colors.green
),
children: [
TextSpan(
text: "K\n",
style: TextStyle(
color: Colors.black
)
),
TextSpan(
text: "LMN\n",
)
]
)
]
),
style: TextStyle(
fontSize: 24,
color: Colors.red
)
)
显示效果如下:

代码结合上图图片显示可知,代码中的顺序为从上往下来显示每个TextSpan数据,这是符合用户角度的。
我们再来看该段代码演示的树形图:

从图形中的树状结构可知,每个TextSpan中都有“data”和"style"两个字段描述文本,其中“data”是文本字符内容,而“style”是用来修饰文本的显示风格的,我们称为文本样式。可以看到,如果子节点的文本样式为空或者样式中的某个属性没有,它就会继承父样式中的属性,这里采用的是就近原则,直到找到根Root的样式。当然如果根样式中依然未找到,就会使用默认值,像字体fontSize的默认值为14,颜色color的默认值为黑色等等。
那么,对于这种树形特性,其内部是如何工作的呢?我们知道Text类的构建函数“build“最终返回的是RichText类对象,我们再深入RichText类代码中一探究竟:
class RichText extends MultiChildRenderObjectWidget {
......
@override
RenderParagraph createRenderObject(BuildContext context) {
assert(textDirection != null || debugCheckHasDirectionality(context));
return RenderParagraph(text,
textAlign: textAlign,
textDirection: textDirection ?? Directionality.of(context),
softWrap: softWrap,
overflow: overflow,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
locale: locale ?? Localizations.localeOf(context, nullOk: true),
);
}
......
}
可以看到RichText类创建的渲染对象为RenderParagraph,并且将TextSpan的根节点“text”传入,其余参数我们暂且不讨论。继续看RenderParagraph类的关键代码:
@override
void performLayout() {
final BoxConstraints constraints = this.constraints;
_layoutChildren(constraints);
_layoutTextWithConstraints(constraints);
_setParentData();
......
}
该函数为RenderParagraph类的布局函数,布局函数主要是计算RenderParagraph的尺寸和计算孩子们的尺寸及位置的,这里请注意RenderParagraph的孩子们的类型并不是TextSpan,而是PlaceholderSpan类型。请看以下继承结构类图:

所以TextSpan树形结构中可能还存在PlaceholderSpan类型的节点,PlaceholderSpan其实是占位节点,它的绘制工作不由RenderParagraph负责,RenderParagraph只负责它的布局来确定PlaceholderSpan的大小和位置。PlaceholderSpan意思其实已经很明确了,就是树中的某个节点位置由我先来占用,RenderParagraph在布局的时候先将这个位置预留给我,绘制的时候我将内容绘制到这个位置上即可,这样一来不管是TextSpan还是PlaceholderSpan,都是按照它们在树中的顺序来排列的。目前flutter中PlaceholderSpan只衍生出一个WidgetSpan,该类中保存了一个Widget对象,意味着在TextSpan树中可以放置任何的组件了,你只需要使用WidgetSpan即可,说白了就是文本内容中除了文字还可以包含图片、视频、甚至你自定义的控件等等内容。这里我们暂且只了解一下PlaceholderSpan的作用,具体我们还是以TextSpan来作说明。
我们再回到RenderParagraph类的布局函数“performLayout“中,从上往下看,这里只是顺带解释一下每个函数的作用,跟主题不太相关的代码我们后面再讨论:
- _layoutChildren(constraints) - 布局孩子们,前面做了说明,RenderParagraph类的孩子属性“children“中只包含PlaceholderSpan节点,所以该函数会调用占位节点的布局函数主要来确定他们的尺寸size,方便在后面来确定他们在父RenderParagraph中的偏移位置。
- _layoutTextWithConstraints(constraints) - 布局文本text,RenderParagraph类中的文本text属性在设置text时就已经保存在了“_textPainter“对象中。注意此处的text为TextSpan树形中的根节点。我们应该知道在布局text时肯定是需要知道PlaceholderSpan占位节点的尺寸数据的,这样才能确定好TextSpan和PlaceholderSpan的位置,因为整个树形是由TextSpan和PlaceholderSpan构成的。
- _setParentData() - 设置孩子们的父数据,这个父数据中主要包含孩子们左上点的坐标,也就是说该函数一旦调用,PlaceholderSpan节点在父RenderParagraph中的偏移位置就设置好了。注意TextSpan和PlaceholderSpan偏移位置的计算都是在“_layoutTextWithConstraints”函数中完成的,这个函数只是做了简单的设置而已。
了解了以上三个函数的作用后,我们知道主要的布局逻辑在"_layoutTextWithConstraints"函数中:
void _layoutTextWithConstraints(BoxConstraints constraints) {
_textPainter.setPlaceholderDimensions(_placeholderDimensions);
_layoutText(minWidth: constraints.minWidth, maxWidth: constraints.maxWidth);
}
可以看到该函数中需要先将占位节点尺寸数据设置完毕后才调用"_layoutText"函数进行布局,我们继续看“_layoutText”函数:
void _layoutText({ double minWidth = 0.0, double maxWidth = double.infinity }) {
final bool widthMatters = softWrap || overflow == TextOverflow.ellipsis;
_textPainter.layout(
minWidth: minWidth,
maxWidth: widthMatters ?
maxWidth :
double.infinity,
);
}
该函数最终会调用到“_textPainter”的"layout"函数进行布局,该“_textPainter”属性的类型为TextPainter,在调用该函数之前我们知道TextSpan的根节点"text"和占位节点的尺寸已经被设置进“_textPainter”对象了,再进入TextPainter类的布局函数“layout”:
void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
if (!_needsLayout && minWidth == _lastMinWidth && maxWidth == _lastMaxWidth)
return;
_needsLayout = false;
if (_paragraph == null) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
_text!.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions);
_inlinePlaceholderScales = builder.placeholderScales;
_paragraph = builder.build();
}
_lastMinWidth = minWidth;
_lastMaxWidth = maxWidth;
_previousCaretPosition = null;
_previousCaretPrototype = null;
_paragraph!.layout(ui.ParagraphConstraints(width: maxWidth));
......
}
语句“ final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle())”会创建一个段落构建器ui.ParagraphBuilder,会通过"_createParagraphStyle"创建一个段落样式ui.ParagraphStyle(段落样式包含三个部分:textStyle + strutStyle + 其余属性)并传入该段落构建器中。我们进入ui.ParagraphBuilder(ui.ParagraphStyle)最终的调用点:
void _constructor(
Int32List encoded,
ByteData? strutData,
String? fontFamily,
List<dynamic>? strutFontFamily,
double? fontSize,
double? height,
String? ellipsis,
String locale
) native 'ParagraphBuilder_constructor';
可以看到最终会调用到flutter中engine层c++中一个“ParagraphBuild_constructor“函数。
语句“_text!.build(builder, textScaleFactor: textScaleFactor, dimensions: _placeholderDimensions)”是最为关键的代码语句,它会按照树中的顺序向flutter的engine层添加数据。我们知道"_text"为TextSpan的根节点,所以他会调用TextSpan类的build函数,以下为TextSpan类build函数中的代码逻辑:
void build(
ui.ParagraphBuilder builder, {
double textScaleFactor = 1.0,
List<PlaceholderDimensions>? dimensions,
}) {
final bool hasStyle = style != null;
if (hasStyle)
builder.pushStyle(style!.getTextStyle(textScaleFactor: textScaleFactor));
if (text != null)
builder.addText(text!);
if (children != null) {
for (final InlineSpan child in children!) {
assert(child != null);
child.build(
builder,
textScaleFactor: textScaleFactor,
dimensions: dimensions,
);
}
}
if (hasStyle)
builder.pop();
}
从上图中我们知道它是如何向flutter中engine层添加数据的,主要分为以下两种情况:
1. 当TextSpan提供style样式时,调用顺序为:
builder.pushStyle() -> builder.addText() -> 循环孩子们,调用孩子的"build"函数 -> builder.pop()
2. 当TextSpan不提供style样式时,调用顺序为:
builder.addText() -> 循环孩子们,调用孩子的"build"函数
这里我就不贴具体代码了,以下我简述下framework层和engine层函数的对应关系:
- builder.pushStyle() -- ParagraphBuilder_pushStyle
- builder.addText() -- ParagraphBuilder_addText
- builder.pop() -- ParagraphBuilder_pop
当然当树中存在PlaceholderSpan节点时,也会调用到PlaceholderSpan类的build函数,我们暂且不讨论有PlaceholderSpan节点的情况。从上面的讨论我们得知,flutter是按照什么顺序将TextSpan树添加到engine层的:从上往下,从左往右。如下图:

我们再回到“TextPainter”类的布局函数“layout”中,将TextSpan树中的数据添加到engine层后,会调用到 ui.ParagraphBuilder对象的build()函数:
Paragraph build() {
final Paragraph paragraph = Paragraph._();
_build(paragraph);
return paragraph;
}
void _build(Paragraph outParagraph) native 'ParagraphBuilder_build';
可以看到builder.build()对应engine中的"ParagraphBuilder_build"函数。builder.build()会返回一个ui.Paragraph类型的段落对象paragraph,当"ParagraphBuilder_build"函数结束后,段落对象paragraph就设置完了TextSpan树形中所有数据,往后再调用"paragraph.layout"进行段落的布局即可,我们再看看ui.Paragraph类的布局函数“layout”代码逻辑:
void layout(ParagraphConstraints constraints) => _layout(constraints.width);
void _layout(double width) native 'Paragraph_layout';
可以看到最终的布局逻辑在engine层的“Paragraph_layout”函数中。
第二节:Text之engine层的TextSpan数据添加过程
根据第一节内容我们了解到,Text主要的布局逻辑在TextPainter类的“layout”函数中,该函数有如下调用流程:
- 调用ui.ParagraphBuilder类的构造函数,创建出一个builder对象。(该对象是framework层和engine层构建TextSpan数据沟通的桥梁)
- 调用根节点TextSpan的build函数向engine层追加所有节点数据。
- 调用ui.ParagraphBuilder类型对象builder的build()函数创建一个ui.Paragraph类型的段落对象_paragraph。
- 调用ui.Paragraph类型的_paragraph对象的布局函数"layout"进行布局。
那么在engine层的函数调用顺序如下:
1. 调用:"ParagraphBuilder_constructor"
该函数位置:engine/lib/ui/text/paragraph_builder#ParagraphBuilder_constructor(Dart_NativeArguments)
2. 构建TextSpan树中的数据分以下两种情况(此处举例都有“data”文本属性的情况):
- 当TextSpan存在style属性时:
“ParagraphBuilder_pushStyle“ -> "ParagraphBuilder_addText" -> 循环孩子们追加孩子的TextSpan数据 -> "ParagraphBuilder_pop"
- 当TextSpan不存在style属性时:
"ParagraphBuilder_addText" -> 循环孩子们追加孩子的TextSpan数据
“ParagraphBuilder_pushStyle“函数位置:engine/lib/ui/text/paragraph_builder#pushStyle(...)
“ParagraphBuilder_addText”函数位置:engine/lib/ui/text/paragraph_builder#addText(...)
“ParagraphBuilder_pop”函数位置:engine/lib/ui/text/paragraph_builder#pop()
3. 调用:"ParagraphBuilder_build"
该函数位置:engine/lib/ui/text/paragraph_builder#build(Dart_Handle)
4. 调用:"Paragraph_layout"
该函数位置:engine/lib/ui/text/paragraph#layout(double width)
好了,了解了engine层的这些函数后,我们再按照调用顺序深入到这些函数内部看看是如何实现的:
第一步:调用“engine/lib/ui/text/paragraph_builder#ParagraphBuilder_constructor(Dart_NativeArguments)“函数
static void ParagraphBuilder_constructor(Dart_NativeArguments args) {
UIDartState::ThrowIfUIOperationsProhibited();
DartCallConstructor(&ParagraphBuilder::create, args);
}
该函数第二个语句中会传入“ParagraphBuilder::create”函数的引用到DartCallConstructor(Sig,Dart_NativeArguments)函数中。“DartCallConstructor“函数我们点到为止,该函数会处理从framework层传过来的参数,并调用“ParagraphBuilder::create”函数。
fml::RefPtr<ParagraphBuilder> ParagraphBuilder::create(
tonic::Int32List& encoded,
Dart_Handle strutData,
const std::string& fontFamily,
const std::vector<std::string>& strutFontFamilies,
double fontSize,
double height,
const std::u16string& ellipsis,
const std::string& locale) {
return fml::MakeRefCounted<ParagraphBuilder>(encoded, strutData, fontFamily,
strutFontFamilies, fontSize,
height, ellipsis, locale);
“ParagraphBuilder::create”函数最终会借助fml::MakeRefCounted创建引用计数器函数返回一个"fml::RefPtr"对象,其中摸版参数类型为ParagraphBuilder,在c++中最终会通过摸版类型调用ParagraphBuilder的构造函数创建出一个ParagraphBuilder对象:
ParagraphBuilder::ParagraphBuilder(
tonic::Int32List& encoded,
Dart_Handle strutData,
const std::string& fontFamily,
const std::vector<std::string>& strutFontFamilies,
double fontSize,
double height,
const std::u16string& ellipsis,
const std::string& locale) {
int32_t mask = encoded[0];
txt::ParagraphStyle style;
......
#if FLUTTER_ENABLE_SKSHAPER
#define FLUTTER_PARAGRAPH_BUILDER txt::ParagraphBuilder::CreateSkiaBuilder
#else
#define FLUTTER_PARAGRAPH_BUILDER txt::ParagraphBuilder::CreateTxtBuilder
#endif
m_paragraphBuilder =
FLUTTER_PARAGRAPH_BUILDER(style, font_collection.GetFontCollection());
}
注意观察构造函数中最后一个语句,会通过“txt::ParagraphBuilder::CreateSkiaBuilder”(当FLUTTER_ENABLE_SKSHAPER标志打开时)或“txt::ParagraphBuilder::CreateTxtBuilder”函数创建出一个“text:ParagraphBuilder”类型的m_paragraphBuilder对象,“text:ParagraphBuilder”类位置:engine/third_party/txt/src/txt/ParagraphBuilder。我们深入函数"CreateTxtBuilder"(“CreateSkiaBuilder”函数这里不做讨论,实现方式都类似)中做分析:
std::unique_ptr<ParagraphBuilder> ParagraphBuilder::CreateTxtBuilder(
const ParagraphStyle& style,
std::shared_ptr<FontCollection> font_collection) {
return std::make_unique<ParagraphBuilderTxt>(style, font_collection);
}
该函数会借助std::make_unique函数创建出一个std:unique_ptr对象,其摸版参数类型为ParagraphBuilderTxt,最终也会调用到ParagraphBuilderTxt类的构造函数:
ParagraphBuilderTxt::ParagraphBuilderTxt(
const ParagraphStyle& style,
std::shared_ptr<FontCollection> font_collection)
: font_collection_(std::move(font_collection)) {
SetParagraphStyle(style);
}
可以看到最终会调用到"SetParagraphStyle"函数,从函数名称可知该函数为设置段落样式的,我们进入函数中:
void ParagraphBuilderTxt::SetParagraphStyle(const ParagraphStyle& style) {
paragraph_style_ = style;
paragraph_style_index_ = runs_.AddStyle(style.GetTextStyle());
runs_.StartRun(paragraph_style_index_, text_.size());
}
这里便一目了然了,段落样式paragraphStyle是用户给Text.rich或Text设置的根属性值的集合,前面我们已经提到过段落样式包括三种数据:textStyle、strutStyle、其他属性。语句“runs_AddStyle(style.GetTextStyle())”会将段落样式中的文本样式textStyle添加到runs_中,该函数会返回runs_中文本样式的位置下标值,此时应该为0。runs_属性类型为StyledRuns,我们进入StyledRuns类分析下代码:
class StyledRuns {
public:
struct Run {
const TextStyle& style;
size_t start;
size_t end;
};
......
struct IndexedRun {
size_t style_index = 0;
size_t start = 0;
size_t end = 0;
explicit IndexedRun(size_t style_index, size_t start, size_t end)
: style_index(style_index), start(start), end(end) {}
};
std::vector<TextStyle> styles_;
std::vector<IndexedRun> runs_;
};
该类一共有两种数据:列表styles_和列表runs_。其中styles_列表中存的是TextStyle类型的对象(每一种TextSpan使用的文本样式都保存在这个集合中)。runs_中存的是IndexedRun类型的对象,IndexedRun为一个结构类型,下面对IndexedRun中的三个字段做一下说明:
- style_index - 为列表styles_中的某个文本样式的下标值,在start ~ end之间(不包括end)的字符所应用的文本样式。
- start - 为文本字符列表中的以某个字符开始的下标值。
- end - 为文本字符列表中的以某个字符结束的边界值,该结束边界值为:该字符下标值 + 1。
其实对于结构类型Run和IndexedRun是对应的,只不过一个是文本样式textStyle的下标,一个是文本样式textStyle自身而已。也就是说,一般通过IndexedRun对象能拿到一个Run对象。
我们再回到“ParagraphBuilderTxt::SetParagraphStyle(...)”函数中,第二个语句使用“runs_.AddStyle(style.GetTextStyle())“将段落样式中的文本样式添加到styles_列表中,具体“AddStyle(TextStyle)”函数代码如下:
size_t StyledRuns::AddStyle(const TextStyle& style) {
const size_t style_index = styles_.size();
styles_.push_back(style);
return style_index;
}
该函数仅仅是添加一个文本样式到styles列表的末尾并返回该文本样式在styles列表中的位置下标值。
再看“ParagraphBuilderTxt::SetParagraphStyle(...)”函数中最后一条语句“runs_.StartRun(paragraph_style_index_, text_.size())”,这条语句有什么作用呢?该语句调用了一下runs对象的“StartRun(...)”函数,该函数第一个参数为文本样式的下标值,第二个参数为字符列表的数量(这里的text_为std::vector<uint16_t>类型,会按照顺序保存TextSpan树中的每一个字符)。我们来分析下"_StartRun(...)"函数:
void StyledRuns::StartRun(size_t style_index, size_t start) {
EndRunIfNeeded(start);
runs_.push_back(IndexedRun{style_index, start, start});
}
首先会调用EndRunIfNeeded(size_t)函数来处理属性"runs_"列表中最后一条数据,接着调用"push_back"函数向“runs_“列表末尾追加一条IndexedRun数据。我们之前讨论过,每一个IndexedRun对象代表的是用字符的开始下标位置和结束边界来描述某段字符所使用的样式,可以看到此处start=text_.size(),也就是说该函数中的开始位置和结束边界为暂时为字符列表的总数量。这里先不去理解为什么开始位置和结束边界值是相同的,我们先来看EndRunIfNeeded(size_t)函数是如何来处理“runs_”列表的最后一条数据的:
void StyledRuns::EndRunIfNeeded(size_t end) {
if (runs_.empty())
return;
IndexedRun& run = runs_.back();
if (run.start == end) {
// The run is empty. We can skip it.
runs_.pop_back();
} else {
run.end = end;
}
}
函数开头做了非空判断是没毛病的,接着会取出runs_列表的最后一条数据,有如下两种情况的处理方式:
1. 当run.start == end时。
此处的end为现有text_字符列表的总数量,而run.start为现有字符段样式描述列表runs_最后一个样式的字符段开始下标值,当这两个值一样的时候,会调用"pop_back()"函数将最后一个字符段样式描述删除。
2. 当run.start != end时。
此处只是简单地更改了一下现有字符段样式描述列表runs_最后一个样式的字符段结束边界值为现有text_字符列表的总数量。
讲到这里可能大家还是不清楚这段代码逻辑的作用,我们重新整理一下追加TextSpan数据对应engine层函数调用顺序:
“ParagraphBuilder_pushStyle“ -> "ParagraphBuilder_addText" -> 循环孩子们追加孩子的TextSpan数据 -> "ParagraphBuilder_pop"
我们分别看一下这些函数的代码逻辑:
std::vector<size_t> style_stack_;
void ParagraphBuilderTxt::PushStyle(const TextStyle& style) {
size_t style_index = runs_.AddStyle(style);
style_stack_.push_back(style_index);
runs_.StartRun(style_index, text_.size());
}
PushStyle(const TextStyle&)函数最终会调用到“StartRun(...)“函数,并且会先将字符样式的位置暂时添加到style_stack_栈中,如果它没有孩子数据调用完“AddText(...)”函数后会调用“Pop()”函数马上出栈。该函数最终会进入到EndRunIfNeeded(size_t end)函数中,这是需要的,因为可能会发生这样的两个场景:
- 当其前面的TextSpan数据都是无样式的,此时需要触发“run.start != end“逻辑,目的是设置上一个文本字符段样式描述的结束位置的边界值。因为这中间有很多无样式的TextSpan的文本字符需要设置样式为其最近的祖先的样式,这里如果存在上一个文本字符段样式,一定是最接近的祖先样式。(它的祖先如果有样式,肯定都只调用了"PushStyle(...)"函数并没有调用"Pop()函数,这些祖先都是按照先进后出的顺序入栈style_stack_的,那么跟它最近的祖先肯定是最后入栈的,也就是在style_stack_栈顶,当然是先拿它的文本样式了)
- 当其同级的上一个TextSpan数据是有样式,此时肯定已经调用了“Pop()”函数,因为“Pop”函数肯定也会调用到“StartRun()“函数中,最终会向runs_列表中添加一个字符段样式描述对象IndexedRun,该对象中的文本样式的下标现在指向的是当前文本样式出栈后的父样式(当前style_stack_中保留的永远是父样式的下标,因为当前样式和前面同级的样式已经出栈了。此处保留父样式下标的目的是为了让后续同级的没有样式的字符串文本继承这个样式),如果当前已经有了样式肯定是需要通过调用EndRunIfNeeded(size_t end)函数进入“run.start == end”逻辑将这个无用的字符段样式描述对象IndexedRun删除掉;如果当前没有样式但下一个文本数据有样式也是需要调用EndRunIfNeeded(size_t end)函数进入“run.start != end”逻辑将当前没有样式的文本字符串的样式指向父样式(发生在下一个文本数据添加样式的时候,因为本次的Pop()函数已经将栈顶的父样式拿到了runs_列表末尾)。
void ParagraphBuilderTxt::AddText(const std::u16string& text) {
text_.insert(text_.end(), text.begin(), text.end());
}
AddText(const std::u16string&)函数是将每一个TextSpan中的字符串追加到字符列表text_的末尾。
void ParagraphBuilderTxt::Pop() {
if (style_stack_.empty()) {
return;
}
style_stack_.pop_back();
runs_.StartRun(PeekStyleIndex(), text_.size());
}
size_t ParagraphBuilderTxt::PeekStyleIndex() const {
return style_stack_.size() ? style_stack_.back() : paragraph_style_index_;
}
Pop()函数最终也会调用到“StartRun(...)”函数,但是会先将style_stack_列表中的最后一个文本样式的位置移除。此时在“StartRun(...)”中会做两件事:
- 调用EndRunIfNeeded(size_t end)函数进入“run.start != end“逻辑更改本次字符段样式描述的结束边界值,让本次文本样式对应好本次的文本字符串。
- 将父样式添加到runs_的末尾,目的是让后面无样式的文本数据继承它。
第二步:向engine层添加TextSpan树中的所有数据,调用“engine/lib/ui/text/paragraph_builder#pushStyle(...)“、“engine/lib/ui/text/paragraph_builder#addText(...)“、“engine/lib/ui/text/paragraph_builder#pop()“
此时有以下三种情况:
- 有文本样式无文本数据:pushStyle(...) -> 追加孩子们的TextSpan数据 -> pop()
- 有文本数据无文本样式:addText(...) -> 追加孩子们的TextSpan数据
- 既有文本样式又有文本数据:pushStyle(...) -> addText(...) -> 追加孩子们的TextSpan数据 -> pop()
当添加文本样式时,首先会调用“engine/lib/ui/text/paragraph_builder#pushStyle(...)“函数,以下为该函数代码实现:
void ParagraphBuilder::pushStyle(tonic::Int32List& encoded,
const std::vector<std::string>& fontFamilies,
double fontSize,
double letterSpacing,
double wordSpacing,
double height,
double decorationThickness,
const std::string& locale,
Dart_Handle background_objects,
Dart_Handle background_data,
Dart_Handle foreground_objects,
Dart_Handle foreground_data,
Dart_Handle shadows_data,
Dart_Handle font_features_data) {
FML_DCHECK(encoded.num_elements() == 8);
int32_t mask = encoded[0];
// Set to use the properties of the previous style if the property is not
// explicitly given.
txt::TextStyle style = m_paragraphBuilder->PeekStyle();
// Only change the style property from the previous value if a new explicitly
// set value is availabl
if (mask & tsColorMask) {
style.color = encoded[tsColorIndex];
}
......
m_paragraphBuilder->PushStyle(style);
}
以上代码中首先会通过m_paragraphBuilder->PeekStyle()获取到父样式所有属性到style对象中,然后再添加自己文本样式的属性值到style对象中,此时style中便继承了父样式中的属性。如下为PeekStyle函数代码:
const TextStyle& ParagraphBuilderTxt::PeekStyle() {
return runs_.GetStyle(PeekStyleIndex());
}
可以看到代码中使用"PeekStyleIndex()"获取栈顶的文本样式下标值,之前已经谈到栈顶的文本样式永远是跟当前子样式最接近的父样式,如下图:

根据上图所示,如果添加当前TextSpan文本样式S3时,此时栈顶style_stack_中的样式肯定是最接近它的父样式S2了,也就说明在“ParagraphBuilder::pushStyle(...)”函数中语句“m_paragraphBuilder->PeekStyle()“获取到的样式一定是其父样式。注意看“txt::TextStyle style = m_paragraphBuilder->PeekStyle()”语句,此时style指向的对象和父样式指向的对象一定不是同一个对象,所以这里只是复制了父样式的值(这里大家可以搜索一下c++引用赋值和直接赋值的区别,这里为直接赋值,此时style会先调用其默认构造函数,再调用其默认的赋值函数将父样式对象传入)。如果是同一个对象,那么下面设置新的样式S3的属性值时就会影响到父样式S2的值了,这样的逻辑显然是不正确的。
最后当新的style属性全部设置完后,便会调用m_paragraphBuilder->PushStyle(style)函数保存文本样式,也就走到了上面提到的ParagraphBuilderTxt::PushStyle(const TextStyle& style)函数中,这里就不再重复解释,前面对该函数已经做过了详细的分析。对于其他两个函数ParagraphBuilder::addText(const std::u16string& text)和ParagraphBuilder::pop()没有比较重要的处理,最后会分别调用到:ParagraphBuilderTxt::AddText(const std::u16string& text)和ParagraphBuilderTxt::Pop()。
最后当第二步走完后,ParagraphBuilderTxt类中对应的属性在内存中的表现该是什么样呢?我们结合之前的一张图片来说明下:

上图中的T标识每一个节点本身,S标识每一个节点的样式,D标识每一个节点的文本数据。当T₇的数据添加完毕后,此时engine层内存中的各数据如下图所示:

第三步:调用engine/lib/ui/text/paragraph_builder#build(Dart_Handle)函数
void ParagraphBuilder::build(Dart_Handle paragraph_handle) {
Paragraph::Create(paragraph_handle, m_paragraphBuilder->Build());
}
可以看到函数中会调用到Paragraph类的Create(...)函数,该函数内部会创建一个Paragraph对象(该对象来自于第二个参数“m_paragraphBuilder->Build()”函数的返回值),用于和framework层的ui.Paragraph对象进行交互。第二个参数中的“Build()“函代码如下:
std::unique_ptr<Paragraph> ParagraphBuilderTxt::Build() {
runs_.EndRunIfNeeded(text_.size());
std::unique_ptr<ParagraphTxt> paragraph = std::make_unique<ParagraphTxt>();
paragraph->SetText(std::move(text_), std::move(runs_));
paragraph->SetInlinePlaceholders(std::move(inline_placeholders_),
std::move(obj_replacement_char_indexes_));
paragraph->SetParagraphStyle(paragraph_style_);
paragraph->SetFontCollection(font_collection_);
SetParagraphStyle(paragraph_style_);
return paragraph;
}
本文详细介绍了Flutter中Text组件的内部实现,特别是TextSpan树的形成过程。Text组件的构建有两种方式,分别基于"data"和"textSpan"。在构建过程中,TextSpan树被用来表示文本内容,最终通过TextPainter布局到屏幕上。在engine层,TextSpan数据按照树形结构添加,遵循从上到下、从左到右的顺序。文章还探讨了TextSpan与engine层数据的交互,涉及ParagraphBuilder及其在engine层的实现。

2449

被折叠的 条评论
为什么被折叠?



