[読書会]RenderObject クラスについて

本サイトはapi.flutter.dev/RenderObject classのについてのリファレンスブログです。

目次 index【閲覧時間3分】 click !!>>

1.RenderObject の基本機能

RenderObject はレンダリングライブラリの中心となるクラスです。

(1)RenderObject の親子関係と役割(レイアウト、描画)

Flutterの低レベルのレンダリング層のコンポーネントで、ウィジェットの描画やレイアウトの背後で機能しています。
各RenderObjectには親が存在します。これは、Flutterのウィジェットツリーやレンダーツリーにおける階層構造を表しています。
各RenderObjectはツリー構造に従い、親子関係を持っています。親のRenderObjectがその子をどのようにレイアウトし、描画するかを制御します。

(2)RenderObject が保有する情報(parentData(スロット(変数)))

RenderObjectにはparentDataというスロット(変数)があり、このスロットには親のRenderObject子に関する特定の情報保持します。parentDataは各RenderObjectの親管理し、特定のレイアウトや位置情報を保存するために使われます。
親のRenderObjectは子に関連する特定のデータを保持するために、このparentDataを使用します。これにより、親が子の位置やその他の情報を適切に管理できるようになります。たとえば、子の位置(child position)やサイズ配置順序などの情報が含まれます。

(3)RenderObject の提供する主なメソッド(layout()、paint())、クラス(Canvas())

親RenderObjectが子RenderObjectに対してサイズ計算し、どこ配置するかを決めるためのメソッド(例: layout()メソッド)を提供しています。このレイアウトプロトコルに基づき、各RenderObjectは自分のサイズや子要素のサイズを計算し、親からの指示に従ってレイアウトを決定します。
ペイントプロトコル」とは、実際にUIを描画するための一連の手順です。RenderObjectは、Flutterの画面上に描画される要素をどのように描画するか(つまりピクセル単位でどのように描くか)を処理する役割を果たします。具体的には、paint()メソッドがあり、ここで具体的な描画処理を行います。

例えば、RenderObjectは自身や子要素の境界、背景、テキスト、画像などをどのように描画するかを定義できます。Canvasクラスを使用して、これらの要素を具体的に描画することになります。

2.RenderObject の柔軟性と汎用性

RenderObjectはFlutterのレンダリングシステムの基礎を提供するクラスですが、
特定の子供モデルや座標系、レイアウトプロトコルを定義していません。
故に、開発者は特定のレイアウトロジックを実装する際に、RenderObjectのサブクラスを拡張して、独自の動作を定義できます。

(1)子RenderObject のモデルは定義されていない

RenderObjectは以下のような情報を決めていません:

  • ノードが子供を持たないのか (zero children)
  • 一つの子供を持つのか (one child)
  • 複数の子供を持つのか (more children)

これにより、RenderObjectは非常に汎用的で、具体的な実装や要件に合わせて、子供モデルを定義することができます。
例えば、RenderBoxなどのサブクラスがその具体的な子供モデル(例えば、単一の子や複数の子を持つかどうか)を定義します。

(2)RenderObject の座標系は定義されていない

RenderObjectは座標系(coordinate system)も定義しません。
つまり、子要素がどの座標系で配置されるかについても特定のルールは持たず、以下のような点を決めていません:

  • 子がデカルト座標系(Cartesian coordinates、X軸とY軸で位置を指定する一般的な座標系)で配置されるのか
  • 子が極座標系(Polar coordinates、角度と距離で位置を指定する座標系)で配置されるのか

これは、RenderObjectが具体的なレイアウトロジックに縛られていないため、柔軟に様々なレイアウト方法や座標系に対応できることを意味します。

(3)RenderObject のレイアウトプロトコルを定義していない

RenderObjectは特定のレイアウトプロトコルを定義していません。
つまり、どのようにレイアウトが計算され、子のサイズや位置が決定されるかについて、
以下のような具体的な方法はRenderObjectには含まれていません:

  • width-in-height-out: 幅を入力として高さを出力するレイアウト手法。
  • constraint-in-size-out: 制約を入力としてサイズを出力する手法。
  • 親が子のサイズや位置を設定するのは、子のレイアウトが完了する前か後か(親が子をレイアウトする順序に関するルール)。
  • 子が親のparentDataスロットを読み取れるかどうか(親子間でどのようなデータがやり取りされるか)。

