FlutterカスタムWidgetで実現!サイズ制御を簡単にするFlex系レイアウト
FlutterでUIを組む中で、Widgetのサイズをどう指定・取得するかは思いのほか手間がかかります。
例えば Container に padding や border をつけた際に、思った通りのサイズにならなかったり、 比率でWidgetを配置したいのに Expanded をいちいち噛ませなければいけなかったり。 SafeAreaとの兼ね合いなども含めて、サイズ周りはなんかめんどくさくなりがち...
そこで、自分の中でもっとシンプルに「サイズを制御できるWidget構成」があったらと思い、 今回の Flex 系ウィジェットを作ってみました!
比率での指定、固定サイズとの併用、装飾、直感的かつシンプルな余白、オフセットの取得など、 レイアウトに関わる面倒ごとを少しでも軽減できるよう設計しています。
※以下、Flex系といったらこちらにあるFlexから始まるクラスのみを含むものとします。
背景と目的
標準の Row や Column を使ったレイアウトは非常に便利ですが、 細かいサイズ指定や比率指定を伴うケースではやや冗長になりがちです。
また、共通の装飾を含めたい場合にも、 ContainnerなどでWidgetを毎回ラップする必要があり、書いていてめんどくさくなったり見通しが悪くなることがあります。
そこで以下のような目的でFlexLayoutを作りました:
- 比率 (
weight) と固定長 (sideLength) を組み合わせた柔軟なサイズ指定 - パディングやボーダー、装飾を1つのWidgetで書ける
- HTMLで言うところの
box-sizingみたいな感じに -
LayoutBuilderやらSafeAreaやらはFlexLayoutにお任せ! - グローバル座標取得用のオプション(必要な時だけ)
基本構成
FlexLayoutConstraints / FlexLayoutSize
サイズ指定する際の登場人物
-
FlexLayoutSize: 今回紹介するFlex系のサイズを表してる -
FlexLayoutConstraints:FlexLayoutSizeをよしなに生成してくれる
サイズの指定方法は以下の通り:
-
FlexLayoutConstraintsから生成(FlexRowならwidth, FlexColumならheight)-
weight(double):残りの空間を比率で分けます -
extend(): 残りの空間いっぱいに広がる。weight(1)と同等です -
sideLength(double):固定長を指定し、残りから差し引きます
-
-
FlexLayoutSize.size(double, double): 直接widthとheightを指定する。Scrollの中とかで活躍します
この仕組みにより、比率ベースと固定長を混ぜたWidget構成も扱いやすくなっています。
paddingとinnerPadding
FlexContainnerなどは指定できるpaddingとinnerPaddingがあります。Flex系のやつに指定するサイズをouterSizeとした場合、その中の一番外側にpaddingが入ります。paddingよりも内側から、FlexContainnerなどで指定できるdecoration...つまり装飾系が効きます。そして、枠線をそこで指定した場合は装飾系が効く、一番外側になります。枠線の次にinnerPaddingが効いて、innerPaddingよりも内側全てが子要素ないし子要素たちに与えられた最大限のスペースです。これが、constraints.parentWidth * constraints.parentHeightとなります。(ここで言うconstraintsはFlexLayoutConstraintsです)
つまり...
padding=>枠線=>innerPadding=>子要素の最大サイズ
...となります。
実際の使い方
Flex系はFlexLayoutを除いて、全てFlexLayoutSizeを指定して使います。そしてFlex系を使う場合、最初は大体FlexLayoutから始めます。FlexLayoutは自身のサイズを親要素のサイズなどから自動で決めます。あとはFlexColum, FlexContainer, FlexSimpleItemなどを直感的に使います。
以下のコードは画面(親要素)いっぱいの領域内に、上から順にタイトル、内容(内部でさらに幅を比率分割)、フッターって感じに表示します。
例文
FlexLayout(
isSafeArea: true,
builder: (size) => FlexColum(
size: size,
builder: (constraints) => [
FlexContainer(
size: constraints.sideLength(80),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.blueGrey.shade50,
border: Border.all(
color: Colors.deepPurple,
width: 2,
),
borderRadius: BorderRadius.circular(8),
),
alignment: Alignment.center,
builder: (constraints) => Text(
"タイトルエリア ${constraints.parentWidth} * ${constraints.parentHeight}",
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
),
FlexSpacer(size: constraints.sideLength(16)),
FlexRow(
size: constraints.extend(),
builder: (constraints) => [
FlexContainer(
size: constraints.weight(1),
decoration: BoxDecoration(
color: Colors.lightGreen.shade100,
),
alignment: Alignment.center,
neededOffset: true,
builder: (constraints) => Text(
"コンテンツA\n(幅 Weight 1)\n offset: ${constraints.parentOffset}",
textAlign: TextAlign.center,
),
),
FlexSpacer(size: constraints.sideLength(8)),
FlexColum(
size: constraints.weight(2),
decoration: BoxDecoration(
color: Colors.orange.shade100,
),
builder: (constraints) => [
FlexContainer(
size: constraints.sideLength(50),
decoration: BoxDecoration(
color: Colors.red.shade100,
),
alignment: Alignment.center,
builder: (constraints) => const Text("固定要素 (高さ50)"),
),
FlexContainer(
size: constraints.weight(1),
decoration: BoxDecoration(
color: Colors.cyan.shade100,
),
builder: (constraints) => const Text("残りを全部使う要素"),
),
],
),
],
),
FlexSpacer(size: constraints.sideLength(16)),
FlexContainer(
size: constraints.sideLength(60),
decoration: BoxDecoration(
color: Colors.blue.shade50,
),
builder: (_) => const Center(
child: Text(
"フッターエリア",
style: TextStyle(
fontStyle: FontStyle.italic,
),
),
),
),
],
),
);
特徴と設計上の工夫
共通処理の抽出
-
_BaseFlex抽象クラスで装飾・パディング・変換などの共通処理を一括管理 - 子ウィジェットは
buildLayout(...)経由で描画され、ContainerかSizedBoxを適切に切り替え
必要なときだけオフセット取得
-
neededOffset: trueを指定すると、GlobalKeyが有効になり、FlexLayoutConstraints.outerOffsetからグローバル座標を取得可能になります - 不要時は無効化されるため、パフォーマンスを損なわないです
各クラス大雑把な説明
共通事項など
neededOffsetをtrueにして、かつneededOffsetを指定したFlex系のbuilderから出てるFlexLayoutConstraintsのouterOffsetを参照すると位置を取得できます。GlobalKeyを取って何して...ってよく忘れるような面倒臭いことをしなくて済むようになります。但し、buildが終わった後に限ります。
下記に記すWidgetはどれもisShowingProgressという引数があり、これをtrueにするといわゆるぐるぐるするやつを表示できるがそこは適当なので実用性はないと思って良いです。軽くぐるぐるを表示するのには使えるといった程度。恐らくいつか無くなります。
FlexLayout
大体一番最初に使うやつ。自動でサイズを決めてくれます。引数にwidthとheightがありそれぞれを指定することもできます。何も指定しないと親要素いっぱいに広がります。isSafeAreaをtrueに設定しないとセーフエリアの外まで広がるため注意。isTopSafeAreaなどもあります。
FlexSimpleItem
Flex系で、SizedBox感覚で使えるやつ。paddingの指定は可能だが装飾などは一切なしです
FlexSpacer
Flex系で、単純にスペーサーの役割を持ってるだけ。sizeしか指定できません。
FlexContainner
Flex系で、Containner感覚で使えるやつ。基本Containnerで指定できるものは指定できるって思ってください。
FlexRow
Flex系で、Row感覚で使えるやつ。基本Containnerで指定できるもの + Rowで指定できるものは指定できるって思ってください。
FlexColum
Flex系で、Colum感覚で使えるやつ。基本Containnerで指定できるもの + Columで指定できるものは指定できるって思ってください。
FlexStack
Flex系で、Stack感覚で使えるやつ。基本Containnerで指定できるもの + Stackで指定できるものは指定できるって思ってください。一つ注意としたら、FlexStackのbulderから出てくるのはFlexLayoutConstraintsではなくFlexLayoutSizeってぐらいです。
FlexDialog
今回は説明を割愛します。詳しくはコードを見てみてください。(ごめんなさい!)
まとめ
このFlexLayoutシリーズは、Flutterの標準的なWidgetでは少し手間がかかる サイズ管理や装飾の構成を簡潔にしようとする取り組みのひとつです。
設計や構築方針が決まっている中で、Widgetごとのサイズ指定や 全体の比率調整を気軽にやりたい場合に特に効果があります。
全コードはGitHubにまとめていますので、必要に応じてご覧ください。
👉 GitHub - TowelMan-public/qiita-posts-flutter/flex_layout
※一部kotlinのようにlet、alsoが使われています。
質問や改善提案なども歓迎です。