Flutter RenderObjectWidgetを利用したグラフィックス描画

# はじめに

Flutter のスーパークラスには以下の4種類があります。その他のウィジェット (クラス) はこれらを継承した派生クラスです。

|クラス名|用途|
|---|---|
|`StatelessWidget`|状態 (Stateクラス) を持たない静的なウィジェットのコンテナ (固定ページ用)|
|`StatefulWidget`|状態 (Stateクラス) を持ち、動的変化があるウィジェットのコンテナ (動的変化があるページ用)|
|`InheritedWidget`|ウィジェットツリーの上位の情報を子ノードについた得るためのウィジェット|
|`RenderObjectWidget`|グラフィックスをレンダリングするオブジェクトのRenderObjectのコンテナとなるウィジェット|

今回は一番下の`RenderObjectWidget`を利用したグラフィックス描画について解説します。あまり見慣れないものですが、Flutterのレンダリング過程で重要になってくるクラスなのと、Flutterでゲームアプリを作る場合には利用すると思います。

# RenderObjectWidgetとは

[`RenderObjectWidget`](https://api.flutter.dev/flutter/widgets/RenderObjectWidget-class.html)とは、グラフィックスの描画 (レンダリング) オブジェクトである[`RenderObject`](https://api.flutter.dev/flutter/rendering/RenderObject-class.html)のコンテナになるウィジェットのことです。このウィジェットの中に描画対象のウィジェットを格納して利用します。

これらはFlutterのレンダリングフローの中でも重要な働きをしているため、別途どこかか (別の記事) で解説したいと思います。

# グラフィックス描画してみる

`RenderObjectWidget`の使い方を知るために、簡単なペイント程度のサンプルを解説します。
Androidで言うところの`Canvas`と同じようなイメージです。
<img width="320" alt="flutter_01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259963/f65b26e1-3e46-734b-b1ad-589ba60cca4c.png">

## メインページの作成

適当にメインページを用意します。`_RenderBoxWidget`がグラフィックスのサンプル用の独自クラスです。

```Dart:main_page.dart
class MainPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Flutter RenderObjectWidget Example'),
      ),
      body: Center(
        child: _RenderBoxWidget(),
      ),
    );
  }
}
```

## 描画用コンテナクラスの作成

`_RenderBoxWidget`の実装は以下です。

```Dart:_RenderBoxWidgetクラス
class _RenderBoxWidget extends SingleChildRenderObjectWidget {
  @override
  RenderObject createRenderObject(BuildContext context) {
    return _MyRenderBox();
  }
}
```

[`SingleChildRenderObjectWidget`](https://api.flutter.dev/flutter/widgets/SingleChildRenderObjectWidget-class.html)クラスを継承し、`createRenderObject`メソッドの中で`RenderObject`を生成します。

`SingleChildRenderObjectWidget`は`RenderObjectWidget`の派生クラスで、一つだけの子ウィジェット (child) を持つことができます。

`SingleChildRenderObjectWidget`の他には、複数の子ウィジェット (children) を持てる[`MultiChildRenderObjectWidget`](https://api.flutter.dev/flutter/widgets/MultiChildRenderObjectWidget-class.html)や、アプリ開発者が直接は扱う事がない基底クラスの[`LeafRenderObjectWidget`](https://api.flutter.dev/flutter/widgets/LeafRenderObjectWidget-class.html)が存在します。


## 描画クラスウィジェットの作成

描画処理は[`RenderBox`](https://api.flutter.dev/flutter/rendering/RenderBox-class.html)ウィジェットが担当しています。

なので`RenderBox`継承したクラスを作成して`paint()`の中で実際の描画処理を行います。
ここまで来れば実際の描画処理はほとんどAndroidの`Canvas`と同じ操作で簡単です。

`paint()`の引数の`PaintingContext`とは、描画処理するため制御情報を保有しているコンテキストのことで、このコンテキストのキャンパスに描画したいデータを書き込みます。それと注意点として、`paint()`の引数のOffsetには画面内の描画開始 (ウィジェットが配置された位置) の情報が来るため、そこからのオフセットという座標軸で描画を行うことになります。

あとは、Paintで描画対象の色や塗り潰しなどを定義し、四角形を描画する場合は`Rect`を利用してサイズを指定します。
最後に`Canvas`で描画すればOKという感じです。この辺は解説しなくてもソースコードを見たら雰囲気でわかるかと思います。

```Dart:
class _MyRenderBox extends RenderBox {
  /*
    以下のエラー対応
    flutter: The following assertion was thrown during performLayout():
    flutter: _MyRenderBox did not implement performLayout().
    flutter: RenderBox subclasses need to either override performLayout() to set a size and lay out any children,
    flutter: or, set sizedByParent to true so that performResize() sizes the render object.
   */
  @override
  bool get sizedByParent => true;

  @override
  void performResize() {
    size = constraints.biggest;
  }

  @override
  void paint(PaintingContext context, Offset offset) {
    Canvas c = context.canvas;

    // offsetのdx, dyにはスクリーン内の描画開始エリア情報 (オフセット) が入っています
    double dx = offset.dx;
    double dy = offset.dy;

    Paint p = Paint();
    p.style = PaintingStyle.fill; // 図形内部を塗りつぶす
    p.color = Color.fromARGB(255, 255, 0, 0);
    Rect r = Rect.fromLTWH(dx + 10.0, dy + 10.0, 100.0, 100.0);
    c.drawRect(r, p);

    p.style = PaintingStyle.stroke; // 図形内部を塗り潰さない
    p.color = Color.fromARGB(255, 0, 255, 0);
    p.strokeWidth = 10.0; // 線の太さ
    r = Rect.fromLTWH(dx + 150.0, dy + 110.0, 100.0, 100.0);
    c.drawRect(r, p);

    // 丸を描画する場合も四角とほぼ同じ感じです
    p.style = PaintingStyle.fill;
    p.color = Color.fromARGB(255, 0, 0, 255);
    Offset ctr = Offset(dx + 350.0, dy + 260.0);
    c.drawCircle(ctr, 50.0, p);
  }
}
```

### アサートエラー対応

`sizedByParent()`と`performResize()`については、無くても動作自体はするのですが、実行時にアサートでエラーログが出るため、[Flutter 入門でつまづいたところまとめ](https://qiita.com/noobar/items/ac9e47107ecb5230a20c#6-1%E3%82%B0%E3%83%A9%E3%83%95%E3%82%A3%E3%83%83%E3%82%AF%E6%8F%8F%E7%94%BB%E3%81%AE%E5%9F%BA%E6%9C%AC)を参考に実装しました。