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が使われています。
質問や改善提案なども歓迎です。