今回読んだページ
個人的なまとめ
- Widgetは、自身の制約を、その親から取得する。
- 制約は、最小・最大の幅と、最小・最大の高さの、4つのdoubleのセットである。
-
ConstrainedBoxは親から受けとった制約に追加の制約を課すだけである。
Understanding constraints
制約は下がる。(階層的な意味か??)
サイズは上がる。
親が位置をセットする。
Flutterを学習している人から「width:100をセットしたWidgetが100pxの幅でないことがあるのは何故か」と尋ねられたとき、デフォルトの答えは、そのWidgetをCenterに配置するように指示することである。
そんなことしないで!!
もしそうした場合、彼らは何度も戻ってきて、何故FittedBoxが動かないのか、何故Columnがはみ出るのか、あるいは、IntrinsicWidthが行うことになっていることを尋ねてくる。
代わりに、まずFlutterのレイアウトはHTMLのレイアウトとは大きく違うことを教えて、次に以下のようなルールを覚えさせる。
Constraints go down. Sizes go up. Parent sets position.
このルールを知らないと、Flutterのレイアウトを本当に理解することはできないので、
Flutterの開発者たちは早い段階でこれを学ぶべきである。
詳細:
- Widgetは、自身の制約を、その親から取得する。制約は、最小・最大の幅と、最小・最大の高さの、4つのdoubleのセットである。
- それから、そのWidgetは自身の子リストを通過する。そのWidgetは1つずつ子の制約が何かを教え(子によって異なる可能性がある)、各子にどのくらいのサイズが欲しいか尋ねる。
- そして、そのWidgetは子たちを1つずつ配置する。
- 最後に、そのWidgetは、その親に自身のサイズを伝える(もちろん、元の制約の範囲内で)。
例えば、構成されたWidgetがいくらかのpaddingを持ったColumnを含み、2つの子たちを以下のように配置したい場合:
交渉は次のようになる。
Widget: 「親よ、私の制約はなんですか?」
Parent: 「幅は90~300pxで、高さは30~85だよ。」
Widget: 「うーん、5pxのpaddingが欲しいから、私の子供達は最大で290pxの幅と75pxの高さを持てますね。」
Widget: 「1番目の子よ、君は0~290pxの幅、0~75pxの高さである必要があるよ。」
First child: 「OKです。でしたら290pxの幅と、20pxの高さが欲しいです。」
Widget: 「うーん、2番目の子は1番目の子の下におきたいので、2番目の子の高さは55pxしか残っていません。」
Widget: 「2番目の子よ、君は0~290の幅、0~55pxの高さである必要があるよ。」
Second child: 「OKです、140pxの幅と、30pxの高さが欲しいです。」
Widget: 「結構。 1番目の子の位置はx:5, y:5で、2番目の子の位置はx:80,y:25です。」
Widget: 「親よ、私のサイズは300pxの幅と、60pxの高さに決めました。」
Limitations
上述したレイアウトのルールの結果、Flutterのレイアウトエンジンには、いくつかの重要な制限がある。
- Widgetは、その親から得た制約内でのみ自身の大きさを決定できる。これは、Widgetはたいてい必要なサイズにすることはできないことを意味する。
- Widgetの位置を決定するのはその親なので、Widgetはスクリーン内の自身の位置を知らないし、決定することもできない。
- 同様に、親の大きさと位置はそのさらに親(親の親)に依存するので、ツリー全体を考慮せずにWidgetのサイズと位置を正確に定義することは不可能である。
Examples
インタラクティブな体験をするには、以下のDartPadを使用する。(ここで使っているのはCodePen)
番号つきの水平スクロールバーを使用して、29個の様々な例を切り替える。
See the Pen flutter-constraints-example29 by popy1017 (@popy1017) on CodePen.
各例については、以下で説明する。
Example 1
Container(color: Colors.red)
スクリーンはContainerの親で、Containerにスクリーンと正確に同じサイズであることを強制している。
したがって、Containerはスクリーンを赤く塗りつぶしている。
Example 2
Container(width: 100, height: 100, color: Colors.red)
赤いContainerは100x100を希望したが、スクリーンがスクリーンと同じサイズになるように強制しているためそれはできない。
そのため、Containerはスクリーンを塗りつぶしている。
補足
コードを読むと、(100行目あたりから)
child: Container(
width: 400,
height: 670,
color: Color(0xFFCCCCCC),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: ConstrainedBox(
constraints: BoxConstraints.tightFor(
width: double.infinity, height: double.infinity),
child: widget.examples[count - 1] // <- これが赤い部分
)
),
となっているので、このContainerの親はConstrainedBoxである。(Deeply nested...)
1. Container: 制約(width: 400, height: 670)
↓
2. Column: 制約(指定なし)
↓
3. Expanded: 制約(指定なし)
↓
4. ConstrainedBox:
制約(BoxConstraints.tightFor(
width: double.infinity,
height: double.infinity
)
)
↓
5. Container(赤い部分): 制約(width:100, height:100)
「スクリーンと正確に同じサイズであることを強制している」と言っているのが4番目のConstrainedBoxである。
このWidgetでは、ConstrainedBox.constraintsにBoxConstraints.tightForが指定されており、こいつの役割は、「子のサイズを、指定した幅と高さに固定する」というもの。
ここで指定されている幅と高さは両方ともdouble.infinityであり、子を可能な限り大きくするという意味になる。
この例では、親であるConstrainedBoxの「可能な限り大きくなれよ」という命令に対し、子は「100x100がいいです」と言っているため、それは聞き入れてもらえず、結果的に可能な限り大きくなった。(のだと思われる。)
Example 3
Center(
child: Container(width: 100, height: 100, color: Colors.red)
)
スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。
CenterはContainerに、スクリーン(400x670)より大きくはできないけど、どんなサイズでも良いよと言っている。
そのため、Containerは100x100となっている。
Example 4
Align(
alignment: Alignment.bottomRight,
child: Container(width: 100, height: 100, color: Colors.red),
)
これは、Centerの代わりにAlignを使っている点で、前の例とは異なっている。
AlignもContainerに、サイズは何でも良いと言っているが、空のスペースがある場合は、コンテナを中央に配置しない。(Alignment.bottomRightが指定されているので)
代わりに、Containerを利用可能スペースのな右下に揃えている。
Example 5
Center(
child: Container(
color: Colors.red,
width: double.infinity,
height: double.infinity,
)
)
スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。
CenterはContainerに、スクリーンより大きくなければ、どんなサイズでも良いとしている。
Containerは無限のサイズを御所望だが、スクリーンよりは大きくなれないので、結果的にスクリーン全体を赤く色付けている。
Example 6
Center(child: Container(color: Colors.red))
スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。
CenterはContainerにスクリーンより大きくはできないけど、好きなサイズで良いと言っている。
Containerは子を持たず、固定されたサイズもないので、可能な限り大きくすると決めた結果、スクリーン全体を満たしている。
しかし、何故Containerがそれを決めるのだろう?
単純にContainerwidgetを作った人による設計上の決定だからである。
これは異なる方法で作成された可能性があり、コンテナのドキュメントを読んで動作を理解する必要がある。
Example 7
Center(
child: Container(
color: Colors.red,
child: Container(color: Colors.green, width: 30, height: 30),
)
)
スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。
Centerは赤いContainerにスクリーンより大きくはできないけど、好きなサイズで良いと言っている。
赤いContainerはサイズは持たないが、子を持っていて、子と同じサイズであることを決めている。
赤いContainerはその子に、スクリーンよりは大きくできないけど、希望するサイズで良いと言っている。
その子は、30x30を希望する緑のContainerである。
赤いContainerは、自身のサイズを子のサイズに合わせてサイズ調整するため、30x30になる。
緑のContainerは赤いContainerを覆ってしまうので、赤色は見えない。
"Sizes go up"とはこういうことなのか。
Example 8
Center(
child: Container(
color: Colors.red,
padding: const EdgeInsets.all(20.0),
child: Container(color: Colors.green, width: 30, height: 30),
)
)
赤いContainerは、自身の大きさをその子の大きさに合わせるが、自身のpaddingが考慮される。
そのため、30x30 + paddingとなる。
paddingのおかげで赤色は見えていて、緑のContainerは前のサンプルと同じ大きさである。
Example 9
ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
Containerは70〜150pxでなければならない、と推測するかもしれないが、それは間違いである。
ConstrainedBoxは親から受けとった制約に追加の制約を課すだけである。
ここでは、スクリーンはConstrainedBoxにスクリーンと正確に同じ大きさであることを強制しているため、それは子であるContainerにもスクリーンのサイズであるべきであると伝える。
したがって、constraintsパラメータは無視される。
Example 10
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 10, height: 10),
)
)
ここでは、CenterによりConstrainedBoxを画面サイズまでの任意のサイズにすることができる。
ConstrainedBoxは子に対して、constraintsパラメータから追加の制約を課している。
Containerは70~150pxでなければならない。
それは、10x10を希望しているので、最終的には最小値である70pxになる。
Example 11
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 1000, height: 1000),
)
)
Example 10の逆パターンで、制約である大きさを超えて指定された場合の挙動。
この場合は、maxWidthやmaxHeightで指定した大きさになる。
CenterによりConstrainedBoxを画面サイズまでの任意のサイズにすることができる。
ConstrainedBoxは子に対して、constraintsパラメータから追加の制約を課している。
Containerは70~150pxである必要があるが、1000pxを希望しているため、最終的には(制約の)最大値である150になる。
Example 12
Center(
child: ConstrainedBox(
constraints: BoxConstraints(
minWidth: 70,
minHeight: 70,
maxWidth: 150,
maxHeight: 150,
),
child: Container(color: Colors.red, width: 100, height: 100),
)
)
Example10とExample11は制約の範囲(min~max)を満たさない場合の挙動だったが、
Example12は制約の範囲を満たす場合の挙動
CenterによりConstrainedBoxを画面サイズまでの任意のサイズにすることができる。
ConstrainedBoxは子に対して、constraintsパラメータから追加の制約を課している。
Containerは70~150pxである必要があるが、100pxを希望していて、それは70~150pxの間であるので、100pxとなっている。
Example 13
UnconstrainedBox(
child: Container(color: Colors.red, width: 20, height: 50),
)
スクリーンはUnconstrainedBoxに正確にスクリーンと同じサイズであることを強制している。
しかし、UnconstrainedBoxを使うと、子であるContainerを任意のサイズにすることができる。
Example 14
UnconstrainedBox(
child: Container(color: Colors.red, width: 4000, height: 50),
)
スクリーンはUnconstrainedBoxに正確にスクリーンと同じサイズであることを強制し、UnconstrainedBoxを使うと、子であるContainerを任意のサイズにすることができる。
残念ながら、この場合、Containerは4000pxの幅でUnconstrainedBoxに収まりきらないので、警告が出ている。
Example 15
OverflowBox(
minWidth: 0.0,
minHeight: 0.0,
maxWidth: double.infinity,
maxHeight: double.infinity,
child: Container(color: Colors.red, width: 4000, height: 50),
);
スクリーンは、OverflowBoxに同じサイズであることを強制し、OverflowBoxはその子であるContainerに任意のサイズを許容している。
OverflowBoxはUnconstrainedBoxと似ており、違いは、子がスペースに合わなくても決して警告を出さないことである。
この場合、Containerは4000pxの幅を持ち、OverflowBoxのサイズを超えているが、OverflowBoxは警告を出さずに、可能な限り表示している。
(やっと半分か。。。)
Example 16
UnconstrainedBox(
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
何も表示されず、コンソールにエラーが表示されるだろう。
UnconstrainedBoxを使うと、その子が任意のサイズになるようにできるが、子であるContainerが無限のサイズを指定している。
Flutterは無限のサイズは描画できないため、以下のようなメッセージでエラーを投げる:
BoxConstraints forces an infinite width.
Example 17
UnconstrainedBox(
child: LimitedBox(
maxWidth: 100,
child: Container(
color: Colors.red,
width: double.infinity,
height: 100,
)
)
)
ここではもうエラーは発生しない。
LimitedBoxがUnconstrainedBoxによって無限のサイズを与えられると、LimitedBoxはその子に対して100pxの最大幅を渡しているからである。
UnconstrainedBoxをCenterwidgetに変えると、LimitedBoxはその制限を適用しなくなり(その制約はLimitedBoxが無限の制約を受け取ったときのみ適用されるため)、Containerの幅は、100を超えて拡大できる。
これは、LimitedBoxとConstrainedBoxの違いを説明している。
Example 18
FittedBox(
child: Text('Some Example Text.'),
)
スクリーンは、FittedBoxにスクリーンと正確に同じサイズであることを強制する。
テキストには、テキストの量やフォントサイズなどに応じた自然な幅がある。
FittedBoxを使うと、Textが任意のサイズとなるようにできるが、
TextがそのサイズをFittedBoxに伝えた後で、FittedBoxはすべての利用可能な幅を満たすまでTextを拡大する。
Example 19
Center(
child: FittedBox(
child: Text('Some Example Text.'),
)
)
しかし、FittedBoxをCenterwidgetの中に入れると何が起こるだろう?
Centerを使うと、FittedBoxはスクリーンのサイズまでの任意のサイズになることができるようになる。
FittedBoxは自身をTextと大きさを合わせ、Textが任意のサイズとなれるようにしている。
FittedBoxとTextが両方とも同じ大きさであるため、拡大は起こらない。
Example 20
Center(
child: FittedBox(
child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
)
しかし、FittedBoxはCenterwidgetの中にあるが、Textがスクリーンに収まりきらない場合はどうなるだろう?
FittedBoxは自身のサイズをTextに合わせようとするが、スクリーンより大きくなることはできない。
そのため、スクリーンのサイズとなり、Textもスクリーンに合わせてサイズ変更される。
Example 21
Center(
child: Text('This is some very very very large text that is too big to fit a regular screen in a single line.'),
)
しかし、FittedBoxを削除した場合、Textはスクリーンから最大幅を取得し、画面に合わせて改行する。
Example 22
FittedBox(
child: Container(
height: 20.0,
width: double.infinity,
)
)
FittedBoxは(無限でないwidthとheightを持つ)境界付きのWidgetのみをスケールする。
それ以外は何も描画せず、コンソールにエラーが表示される。
Example 23
Row(
children:[
Container(color: Colors.red, child: Text('Hello!')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
スクリーンは、Rowにスクリーンと同じサイズになるように強制する。
UnconstrainedBoxのように、Rowはその子たちに対してなんの制約も課さない代わりに、任意のサイズになれるようにする。
Rowはそれらを横に並べるので、余りのスペースは空のままとなる。
Example 24
Row(
children:[
Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.')),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
Rowはchildrenに何の制約も課さないので、子が大きすぎてRowの使用可能な幅に収まらない可能性がある。
この場合、UnconstrainedBoxのように、Rowは"overflow warning"を表示する。
Example 25
Row(
children:[
Expanded(
child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))
),
Container(color: Colors.green, child: Text('Goodbye!')),
]
)
Rowの子がExpandedwidgetにラップされているとき、Rowはこの子に自身の幅を定義させない。
代わりに、他の子に応じてExpandedの幅を定義し、Expandedwidgetは子にExpandedの幅を持つよう強制する。
言い換えれば、Expandedを使うと、元の子の幅は無関係となり無視される。
Example 26
Row(
children:[
Expanded(
child: Container(color: Colors.red, child: Text(‘This is a very long text that won’t fit the line.’)),
),
Expanded(
child: Container(color: Colors.green, child: Text(‘Goodbye!’),
),
]
)
RowのすべてのchildrenがExpandedwidgetにラップされている場合、各Expandedはそのflexパラメータに比例するサイズを持ち、各Expandedはchildに対してExpandedと同じ幅を持つことを強制する。
言い換えれば、Expandedはchildrenの適切な幅を無視する。
Example 27
Row(children:[
Flexible(
child: Container(color: Colors.red, child: Text('This is a very long text that won’t fit the line.'))),
Flexible(
child: Container(color: Colors.green, child: Text(‘Goodbye!’))),
]
)
Expandedの代わりにFlexibleを使った場合の唯一の違いは、
Flexibleを使うと、childがFlexibleと同じか小さい幅を持つようになることである。(Expandedはchildに、同じ幅を持つよう強制する)
しかし、ExpandedとFlexibleは両方とも、自身のサイズ変更時にchildrenの幅を無視する。
Note: つまり、
Rowの子をサイズに比例して拡張することはできない。
Rowは、正確なchildの幅を使うか、ExpandedやFlexible`を使う時は完全に無視するかである。
Example 28
Scaffold(
body: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
]
)))
スクリーンはScaffoldにスクリーンと全く同じサイズであることを強制しているので、Scaffoldはスクリーンを満たしている。
ScaffoldはContainerに、スクリーンよりは大きくなれないけど、任意のサイズで良いとと伝えている。
Note: あるWidgetがそのchildに対してある大きさよりも小さくなれるよと伝えるとき、我々はWidgetがその子に緩やかな制約を提供する、と言う。詳しくはあとで。
Example 29
Scaffold(
body: SizedBox.expand(
child: Container(
color: blue,
child: Column(
children: [
Text('Hello!'),
Text('Goodbye!'),
],
))))
ScaffoldのchildにScaffoldのサイズと全く同じになって欲しいとするなら、
SizedBox.expandで子をラップすることで実現できる。
Note: あるWidgetがそのchildにあるサイズである必要があると伝えるとき、我々はwidgetがその子に対して厳しい制約を与える、と言う。
Tight vs loose constraints
制約が「厳しい」または「緩い」と聞くのはあまりに一般的なので、その意味を知っておく価値がある。
「厳しい」制約は、単一の可能性、正確なサイズを提供する。
つまり、厳しい制約は、その最大幅と最小幅が同じであり、最大の高さと最小の高さも同じであるということである。
Flutterのbox.dartファイルを見て、BoxConstraintsコンストラクタを検索すると、以下のようなものが見つかる:
BoxConstraints.tight(Size size)
: minWidth = size.width,
maxWidth = size.width,
minHeight = size.height,
maxHeight = size.height;
もう1度、上記のExample 2を見ると、スクリーンは赤いContanerに、スクリーンと全く同じサイズであることを強制していることがわかる。
もちろん、スクリーンはContainerに対して厳しい制約を渡すことによってそれを行う。
一方で、「緩い」制約は、最大の幅と高さは指定するが、widgetが望む限り小さくなることを許す。
言い換えれば、緩い制約は最小の幅と高さが両方とも0であるということである。
BoxConstraints.loose(Size size)
: minWidth = 0.0,
maxWidth = size.width,
minHeight = 0.0,
maxHeight = size.height;
もう1度Example 3を見ると、Centerが赤いContainerを画面より小さくすることを許していることがわかる。
もちろん、CenterはContainerに緩い制約を渡すことでそれを行っている。
結局のところ、Centerの目的は、親(スクリーン)から受け取った厳しい制約を、その子(Container)に対して緩い制約に変換することである。
Learning the layout rules for specific widgets
一般的なレイアウトルールを知ることは必要だが、それだけでは不十分である。
一般的なルールを適用するとき、各Widgetには多くの自由があるので、Widgetの名前を読むだけで何ができるのかを知る方法はない。
推測しようとしても、きっと間違えてしまうだろう。
ドキュメントを読んだり、ソースコードを勉強しない限りは、あるwidgetがどう動くかを正確に知ることはできない。
レイアウトのソースコードはたいてい複雑であるため、おそらくドキュメントを読んだほうが良いだろう。
しかし、レイアウトのソースコードを勉強すると決めたのであれば、IDEのナビゲーション機能を使って簡単に探すことができる。
ここに例を示す:
- コードから
Columnを見つけ、ソースコードに移動する。これを行うには、Android StudioかIntelliJでcommand+B(macOS)あるいはcontrol+B(Windows/Linux)を使う。basic.dartに移動するだろう。ColumnはFlexの拡張なので、Flexのソースコードに移動する(basic.dartの中)。 -
createRenderObject()と呼ばれる関数を見つけるまでスクロールする。見るとわかるように、この関数はRenderFlexを返す。これはColumnのレンダーオブジェクトである。ここで、RenderFlexのソースコードに移動する。flex.dartファイルに移動するだろう。 -
performLayout()関数を見つけるまでスクロールする。これは、Columnのレイアウトを行う関数である。