このように、RenderObjectはどのようなレイアウトのルールに従うかを決めないため、サブクラスや派生クラスで独自のレイアウトロジックを実装することができます。

(4)まとめ

この説明では、RenderObjectクラスの柔軟性と汎用性について強調されています。
具体的には、RenderObjectは以下の点について固定的なルールを持たず、サブクラスや派生クラスがその振る舞いを決める余地が残されています。

  • 子供の数や構造(子供モデル)
  • 子供の配置に使う座標系(デカルト座標系や極座標系など)
  • レイアウトプロトコル(親子間のレイアウトルールやプロセス)

このため、RenderObjectはさまざまなレイアウトや描画要件に適応可能な、非常に拡張性のある基盤を提供します。
開発者は特定のレイアウトロジックを実装する際に、RenderObjectのサブクラスを拡張して、独自の動作を定義できます。

3.RenderBox を基にしたウィジェットはデカルト座標系によるレイアウト

RenderBoxクラスは、RenderObjectクラスを継承して、座標系に関する特定の前提を導入しています。
それは、「レイアウトシステムがデカルト座標系(Cartesian coordinates)を使用する」という考え方です。

デカルト座標系は、X軸とY軸に基づいて位置を指定する一般的な座標系です。
つまり、RenderBoxを使用する場合、子要素の配置はX軸とY軸の2次元の平面上で行われます。
この点が、RenderObjectクラスが座標系を決定していなかったことに対して、RenderBoxでは明確に定義される部分です。

例えば、RenderBoxを基にするウィジェット(例えば、ContainerやSizedBoxなど)は、指定された幅と高さ(X軸、Y軸)に基づいてレイアウトされます。

4.RenderObject のライフサイクル

(1)RenderObject 生成と破棄

RenderObjectにはライフサイクルがあり、使用されなくなったら適切に破棄(dispose)される必要があります。
これはメモリリークや不要なリソースの浪費を防ぐためです。

RenderObjectの生成者(通常はRenderObjectElement)が、そのRenderObjectを破棄する責任を持っています。
RenderObjectElementは、RenderObjectを生成した後、その要素がマウント解除(unmount)されるときにRenderObjectを破棄します。つまり、ウィジェットツリーが変更されたり、ウィジェットが削除されるときに、そのウィジェットに関連するRenderObjectも適切に破棄されることになります。

(2)RenderObject 破棄の際の保有リソースのクリーンアップ

RenderObjectは、disposeが呼ばれたときに、保持しているリソース(特に高コストなリソース)を適切にクリーンアップする責任があります。

①クリーンアップの対象となる保有リソース例

具体的には、以下のようなリソースが含まれます:

  • Picture: 描画結果をキャッシュするオブジェクト
  • Image: イメージ(画像)データ
  • Layers: レンダリング時に利用されるレイヤー

レンダリングに使用されるレイヤーや画像はメモリを多く消費する可能性があるため、これらが不要になったときに確実に解放することが重要です。

②クリーンアップ処理の実装例

RenderObjectの基底実装(disposeメソッドの基本実装)では、layerプロパティをnullします。
これにより、レンダリングに使用されたレイヤーが不要になったことを示し、メモリが解放されます。

(3)RenderObject を継承したサブクラス( RenderBox )の責任

RenderObjectを継承したサブクラス(例えば、RenderBox)は、自分で作成したその他のレイヤーについても同様にクリーンアップ(nullにする)する必要があります。これにより、すべてのリソースが適切に解放され、パフォーマンスやメモリ使用量に悪影響を与えないようにします。

5.RenderObject のサブクラス作成時の留意点

