-
Notifications
You must be signed in to change notification settings - Fork 0
WidgetSystem
← Home
実装状況:
- 実装済み:
StatelessWidget/StatefulWidget/InheritedWidgetとそれぞれの Element が動作します。State.SetState()による再ビルド、InheritedWidgetの依存追跡・通知、MultiChildRenderObjectElementのKey対応の子リスト差分も実装済みです。ツリー補助のBuilder/KeyedSubtree/RepaintBoundaryと、ParentDataWidget<T>による親固有レイアウト情報の適用にも対応しています。SingleChildRenderObjectWidget<T>/MultiChildRenderObjectWidget<T>ベースのウィジェット(ColoredBox,Align,Flex,Clip*,SizedBox,ConstrainedBox,RichText,Textなど)も使用可能で、BuildOwnerによる差分ビルドが動作します(BuildPipeline 参照)。- 未実装:
ListView,GridView,SingleChildScrollViewはinternalで、公開 API から除外されています。Padding,Container,DecoratedBox,Opacity,Transformは公開 API として利用できます。入力系のGestureDetector/Listenerは公開スタブです。Button/Iconはデザインシステム層(FloatSoda.UI.Cream/FloatSoda.UI.FizzyPop)へ移動しました(→ UILayering)。- WIP:
FloatSoda.Hooks(R3 ベースのUseStateなど)はフレームワークのビルドループと未統合です。ジェスチャ・ヒットテストは未実装です。
Widget (immutable record)
│ CreateElement()
▼
Element (mutable) ← 状態・ライフサイクル管理、BuildOwner が差分ビルド
│ CreateRenderObject() / UpdateRenderObject()
▼
RenderObject ← レイアウト・描画(dirty フラグで差分更新)
-
Widget — UI の設計図。
abstract recordで不変。フレームごとに再生成されても==で差分検知できる。 - Element — Widget と RenderObject を橋渡しする永続ノード。ウィジェットが更新されても Element は再利用される。再ビルドの仕組みは BuildPipeline を参照。
-
RenderObject —
PerformLayoutとPaintを実装する描画エンジン。詳細は RenderObjects を参照。
| 基底クラス | 役割 | 対応する Element |
|---|---|---|
Widget |
すべてのウィジェットの基底。CreateElement() を宣言 |
— |
StatelessWidget |
Build(IBuildContext) で子ツリーを返す純粋関数コンポーネント |
StatelessElement ✓ |
StatefulWidget<T> |
CreateState() で State<T> を分離 |
StatefulElement ✓ |
InheritedWidget |
ツリー下方へのコンテキスト伝播 |
InheritedElement ✓ |
ProxyWidget |
RenderObjectを作らず、単一の Child へ構成を委譲 |
ProxyElement ✓ |
ParentDataWidget<T> |
親RenderObjectが子ごとに持つレイアウト情報を設定 |
ParentDataElement<T> ✓ |
RenderObjectWidget<T> |
CreateRenderObject() / UpdateRenderObject(T) で RenderObject を所有 |
RenderObjectElement<T> ✓ |
SingleChildRenderObjectWidget<T> |
単一の Child を持つ RenderObjectWidget |
SingleChildRenderObjectElement<T> ✓ |
MultiChildRenderObjectWidget<T> |
Children(List<Widget>)を持つ RenderObjectWidget |
MultiChildRenderObjectElement<T> ✓(Key 対応の子リスト差分) |
RenderObjectToWidgetAdapter |
Widget ツリーのルートを RenderView に接続 |
RenderObjectToWidgetElement<RenderView> ✓ |
ParentDataWidget<T> は、自身ではRenderObjectを作らず、子RenderObjectの ParentData を更新します。
親RenderObjectは SetupParentData で T を用意し、派生Widgetは ApplyParentData(T) で値を比較・更新して、変更した場合だけ true を返します。
変更時の MarkNeedsLayout() は基底クラスが親RenderObjectへ伝播します。
Flexible や Positioned のように「親レイアウトだけが解釈する子ごとの情報」を宣言的なWidget APIとして表現するための基盤です。
対応するParentDataを用意しない親の下で使用すると InvalidOperationException になります。
状態を持たない純粋関数コンポーネント。Build(IBuildContext) でウィジェットツリーを返します。
public record MyWidget : StatelessWidget
{
public required string Title { get; init; }
public override Widget Build(IBuildContext context)
{
return new Center
{
Child = new Text(Title)
};
}
}Build() はマウント時と、MarkNeedsBuild() でスケジュールされた再ビルド時に BuildOwner から呼ばれます。
StatefulWidget<T> は Widget から State<T> を分離するパターンです。State.SetState(Action) は状態を書き換えたうえで Element.MarkNeedsBuild() を呼び、次フレームの BuildScope() で再ビルドされます。
public record WatchWidget : StatefulWidget<WatchWidget>
{
public override State<WatchWidget> CreateState() => new WatchState();
}
public record WatchState : State<WatchWidget>
{
private Timer? _timer;
private string _time = "00:00:00";
public override void InitState()
{
_timer = new Timer(_ => SetState(() => _time = DateTime.Now.ToString("HH:mm:ss")),
null, dueTime: 0, period: 1000);
}
public override Widget Build(IBuildContext context) => new Text(_time);
}(このサンプルの全体は samples/FloatSoda.Samples.OverlayApp/WatchWidget.cs にあります)
State<T> のライフサイクルメソッド: InitState() / SetState(Action) / DidUpdateWidget(T oldWidget) / DidChangeDependencies()。
ツリーの下方にコンテキスト(テーマなど)を伝播させるためのウィジェットです。InheritedElement が依存する子孫を追跡し、UpdateShouldNotify(InheritedWidget oldWidget) が true を返したときに依存側を再ビルド対象にします。
現在位置から最も近いスコープを読み、その更新通知を購読するには、IBuildContext.DependOnInheritedWidgetOfExactType<T>() を使います。テーマ側に Of(IBuildContext) を用意すると、利用側が照会方法を毎回書かずに済みます。
Builder は新しい IBuildContext を1段挟み、ChildBuilder で子を構築します。同じ Build() 内で作成した InheritedWidget を、その子側のコンテキストから解決したい場合に使います。
using FloatSoda.Elements;
using FloatSoda.Widgets;
using FloatSoda.Widgets.Components;
public sealed record AlbumTheme : InheritedWidget
{
public required string Title { get; init; }
public static AlbumTheme? Of(IBuildContext context) =>
context.DependOnInheritedWidgetOfExactType<AlbumTheme>();
public override bool UpdateShouldNotify(InheritedWidget oldWidget) =>
oldWidget is AlbumTheme oldTheme && oldTheme.Title != Title;
}
Widget album = new AlbumTheme
{
Title = "VRChat photos",
Child = new Builder
{
ChildBuilder = context =>
new Text(AlbumTheme.Of(context)?.Title ?? "No title")
}
};Issue 記載時の BuildContext ではなく、FloatSoda の公開コンテキスト契約である IBuildContext を受け取ります。
KeyedSubtree は子の内容を変えず、ラッパーに指定した Key でサブツリーの同一性を制御します。同じ位置・同じキーなら子の Element / State を保持し、キーを変えるとサブツリーを差し替えます。
new KeyedSubtree
{
Key = new ValueKey<string>(albumId),
Child = BuildAlbum(albumId)
};RepaintBoundary は子を独立した合成レイヤーへ記録します。境界内の MarkNeedsPaint() は RenderRepaintBoundary で止まり、変更されていない祖先を再描画しません。
using FloatSoda.Widgets.Paint;
new RepaintBoundary
{
Child = BuildFrequentlyChangingWidget()
};WIP:
FloatSoda.Hooksプロジェクトに R3 ベースのHookWidget/HookElementが部分実装されていますが、フレームワークのビルドループとは未統合です。HookExtensionのUseState/UseEffect/Depends/UseMemo/UseActionはNotImplementedExceptionを投げます。
HookWidget.Build() 内で UseState(initialValue) を呼ぶと ReactiveProperty<T> が返り、値の変更が再ビルドをトリガーする、という React フック風の API を目指しています。
// 構想中の API(未動作。Button は FloatSoda.UI.Cream などのデザインシステム層のもの)
public override Widget Build(IBuildContext context)
{
var count = UseState(0);
return new Button
{
Child = new Text($"Count: {count.Value}"),
OnPressed = () => count.Value++,
};
}| ウィジェット | 実装状況 | 説明 | 主なプロパティ |
|---|---|---|---|
Center |
✓ | 子を中央に配置(Align に委譲) |
Child |
Align |
✓ | 子を指定の Alignment で配置 |
Alignment, WidthFactor, HeightFactor, Child
|
Column |
✓ | 垂直方向に並べる(Flex に委譲) |
Children, MainAxisAlignment, CrossAxisAlignment, MainAxisSize
|
Row |
✓ | 水平方向に並べる(Flex に委譲) |
Children, MainAxisAlignment, CrossAxisAlignment, MainAxisSize
|
Flex |
✓ | 方向指定のフレックスレイアウト。UpdateRenderObject と Key 対応の子リスト差分に対応 |
Direction, Children, MainAxisAlignment, CrossAxisAlignment, VerticalDirection
|
SizedBox |
✓ | 固定サイズのボックス |
Width, Height, Child
|
ConstrainedBox |
✓ | 親の制約と交差する追加制約を子へ適用 |
AdditionalConstraints (BoxConstraints, 必須), Child
|
Padding |
✓ | 子の制約を余白分だけ縮小し、子を余白の左上位置へ配置 |
Spacing (EdgeInsets, 必須), Child
|
Stack |
✓ | 複数の子を重ね、非Positioned子をAlignmentとFitで配置 |
Children, Alignment, Fit
|
Positioned |
✓ |
Stackの子を辺からの距離または固定寸法で絶対配置 |
Left, Top, Right, Bottom, Width, Height, Child
|
Container |
✗ internal スタブ |
パディング・色・サイズなどを一括指定 | — |
ListView |
✗ internal スタブ |
スクロール可能なリスト | Children |
GridView |
✗ internal スタブ |
グリッドレイアウト | — |
SingleChildScrollView |
✗ internal スタブ |
単一子をスクロール | Child |
ConstrainedBox は、親から渡される制約を無視せず、その範囲内で追加の最小・最大サイズを子へ適用します。
Widget panel = new ConstrainedBox
{
AdditionalConstraints = new BoxConstraints(
MinWidth: 240,
MaxWidth: 400,
MinHeight: 120,
MaxHeight: 240),
Child = new SizedBox { Width = 320, Height = 180 }
};| ウィジェット | 実装状況 | 説明 | 主なプロパティ |
|---|---|---|---|
ColoredBox |
✓ | 単色背景 |
Color (Color), Child
|
DecoratedBox |
✓ |
BoxDecoration の背景色・角丸・ボーダーを子の前面または背面へ描画 |
Decoration, Position, Child
|
Image (Paint) |
✓ |
ImageProvider 経由で画像を表示 |
ImageProvider, Child
|
ClipRect |
✓ | 矩形クリップ |
Clipper, ClipBehavior, Child
|
ClipRoundRect |
✓ | 角丸矩形クリップ |
BorderRadius, Clipper, ClipBehavior, Child
|
ClipOval |
✓ | 楕円クリップ |
CustomClipper, ClipBehavior, Child
|
ClipCustomPath |
✓ | カスタムパスクリップ |
Clipper, ClipBehavior, Child
|
Opacity |
✓ | 0から1までの固定不透明度を合成レイヤーで適用 |
Value, Child
|
Transform |
✓ | レイアウト後に Matrix3x2 の2次元変換を適用 |
Matrix, Origin, Alignment, TransformHitTests, Child
|
RepaintBoundary |
✓ | 子の再描画を独立した合成レイヤー内に限定 | Child |
| ウィジェット | 実装状況 | 説明 | 主なプロパティ |
|---|---|---|---|
FadeTransition |
✓ |
IAnimation<double> で子の不透明度を駆動(リビルド不要、ペイントのみ)→ Animation
|
Opacity (IAnimation<double>), Child
|
| ウィジェット | 実装状況 | 説明 | 主なプロパティ |
|---|---|---|---|
RichText |
✓ |
TextSpan でスタイル付きテキストを表示 |
Text (TextSpan) |
Text |
✓ | 単一書式のテキスト表示(RichText / TextSpan に委譲) |
Data (string), Style (TextStyle?) |
Text は表示文字列を単一値コンストラクタで受け、書式は init プロパティで指定します。Style を省略すると、フォントサイズ30、Arial、黒、ウェイト400の既定書式を使用します。空文字列は有効です。
using FloatSoda.Geometrics;
using FloatSoda.Painting;
using FloatSoda.Widgets.Components;
new Text("Hello, VR!")
{
Style = new TextStyle
{
FontSize = 36,
Color = new Color(255, 255, 255),
FontFamily = "Arial",
FontWeight = 700,
IsItalic = false
}
}Button / Icon はデザインシステム層(FloatSoda.UI.Cream / FloatSoda.UI.FizzyPop)へ移動しました。振る舞いを担うヘッドレスウィジェット(ButtonBase など)は FloatSoda.UI にあります。詳細は UILayering を参照してください。
| ウィジェット | 実装状況 | 説明 |
|---|---|---|
GestureDetector |
✗ スタブ | タップ・ドラッグ検知 |
Listener |
✗ スタブ | 低レベル入力ハンドラ |
IKey / ValueKey<T> / UniqueKey が定義され、Widget.Key プロパティと差分判定に組み込まれています。Widget.CanUpdate(old, new) は「同じ実行時型かつ Key が等しい」なら既存 Element を再利用します(Flutter と同じ型 + Key 判定)。Element.UpdateChild は先に record 等値の高速パスで同一 Widget をスキップし、その後 CanUpdate で更新可否を判断します。MultiChildRenderObjectElement の子リスト差分でも Key を使って要素の同一性を追跡します(詳細は BuildPipeline)。
既存の子ウィジェット自体を変更せずにキーを付けたい場合は、KeyedSubtree の Key と Child を指定します。
- BuildPipeline — BuildOwner / dirty list / UpdateChild の詳細
- RenderObjects — Widget が生成する RenderObject のリファレンス
- GettingStarted — Widget を使った最初のアプリ
- UILayering — ヘッドレスUI層とデザインシステム層の構成
- APIDesign — ウィジェット API の設計規約