Help us understand the problem. What is going on with this article?

Flutter テキストレンダリングの仕組み

1. はじめに

本記事は、主にFlutterのテキストWidget (Text, RichText) における、テキストのレンダリングの仕組みについての調査結果をまとめています (他の記事も同様ですが、基本的に随時更新しています) 。

なお、テキスト系Widgetの基本的な使い方はこちらにまとめていますので、これらのWidgetに対して知識がない方はまずはこちらに目を通して下さい。

2. Flutterレンダリングの基本

2-1. レンダリングフローについて復習

以下にFlutterの基本的なレンダリングフローを示します。FlutterはWebブラウザと似た感じで、画面情報をツリー構造で管理し、最終的にFlutter Engine (C++の描画エンジン) 経由でグラフィックスライブラリのSkiaで描画を行います。
image.png

フローとしては、まず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

TextRichTextについて、それらのWidget, Element, Render Objectの関係性を示します。この図の通り、RenderParagraphクラスで実際の描画処理を行います。
Frameworkレイヤの構造

Text

Textは、StatelessWidgetの派生クラスです。ソースコードを見ると分かりますが、内部的にはRichTextを利用しており、Widgetのbuild()時にRicthTextのインスタンスが生成されます。

RichText

RichTextは、MultiChildRenderObjectWidgetの派生クラスです。RenderObjectと名前が付いているので、直接描画に関連するWidgetであろうということがすぐに推測されます。RichTextのソースコードを見てみると、RenderParagraphというクラスをインスタンスしています。これがテキスト描画の中心になる重要なクラスです。

RichTextクラスのソースコードを一部抜粋
  @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から繋がる先のモジュール構成を以下に示します。この構成を頭に入れて、後述の内容を読んでいくとスムーズに理解が進むかもしれません。
image.png

なお、TextSpanは厳密にはWidgetではなく、スタイルとテキスト情報を格納するためのクラスです。

RenderParagraph

RenderParagraphは、パラグラフ (Paragraph) を描画/管理するためのRenderBoxの派生クラスです。RenderBoxは、RenderObjectの派生クラスで、2Dグラフィックスの描画のための座標情報を管理して描画するためのクラスです。

テキストは、文字の長さやスタイル、縦書き横書き、テキストエリアのサイズなど様々な要素が存在し、それらを考慮した描画エリアと描画処理を行う必要があります。

RenderParagraphのソースコードを見てみます。

RenderParagraphクラスの定義
/// A render object that displays a paragraph of text.
class RenderParagraph extends RenderBox
    with ContainerRenderObjectMixin<RenderBox, TextParentData>,
             RenderBoxContainerDefaultsMixin<RenderBox, TextParentData>,
                  RelayoutWhenSystemFontsChangeMixin {

ソースコードを追っていくと、TextPainterのインスタンスを生成し、そのほかの各種操作でもテキストの情報を集約していることが分かります。

RenderParagraph内の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クラスです。

Dart
  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クラスですね。

TextPainter.paint()
 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ブラウザの描画でも利用されています。

5. 参考文献

kurun_pan
QiitaではFlutterに関する記事を投稿しています。その他の技術内容やQiita投稿記事の内容以外についての、ご意見・連絡等はTwitterの方へお願いします! 
https://kurun.booth.pm/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away