RenderObject サブクラス作成しなければならない場合、次の判断を行う必要があります。

  • RenderBox継承して済ますべきか
  • RenderObject直接サブクラス化する(自分で書くべきか

以下は、この点についての留意点です。

(1)RenderBoxを継承して済ます方が良い場合

多くの場合、RenderObjectを直接サブクラス化するのは過剰(overkill)で、通常はRenderBoxを基にする方が適切です。
RenderBoxはデカルト座標系を前提としており、幅や高さ(Size)を基にしたレイアウトプロトコルが既に実装されています。
したがって、一般的な2Dレイアウトには十分な機能が備わっています。

(2)RenderObjectを直接サブクラス化する方が良い場合

しかし、デカルト座標系を使用したくない場合や、独自の座標系やレイアウトプロトコルを定義したい場合には、RenderObjectを直接サブクラス化する必要があります。例えば、極座標系や他の特別なレイアウトを実装する場合です。

この場合、次のようなカスタマイズが可能です:

  • Custom Constraints
     RenderBoxが使用するBoxConstraints高さに基づく制約)ではなく、
    新しい制約クラスConstraintsのサブクラス)を定義することができます。
    これにより、独自のレイアウトロジックを実現できます。
  • Custom Output
     出力としてSize高さ)だけでなく、まったく新しいオブジェクトや値を使用できます。
    これにより、サイズ以外の情報をレイアウト結果として扱うことが可能です。

(3)両者はトレードオフの関係にある

RenderObjectを直接サブクラス化することで、柔軟なレイアウトプロトコルを作成できますが、
RenderBoxが提供する便利な機能(例えば、Sizeに基づくレイアウト管理やヒットテスト処理など)を失うことになります。
このため、全体的により多くのコードを書かなければならず、複雑さが増します。

以下は上記トレードオフに相当する検討事項になります。

(3-1)固有サイズプロトコル

RenderBoxは「固有サイズプロトコル(intrinsic sizing protocol)」を実装しています。これは、子要素を完全にレイアウトしなくても、そのサイズを測定する機能です。このプロトコルにより、子ウィジェットのサイズ変更に応じて親ウィジェットも再レイアウトされるようにします。たとえば、親が子ウィジェットの新しいサイズに基づいて自分自身のサイズを変更する場合に、非常に便利です。

このプロトコルは便利ですが、正しく実装するのが難しく、バグを招きやすいという欠点もあります。サイズの変更や依存関係が複雑な場合、期待した通りにレイアウトが更新されないことがあるためです。

(3-2)RenderBoxとRenderObjectのレイアウトとヒットテスト関連部分

RenderBoxを作成する際に必要な多くの要素は、RenderObjectのサブクラスを書く場合にも適用されます。
これには、ペイント処理やリソース管理などが含まれます。
ただし、主に異なる点は、以下のレイアウトヒットテストに関連する部分です。

①レイアウト

RenderBoxは、高さのレイアウトに基づいており、親子間でのレイアウトの調整容易に行えます。
しかし、RenderObjectではこれらのレイアウト処理独自に定義しなければならず座標系制約サイズ計算の方法カスタマイズする必要があります。

②ヒットテスト

ヒットテストは、ユーザーの入力タッチクリックなど)がどのレンダーオブジェクト当たったか判断するための処理です。
RenderBoxはこれをデフォルトサポートしており、ユーザーの入力がボックス内に収まっているか自動判定します。
しかし、RenderObjectサブクラス化している場合、独自ヒットテストロジック定義する必要があります。
特に、非デカルト座標系特殊な形状を持つレンダーオブジェクトの場合、このロジックを慎重に設計しなければなりません。

(4)上記までのまとめ

  • RenderBoxの便利さ
     通常は、RenderBoxを使用することで多くの便利な機能を活用でき、デカルト座標系でのレイアウト処理や固有サイズプロトコルなどが既に提供されています。
  • RenderObjectの柔軟性
     しかし、デカルト座標系以外の座標系を使いたい場合や、カスタムレイアウトプロトコルを定義したい場合には、RenderObjectを直接サブクラス化することで、制約やレイアウトロジック、座標系を自由にカスタマイズできます。
  • Trade-off
     この柔軟性には、RenderBoxが提供する一部の自動機能(特に固有サイズプロトコルやヒットテスト処理)が失われるというコストが伴います。

