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

# 1. はじめに

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

なお、テキスト系Widgetの基本的な使い方は[こちら](https://qiita.com/kurun_pan/items/6fe376612e80b3f74901)にまとめていますので、これらのWidgetに対して知識がない方はまずはこちらに目を通して下さい。

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

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

以下にFlutterの基本的なレンダリングフローを示します。FlutterはWebブラウザと似た感じで、画面情報をツリー構造で管理し、最終的にFlutter Engine (C++の描画エンジン) 経由でグラフィックスライブラリの`Skia`で描画を行います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/ae92cf9d-5523-d8a8-1b08-dff1ed6afcc9.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側に必要な機能は提供しつつ、指定されたスタイルやフォントでの最終的なテキストの描画処理を実現します。
<img width="480" alt="テキストレンダリングのアーキテクチャ" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/4bdf1701-6550-7922-31d4-c566d4a6ec37.png">

では、Framewrok部分とEngine部分の詳細をそれぞれ見ていきましょう。

# 3. Flutter Framework

ここでは、Frameworkレイヤ内部について解説していきます。

## 3-1. Text/RichText

`Text`と`RichText`について、それらのWidget, Element, Render Objectの関係性を示します。この図の通り、`RenderParagraph`クラスで実際の描画処理を行います。
<img alt="Frameworkレイヤの構造" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/36534184-57d1-6d4d-31c8-46c372d67fed.png">

### [Text](https://api.flutter.dev/flutter/widgets/Text-class.html)

`Text`は、StatelessWidgetの派生クラスです。[ソースコード](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/text.dart)を見ると分かりますが、内部的には`RichText`を利用しており、Widgetの`build()`時に`RicthText`のインスタンスが生成されます。

### [RichText](https://api.flutter.dev/flutter/widgets/RichText-class.html)

`RichText`は、`MultiChildRenderObjectWidget`の派生クラスです。RenderObjectと名前が付いているので、直接描画に関連するWidgetであろうということがすぐに推測されます。RichTextの[ソースコード](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/widgets/basic.dart)を見てみると、`RenderParagraph`というクラスをインスタンスしています。これがテキスト描画の中心になる重要なクラスです。

```Dart: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/66a0ce5d-b58e-3f7e-f16c-f36745f5e79b.png)

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

### [RenderParagraph](https://api.flutter.dev/flutter/rendering/RenderParagraph-class.html)

`RenderParagraph`は、パラグラフ (Paragraph) を描画/管理するための`RenderBox`の派生クラスです。[`RenderBox`](https://api.flutter.dev/flutter/rendering/RenderBox-class.html)は、`RenderObject`の派生クラスで、2Dグラフィックスの描画のための座標情報を管理して描画するためのクラスです。

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

RenderParagraphの[ソースコード](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/rendering/paragraph.dart)を見てみます。

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

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

```Dart: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](https://api.flutter.dev/flutter/painting/TextPainter-class.html)

`TextPainter`は、`TextSpan`のツリー (複数のスタイルを持つテキスト郡の集合体) を描画するためのクラスです。では、`TextPainter`の[ソースコード](https://github.com/flutter/flutter/blob/master/packages/flutter/lib/src/painting/text_painter.dart)を見てみます。

Layout処理は、最終的に`_paragraph.layout()`で行われ、テキストのスタイリングは`_text.build()`で行われていることが分かります。それぞれ、[`Paragraph`](https://api.flutter.dev/flutter/dart-ui/Paragraph-class.html)クラスと[`InlineSpan`](https://api.flutter.dev/flutter/painting/InlineSpan-class.html)クラスです。

```Dart:Dart:TextPainter.layout()
  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`](https://api.flutter.dev/flutter/dart-ui/Canvas-class.html)クラスですね。

```Dart: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](https://api.flutter.dev/flutter/dart-ui/Paragraph-class.html)

`Paragraph`は、dart:uiライブラリの一クラスで、パラグラフ (段落付きテキスト) を描画するためのクラスです。[ソースコード](https://github.com/flutter/engine/blob/master/lib/ui/text.dart)的にはFlutter Engine側の持ち物です。

`Paragraph`は、後述の`ParagraphBuilder`クラスを利用して生成します。

### [ParagraphBuilder](https://api.flutter.dev/flutter/dart-ui/ParagraphBuilder-class.html)

`ParagraphBuilder`は、指定されたスタイル情報とテキストで`Paragraph`のインスタンスを生成するためのクラスです。

### [Canvas](https://api.flutter.dev/flutter/dart-ui/Canvas-class.html)

`Canvas`は、dart:uiライブラリの一クラスで、2Dグラフィックスの描画のための操作I/Fを提供するクラスです。`Paragraph`同様、[ソースコード](https://github.com/flutter/engine/blob/master/lib/ui/painting.dart)的にはFlutter Engine側の持ち物です。

# 4. Flutter Engine

ここからは、Flutter Engine側のテキストレンダリングに関わる部分の仕組みについて解説していきます。

Flutter Engine側の役割は主に以下です。

- テキストの最終的なレイアウト (折り返し結果のサイズ、位置など) を決定
- Skiaを利用して指定されたスタイリングでテキストをサーフェスに描画

## 4-1. C++側のParagraph
dart:uiの`Paragraph`の内、多くのメソッドはC++側のネイティブ側のソースコードにバインディングされています。そしてその`Paragraph`のC++側のソースコードは以下に存在します。

- [Paragraph](https://github.com/flutter/engine/blob/master/lib/ui/text/paragraph.cc)
- [ParagraphBuilder](https://github.com/flutter/engine/blob/master/lib/ui/text/paragraph_builder.cc)

## 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](http://site.icu-project.org/home)は、Unicode文字列に関する各種機能を提供してくれるライブラリです。詳細は、[ICUの使用用途](https://qiita.com/496_/items/0664b1963c2d8bd6db5a)を参照して下さい。

文字サイズや配置位置などに応じて、テキストを適切に折り返す必要があります。そのためには、文字列内で改行しても問題ない適切な場所を見つける必要があります。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](https://skia.org/)は、Googleが開発しているOSSで、C++で書かれたベクター方式の2Dグラフィックスライブラリです。Androidでも利用されている他、Chromeブラウザの描画でも利用されています。

# 5. 参考文献

- [My first disappointment with Flutter](https://medium.com/@suragch/my-first-disappointment-with-flutter-5f6967ba78bf)
- [Flutter Text Rendering](https://www.raywenderlich.com/4562681-flutter-text-rendering)