深色模式
文本渲染相关源码分析
Flutter中,文本渲染相关的核心类是RenderParagraph与TextPainter。
依赖关系
先画一个类图
类型说明
面向用户的类:
Text: 显示文本RichText: 显示文本
文本的配置:
TextStyle: 配置文本样式TextSpan: 文本树的一个结点,组成文本树,每个结点可配置样式TextStyleWidgetSpan: 非文字元素
盒子布局与渲染:
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,这样一来,文本树能实现的效果就很强大了。