6.RenderObject の Layout プロトコル

RenderObjectRenderBoxにおけるLayout プロトコルlayout protocol)の仕組み実装における
 Constraints クラスを使用したLayout 処理
 performLayoutメソッドによるLayout 実行
 Layout 変更時対応
についての話題になります。

6-1.Constraints クラス

Layoutプロトコルは、Constraintsサブクラスから始まります。
Constraintsは、Layout 制約(例えば、サイズ位置制限を表すクラスであり、
RenderObject子要素どのように制約適用するかを定義します。
通常は、BoxConstraintsが使われ、これにより高さ最小値最大値指定されます。

しかし、特定の制約定義したい場合は、Constraintsクラス継承して新しい制約クラス作成することができます。
これは、デカルト座標系以外座標系特殊なLayout ロジック実現する際に役立ちます。

6-2.performLayout メソッド

performLayoutメソッドは、RenderObjectやそのサブクラス(例えばRenderBox)で実装されるメソッドです。
このメソッドは、渡された制約(Constraints)に従って実際にレイアウトを計算し、子要素を配置します。

例えば、BoxConstraintsの場合、
親から与えられた最小サイズと最大サイズを使用して、子ウィジェットどれだけの領域を使えるか決定します。

上記をまとめると、performLayoutメソッドは、次の2つを行います。
① 子要素に制約を適用し、そのサイズを計算する。
② 自分自身サイズ配置計算する。

6-3.Layoutアルゴリズムの結果

レイアウトアルゴリズムの結果は、オブジェクトにセットされるフィールドとして表現されます。
具体的には、RenderBoxの場合、RenderBox.sizeフィールドに計算されたサイズ設定されます。
このサイズは、レイアウト決定するために使用します。

RenderObjectサブクラスでは、サイズ以外の出力(例えば、位置や回転などの情報)も使用されるかもしれません

6-4.parentUsesSize

親が子要素のレイアウト情報(例えば、サイズ)を読み取るのは、layoutメソッドを呼び出す際にparentUsesSizetrueに設定されている場合のみです。このフラグが設定されていると、親が子のサイズ情報を使用して、自身のレイアウトを調整することを意味します。

6-5.markNeedsLayout

RenderObjectのレイアウトに影響を与える変更があった場合、そのRenderObjectmarkNeedsLayoutメソッドを呼び出して、レイアウトが再計算される必要があることを通知します。このメソッドは、変更があった際に自動的にレイアウトを更新する仕組みの一部です。

例えば、子要素のサイズが変更された場合や、ウィジェットの状態が変わって再配置が必要な場合、markNeedsLayoutを呼び出すことで次のフレームでレイアウトが再計算されます。このようにして、RenderObjectのレイアウトは動的に更新され、UIが正しく描画され続けることが保証されます。

6-6.上記のまとめ

Layout protocol starts with Constraints:
  レイアウトプロトコルConstraintsクラスに基づいており、
 Constraintsサブクラスを作成することで独自の制約定義できます。

performLayout:
  performLayoutメソッドは、制約に基づいてレイアウトを実行し、
 その結果(レイアウト情報)をRenderObjectフィールド設定します。
  RenderBoxの場合、sizeフィールドサイズ設定されます。

parentUsesSize:
  親がサイズ使用するかどうかは、
 layoutメソッド呼び出し時に指定されるparentUsesSizeフラグによって制御されます。

markNeedsLayout:
  レイアウトに影響を与える変更があった場合にはmarkNeedsLayout呼び出し
 フレームでレイアウト再計算するように指示します。

これらの仕組みが、Flutterのレンダリングエンジンにおける柔軟で効率的なレイアウト処理の基礎となっています。

7.Hit Testing

FlutterのRenderObjectヒットテストhit testing)についての話です。
ヒットテストは、ユーザーが画面上のどの部分をタップやクリックしたか検出するためのメカニズムです。
特に、RenderBox以外のカスタムレンダリングオブジェクトでヒットテストをどのように実装するかについて述べられています。

