深色模式
文本渲染相关源码分析
Flutter中,文本渲染相关的核心类是RenderParagraph
与TextPainter
。
依赖关系
先画一个类图
类型说明
面向用户的类:
Text
: 显示文本RichText
: 显示文本
文本的配置:
TextStyle
: 配置文本样式TextSpan
: 文本树的一个结点,组成文本树,每个结点可配置样式TextStyle
WidgetSpan
: 非文字元素
盒子布局与渲染:
RenderParagraph
: 负责文本的布局与渲染,继承RenderBox
文本布局与渲染:
TextPainter
: 负责文本树的构建、布局、渲染,它是文本渲染逻辑的核心
sky_engine层:
Paragraph
: 文本排版Canvas
: 绘制
源码分析
Text
先总结:
- 它是一个显示文本的
Widget
,是基于RichText
的封装。 - 它有2个构造方法,用来创建简单文本,或富文本。
它继承StatelessWidget
,它的build()
方法中,返回一个RichText
对象。简化的代码:
dart
class Text extends StatelessWidget {
@override
Widget build(BuildContext context) {
return RichText();
}
}
2个构造方法,区别在第一个参数上面。
Text()
的第一个参数是String this.data
,这个String
会被转换为单结点的TextSpan
,所以只能显示简单的文本Text.rich()
的第一个参数是InlineSpan this.textSpan
,意味着可以传入一个InlineSpan
树,所以能显示复杂的富文本
dart
const Text(
String this.data, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
this.textScaleFactor,
this.textScaler,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
}) : textSpan = null,
assert(
textScaler == null || textScaleFactor == null,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
);
dart
const Text.rich(
InlineSpan this.textSpan, {
super.key,
this.style,
this.strutStyle,
this.textAlign,
this.textDirection,
this.locale,
this.softWrap,
this.overflow,
@Deprecated(
'Use textScaler instead. '
'Use of textScaleFactor was deprecated in preparation for the upcoming nonlinear text scaling support. '
'This feature was deprecated after v3.12.0-2.0.pre.',
)
this.textScaleFactor,
this.textScaler,
this.maxLines,
this.semanticsLabel,
this.textWidthBasis,
this.textHeightBehavior,
this.selectionColor,
}) : data = null,
assert(
textScaler == null || textScaleFactor == null,
'textScaleFactor is deprecated and cannot be specified when textScaler is specified.',
);
当然,如果要显示富文本,也可以直接使用RichText
类。
RichText
先总结:
RichText
是一个直接参与布局绘制的Widget
- 对应的
RenderBox
是RenderParagraph
它是一个直接参与布局绘制的Widget
:
dart
class RichText extends MultiChildRenderObjectWidget {
}
所有直接参与布局绘制的Widget
,它们本身的代码都很简单,关键的逻辑就在build()
方法返回的RenderObject
中,这里是RenderParagraph
:
dart
@override
RenderParagraph createRenderObject(BuildContext context) {
return RenderParagraph(
// ...
);
}
RenderParagraph
先总结:
- 它是一个多子结点的
RenderBox
- 它遵循盒子模型布局、绘制流程
- 它利用
TextPainter
对文本排版、绘制文本
创建TextPainter
:
它的构造方法的初始化列表中,会创建一个TextPainter
对象,把文本树传递给了TextPainter
。
dart
RenderParagraph(
InlineSpan text,
// ...
) : // ...
_textPainter = TextPainter(
text: text,
textAlign: textAlign,
textDirection: textDirection,
textScaler:
textScaler == TextScaler.noScaling ? TextScaler.linear(textScaleFactor) : textScaler,
maxLines: maxLines,
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
locale: locale,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior,
) {
// ...
}
计算盒子布局、文本排版:
(盒子的布局与绘制流程,这里略过。)
由于RenderParagraph
盒子的大小,受到文本内容大小的影响,所以,在计算盒子布局尺寸的时候,会多次调用文本排版的方法,即TextPainter
的layout()
方法。(这里不一一贴代码了)
绘制:
在RenderParagraph
的paint()
方法中,通过TextPainter
的paint()
方法,绘制所有文本。
dart
@override
void paint(PaintingContext context, Offset offset) {
// ...
_textPainter.paint(context.canvas, offset);
// ...
}
TextPainter
先总结:
TextPainter
持有文本树结构的所有信息,包括每个结点的样式TextPainter
会把用户配置的文本树转换为sky_engine使用的文本段落结构,即Paragraph
。它提供一个
layout()
方法给上层调用,对文本排版。TextPainter
会把Paragraph
交给Canvas
绘制。它提供一个
paint()
方法给上层调用,把文本段落绘制到Canvas
中。
持有文本树:
在TextPainter
的构造方法中,有一个InlineSpan? text
参数,它就是文本树。
文本树转换:
dart
void layout({double minWidth = 0.0, double maxWidth = double.infinity}) {
// ...
final ui.Paragraph paragraph = (cachedLayout?.paragraph ?? _createParagraph(text))
// ...
}
dart
ui.Paragraph _createParagraph(InlineSpan text) {
final ui.ParagraphBuilder builder = ui.ParagraphBuilder(_createParagraphStyle());
text.build(builder, textScaler: textScaler, dimensions: _placeholderDimensions);
_rebuildParagraphForPaint = false;
return builder.build();
}
接上面代码中的text.build()
和builder.build()
,
text.build()
的实现在TextSpan
类中,递归遍历整个树,它转换结果保存在ParagraphBuilder
中。builder.build()
的实现在_NativeParagraphBuilder
中,它对段落进行排版,排版的逻代码用C++实现。
代码如下:
dart
@override
void build(
ui.ParagraphBuilder builder, {
TextScaler textScaler = TextScaler.noScaling,
List<PlaceholderDimensions>? dimensions,
}) {
assert(debugAssertIsValid());
final bool hasStyle = style != null;
if (hasStyle) {
builder.pushStyle(style!.getTextStyle(textScaler: textScaler));
}
if (text != null) {
try {
builder.addText(text!);
} on ArgumentError catch (exception, stack) {
FlutterError.reportError(
FlutterErrorDetails(
exception: exception,
stack: stack,
library: 'painting library',
context: ErrorDescription('while building a TextSpan'),
silent: true,
),
);
// Use a Unicode replacement character as a substitute for invalid text.
builder.addText('\uFFFD');
}
}
final List<InlineSpan>? children = this.children;
if (children != null) {
for (final InlineSpan child in children) {
child.build(builder, textScaler: textScaler, dimensions: dimensions);
}
}
if (hasStyle) {
builder.pop();
}
}
dart
@override
Paragraph build() {
final _NativeParagraph paragraph = _NativeParagraph._();
_build(paragraph);
return paragraph;
}
@Native<Void Function(Pointer<Void>, Handle)>(symbol: 'ParagraphBuilder::build')
external void _build(_NativeParagraph outParagraph); // 基于C++实现字符的排版
绘制:
在TextPainter
的paint()
方法中,调用了Canvas
的drawParagraph()
方法。
dart
void paint(Canvas canvas, Offset offset) {
// ...
canvas.drawParagraph(layoutCache.paragraph, offset + layoutCache.paintOffset);
}
TextStyle
文本样式的配置类,结构很简单。
从应用层的角度来看,它有各种参数,用于配置文本的样式。
从框架层的角度来看,它有一个getTextStyle()
方法,把自身转换为一个ui.TextStyle
类型,Canvas
在执行绘制的时候,需要这个数据。
这里的ui
包,是sky_engine库中的代码,它不属于flutter库。还有Canvas
,也是sky_engine库中的。
InlineSpan
InlineSpan
一族的继承关系,在前面已有展示,这里再画一遍无妨:
InlineSpan
抽象类,其它3个类都是它的子类。
它定义了文本树的基本特点:
- 有一个
TextStyle
属性 - 有一个
build()
方法,用来把树转换为段落信息,保存在ParagraphBuilder
中 - 有一个
visitChildren()
方法,配合子类的实现,递归遍历整个树 - 其它功能性方法
TextSpan
- 有一个
children
属性,它可以有多个子结点 - 在
build()
方法,递归build()
- 在
visitChildren()
方法,递归visitChildren()
🐶
PlaceholderSpan
它代表嵌入文本树中的一个占位内容。
它有2个属性:
alignment
:文本对齐方式baseline
:文本基线
WidgetSpan
继承PlaceholderSpan
,并多了一个child
属性,child
的类型是Widget
,这样一来,文本树能实现的效果就很强大了。