1. はじめに
本記事は、主にFlutterのテキストWidget (Text
, RichText
) における、テキストのレンダリングの仕組みについての調査結果をまとめています (他の記事も同様ですが、基本的に随時更新しています) 。
なお、テキスト系Widgetの基本的な使い方はこちらにまとめていますので、これらのWidgetに対して知識がない方はまずはこちらに目を通して下さい。
2. Flutterレンダリングの基本
2-1. レンダリングフローについて復習
以下にFlutterの基本的なレンダリングフローを示します。FlutterはWebブラウザと似た感じで、画面情報をツリー構造で管理し、最終的にFlutter Engine (C++の描画エンジン) 経由でグラフィックスライブラリのSkia
で描画を行います。
フローとしては、まずWidget
でUI (画面) を構成し、その画面を描画するための各種情報を管理しているのがElement
です。そして、より低レイヤで実際の描画に関わるレイアウトや描画の定義を行うのがRenderObject
です。最終的にFlutter Eingineに渡される情報がLayer
です。そしてFlutter Engine側でSkiaのAPIに変換し、2Dグラフィックス描画 (OpenGL, Vulkan) が行われます。
FlutterのFrameworkが管理する内容としては、画面の構築作業がBuild
工程、実際の描画のためのレイアウト処理がLayout
工程、そして最後の描画指示がPaint
工程です。
2-2. テキストレンダリング関連のソフトウェアスタック
テキストのレンダリングについて、最初にイメージが沸き易いように概要を説明しておきます。
Framework側では、UIとしてのスタイリングや画面内でのレイアウトなどを実現します。しかし、レイアウトに必要なテキストの折り返しや実際の描画領域のサイズの計算、レイアウトなどはEngine側の機能を利用します。一方、Engine側では、Framework側に必要な機能は提供しつつ、指定されたスタイルやフォントでの最終的なテキストの描画処理を実現します。
では、Framewrok部分とEngine部分の詳細をそれぞれ見ていきましょう。
3. Flutter Framework
ここでは、Frameworkレイヤ内部について解説していきます。
3-1. Text/RichText
Text
とRichText
について、それらのWidget, Element, Render Objectの関係性を示します。この図の通り、RenderParagraph
クラスで実際の描画処理を行います。
Text
Text
は、StatelessWidgetの派生クラスです。ソースコードを見ると分かりますが、内部的にはRichText
を利用しており、Widgetのbuild()
時にRicthText
のインスタンスが生成されます。
RichText
RichText
は、MultiChildRenderObjectWidget
の派生クラスです。RenderObjectと名前が付いているので、直接描画に関連するWidgetであろうということがすぐに推測されます。RichTextのソースコードを見てみると、RenderParagraph
というクラスをインスタンスしています。これがテキスト描画の中心になる重要なクラスです。
@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,
applyTextScaleFactorToWidgetSpan: _applyTextScaleFactorToWidgetSpan,
locale: locale ?? Localizations.localeOf(context, nullOk: true),
);
}
3-2. RenderParagraphから先のモジュール構成
RenderParagraph
から繋がる先のモジュール構成を以下に示します。この構成を頭に入れて、後述の内容を読んでいくとスムーズに理解が進むかもしれません。
なお、TextSpan
は厳密にはWidgetではなく、スタイルとテキスト情報を格納するためのクラスです。
RenderParagraph
RenderParagraph
は、パラグラフ (Paragraph) を描画/管理するためのRenderBox
の派生クラスです。RenderBox
は、RenderObject
の派生クラスで、2Dグラフィックスの描画のための座標情報を管理して描画するためのクラスです。
テキストは、文字の長さやスタイル、縦書き横書き、テキストエリアのサイズなど様々な要素が存在し、それらを考慮した描画エリアと描画処理を行う必要があります。
RenderParagraphのソースコードを見てみます。
/// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox
with ContainerRenderObjectMixin<RenderBox, TextParentData>,
RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
RelayoutWhenSystemFontsChangeMixin {
ソースコードを追っていくと、TextPainter
のインスタンスを生成し、そのほかの各種操作でもテキストの情報を集約していることが分かります。
_textPainter = TextPainter(
text: text,
textAlign: textAlign,
textDirection: textDirection,
textScaleFactor: textScaleFactor,
maxLines: maxLines,
ellipsis: overflow == TextOverflow.ellipsis ? _kEllipsis : null,
locale: locale,
strutStyle: strutStyle,
textWidthBasis: textWidthBasis,
textHeightBehavior: textHeightBehavior
)
さらに、Layout (レイアウト計算) とPaint (描画処理) 自体も、最終的にTextPainter
のそれらをコールしていることが分かります。
TextPainter
TextPainter
は、TextSpan
のツリー (複数のスタイルを持つテキスト郡の集合体) を描画するためのクラスです。では、TextPainter
のソースコードを見てみます。
Layout処理は、最終的に_paragraph.layout()
で行われ、テキストのスタイリングは_text.build()
で行われていることが分かります。それぞれ、Paragraph
クラスとInlineSpan
クラスです。
void layout({ double minWidth = 0.0, double maxWidth = double.infinity }) {
assert(text != null, 'TextPainter.text must be set to a non-null value before using the TextPainter.');
assert(textDirection != null, 'TextPainter.textDirection must be set to a non-null value before using the TextPainter.');
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;
// A change in layout invalidates the cached caret metrics as well.
_previousCaretPosition = null;
_previousCaretPrototype = null;
_paragraph.layout(ui.ParagraphConstraints(width: maxWidth));
if (minWidth != maxWidth) {
final double newWidth = maxIntrinsicWidth.clamp(minWidth, maxWidth) as double;
if (newWidth != _applyFloatingPointHack(_paragraph.width)) {
_paragraph.layout(ui.ParagraphConstraints(width: newWidth));
}
}
_inlinePlaceholderBoxes = _paragraph.getBoxesForPlaceholders();
}
Paint処理は、普通にCanvas
クラスですね。
void paint(Canvas canvas, Offset offset) {
assert(() {
if (_needsLayout) {
throw FlutterError(
'TextPainter.paint called when text geometry was not yet calculated.\n'
'Please call layout() before paint() to position the text before painting it.'
);
}
return true;
}());
canvas.drawParagraph(_paragraph, offset);
}
Paragraph
Paragraph
は、dart:uiライブラリの一クラスで、パラグラフ (段落付きテキスト) を描画するためのクラスです。ソースコード的にはFlutter Engine側の持ち物です。
Paragraph
は、後述のParagraphBuilder
クラスを利用して生成します。
ParagraphBuilder
ParagraphBuilder
は、指定されたスタイル情報とテキストでParagraph
のインスタンスを生成するためのクラスです。
Canvas
Canvas
は、dart:uiライブラリの一クラスで、2Dグラフィックスの描画のための操作I/Fを提供するクラスです。Paragraph
同様、ソースコード的にはFlutter Engine側の持ち物です。
4. Flutter Engine
ここからは、Flutter Engine側のテキストレンダリングに関わる部分の仕組みについて解説していきます。
Flutter Engine側の役割は主に以下です。
- テキストの最終的なレイアウト (折り返し結果のサイズ、位置など) を決定
- Skiaを利用して指定されたスタイリングでテキストをサーフェスに描画
4-1. C++側のParagraph
dart:uiのParagraph
の内、多くのメソッドはC++側のネイティブ側のソースコードにバインディングされています。そしてそのParagraph
のC++側のソースコードは以下に存在します。
4-2. Paragraphから先のモジュール
以下のソースコード (特にparagraph_txt.cc) から後述のOSSに繋がっていきますが、今時点でまだ詳細が終えていません。。追って更新します。
https://github.com/flutter/engine/blob/master/third_party/txt/src/txt/paragraph_txt.cc
https://github.com/flutter/engine/blob/master/third_party/txt/src/txt/paragraph_builder_txt.cc
4-3. テキスト関連ライブラリ
主要なソースコードは以下にありますが、基本的には文字関連でメジャーなOSSを利用しています。
https://github.com/flutter/engine/tree/master/third_party/txt
ICU (International Components for Unicode)
ICUは、Unicode文字列に関する各種機能を提供してくれるライブラリです。詳細は、ICUの使用用途を参照して下さい。
文字サイズや配置位置などに応じて、テキストを適切に折り返す必要があります。そのためには、文字列内で改行しても問題ない適切な場所を見つける必要があります。Flutterでは、このユースケースのために利用されています。また、Flutterのビルド成果物 (ただしデバッグモード時のみファイルとしては単体で存在) の中にicudtl.dat
というファイルが存在しますが、これはICUのライブラリで必要となるデータです (ICUライブラリ付属のファイルというイメージ) 。
参考までに、DateTimeの国際化など、ICUはAndroidでも利用されています。
Minikin
Minikin
は、テキストのサイズやレイアウトを決めるためのAndroid用に開発され、利用されているOSSです。Flutter用にカスタマイズされている様子です。ソースコード↓
https://github.com/flutter/engine/tree/master/third_party/txt/src/minikin
HarfBuzz
HarfBuzz
、選択されたフォントから目的の文字にあった字形を選択する業界標準のOSSです。
https://github.com/harfbuzz/harfbuzz
4-4. Skia
Skiaは、Googleが開発しているOSSで、C++で書かれたベクター方式の2Dグラフィックスライブラリです。Androidでも利用されている他、Chromeブラウザの描画でも利用されています。