7-1.ヒットテストは自由度が高い

レイアウト処理に比べて、ヒットテストはさらに自由度が高いということを意味しています。
レイアウトには特定のプロトコルやメソッド(例えばperformLayout)が用意されていますが、ヒットテストにはそのような規定されたメソッドがありません。つまり、ヒットテストの具体的な実装は開発者に任されており、特定のメソッドをオーバーライドするのではなく、自分でメソッドを提供する必要があります。

7-2.一般的なヒットテストの挙動

ヒットテストの一般的な挙動は、RenderBoxで定義されているものと似たようなものにするのが推奨されています。
RenderBoxのヒットテストの仕組みは、基本的にはユーザーのタップやクリックがそのボックス内に入っているかどうかを判断するものです。

RenderBoxでは、ヒットテスト時にタップ位置がOffsetで指定され、その座標に基づいてボックス内に収まっているかが判断されます。

7-3.カスタムの座標系やエントリが可能

ただし、カスタムのRenderObjectを作成する場合、ヒットテストに使用する入力は必ずしもOffset(デカルト座標のX軸・Y軸のオフセット)である必要はありません。新しいレイアウトプロトコルを定義した場合、そのプロトコルに基づいた異なる座標系を使用することができます。

例えば、極座標系やカスタムの座標系を使用している場合、その座標系に適した入力形式を使うことができます。

7-4.handleEventとヒット情報の連携

(1)HitTestEntry と HitTestResult

ヒットテスト時には、タップやクリックが特定の場所に当たった場合、その情報をHitTestResultクラスに追加します。
通常はHitTestEntryクラスを使って情報を追加しますが、カスタムヒットテスト実装する場合、HitTestEntryを継承した独自のサブクラスを使用することができます。

例えば、HitTestEntryに加えて独自の追加情報(ヒットした位置やレイアウト情報など)を持たせたい場合には、HitTestEntry拡張したサブクラス作成し、それを使うことができます。

(2)handleEvent メソッド

ユーザーがクリックやタップをした時handleEventメソッドが呼び出されます。
このメソッドでは、HitTestResultに追加されたのと同じオブジェクト(HitTestEntryまたはそのサブクラス)が引数として渡されます。これにより、ヒットテスト時に追跡した情報(タップ位置など)を、イベントの処理時に参照することが可能です。

例えば、カスタムのヒットテストエントリを使用してヒットした正確な座標やレイアウト情報を追跡していた場合、その情報をhandleEventで利用して、どの位置でイベントが発生したのかを正確に把握できます。

8.異なるLayoutプロトコル(RenderObject – RenderBox)間の適応

8-1.RenderViewとRenderBoxの関係

Flutterのレンダーツリーのルートは、通常RenderViewです。
RenderViewは、画面全体の描画を管理する最上位のレンダーオブジェクトです。
RenderViewは通常、1つの子要素を持ち、その子要素はRenderBoxである必要があります。

Flutterのレンダリングシステムにおいて、ほとんどのレンダーツリーはRenderBoxを基準にしています。
RenderBoxデカルト座標系を前提にしたLayoutプロトコルを持ち、通常のLayoutやhitTestはこれに従って実行されます。

8-2.カスタムRenderObjectの選択肢

カスタムRenderObjectサブクラスをレンダーツリーに含めるには、以下の2つの方法があります。

  1. RenderView置き換える
     RenderView自体を置き換えて、独自のRenderObjectルートにする方法です。
    ただし、これはあまり一般的ではありません。
  2. RenderBox子要素として追加する:
     カスタムRenderObjectRenderBoxの子として含める方法です。
    この方法が一般的で、RenderBoxを介して異なるプロトコルを調整する役割を果たします。

8-3.RenderBoxでのプロトコル変換

(1)RenderBox サブクラスによるRenderObject プロトコルの変換

RenderBoxのサブクラス作成して、RenderBoxのプロトコル(デカルト座標系やBoxConstraints)と、カスタムRenderObjectのプロトコル(異なる座標系や制約)を変換します。つまり、RenderBoxが親としてカスタムのRenderObject親和性を持たせるための橋渡しを行います。

(2)hitTest メソッド オーバーライドによる Hit Tesing プロトコル の変換

RenderBoxサブクラスは、RenderBoxhitTestメソッドオーバーライドします。
ヒットテストは、ユーザーのタッチやクリックがどのオブジェクトにヒットしたかを判断するメカニズムです。
RenderBoxhitTestは通常、デカルト座標系を使いますが、カスタムRenderObjectが異なる座標系を使用している場合、そのプロトコルに従ってヒットテストを行うため、hitTestメソッドを上書きして、カスタムクラスのヒットテストメソッドを呼び出します。

具体的には、RenderBoxの座標系やhitTestのロジックを使い、適切にカスタムRenderObjecthitTestingプロトコルを適用する必要があります。

(3)performLayout メソッドオーバーライドによる 制約の変換

RenderBoxのサブクラスでは、performLayoutメソッドもオーバーライドします。
このメソッドは、RenderObjectのレイアウトを決定するために使われる重要なメソッドで、ここでConstraintsを適用します。

RenderBoxが持つBoxConstraints(幅や高さの制約)をカスタムの制約クラス(例えば、極座標系や特別なレイアウト制約)に変換し、その制約をカスタムRenderObjectのlayoutメソッドに渡します。
これにより、RenderBoxは標準的な制約を適用しつつ、カスタムRenderObjectが独自のレイアウトプロトコルに基づいて動作できるようにします。

9.RenderObjects 間の Layout 相互作用

9-1.親子のレイアウト依存関係

一般的に、RenderObjectのLayoutは、その子のLayoutの結果に依存します。
つまり、親は子のLayout結果を元にして自身のLayoutを決定します。

但し、上記は、親が parentUsesSizetrueに設定している場合です。
(親は子のLayout結果を利用して自分自身のLayoutを計算します。)
もし true でない場合、親は子のLayout情報を無視し、独立してLayoutを行います。

さらに、parentUsesSizetrueであれば、親は子のLayoutを必ず呼び出さなければなりません
そうでないと、子がLayout結果を変更した際に親がその変更に気付かず、親のLayoutが更新されなくなる可能性があるためです。

9-2.追加のレイアウト情報のやり取り

通常のLayout情報(サイズや位置)以外に、親と子のRenderObject間で追加の情報をやり取りするプロトコルを設定することが可能です。例えば、RenderBoxプロトコルでは、子の固有の寸法(intrinsic dimensions)やベースライン(baseline)に関する情報を問い合わせることができます。これにより、親はより詳細なLayout情報を基にして、自分自身のLayoutを調整できます。

  • Intrinsic dimensions: ウィジェットの幅や高さの最小・最大の情報。
  • Baseline geometry: テキストや他のコンテンツが配置される基準となる位置情報。

9-3.レイアウト変更時の通知

もし親が、子の固有の寸法やベースライン情報を利用してLayoutを決定している場合、子がその情報を変更した際に、親がその変更を知る必要があります。このため、markNeedsLayoutを親に対して呼び出し、親に再Layoutが必要であることを通知する必要があります。

具体的には、子がmarkNeedsLayoutを呼び出すことで、その親が再度Layoutを計算し、最新の情報を基に再配置が行われるようになります。これにより、親子間のLayoutの整合性が保たれます。

9-4.RenderBoxの例

RenderBoxmarkNeedsLayoutメソッドは、RenderObjectmarkNeedsLayoutオーバーライドしており、親が子の固有寸法やベースライン情報を問い合わせた場合に、その情報が変更されると親がマークされるように設計されています。このメソッドにより、子のLayout情報が変更された際に親も適切に通知され、再Layoutが発生する仕組みが提供されています。

つまり、RenderBoxでは親が子の固有情報を問い合わせた場合、その後に子の寸法が変わった場合には、親のLayoutが自動的に「dirty状態Layoutの更新が必要な状態)としてマークされ、次のフレームで再Layoutが行われます。

コメントを残す