13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Flutterの公式ドキュメントを読んで理解する 〜Building layouts/Understanding constraints〜

13
Posted at

今回読んだページ

個人的なまとめ

  • 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.constraintsBoxConstraints.tightForが指定されており、こいつの役割は、「子のサイズを、指定した幅と高さに固定する」というもの。
ここで指定されている幅と高さは両方ともdouble.infinityであり、子を可能な限り大きくするという意味になる。
この例では、親であるConstrainedBoxの「可能な限り大きくなれよ」という命令に対し、子は「100x100がいいです」と言っているため、それは聞き入れてもらえず、結果的に可能な限り大きくなった。(のだと思われる。)

Example 3

Center(
   child: Container(width: 100, height: 100, color: Colors.red)
)

スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。

CenterContainerに、スクリーン(400x670)より大きくはできないけど、どんなサイズでも良いよと言っている。
そのため、Containerは100x100となっている。

Example 4

Align(
   alignment: Alignment.bottomRight,
   child: Container(width: 100, height: 100, color: Colors.red),
)

これは、Centerの代わりにAlignを使っている点で、前の例とは異なっている。

AlignContainerに、サイズは何でも良いと言っているが、空のスペースがある場合は、コンテナを中央に配置しない。(Alignment.bottomRightが指定されているので)
代わりに、Containerを利用可能スペースのな右下に揃えている。

Example 5

Center(
   child: Container(
      color: Colors.red,
      width: double.infinity,
      height: double.infinity,
   )
)

スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。

CenterContainerに、スクリーンより大きくなければ、どんなサイズでも良いとしている。
Containerは無限のサイズを御所望だが、スクリーンよりは大きくなれないので、結果的にスクリーン全体を赤く色付けている。

Example 6

Center(child: Container(color: Colors.red))

スクリーンは、Centerに対して正確にスクリーンと同じサイズであることを強制しているので、Centerはスクリーン全体に表示されている。

CenterContainerにスクリーンより大きくはできないけど、好きなサイズで良いと言っている。
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に任意のサイズを許容している。

OverflowBoxUnconstrainedBoxと似ており、違いは、子がスペースに合わなくても決して警告を出さないことである。

この場合、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,
      )
   )
)

ここではもうエラーは発生しない。
LimitedBoxUnconstrainedBoxによって無限のサイズを与えられると、LimitedBoxはその子に対して100pxの最大幅を渡しているからである。

UnconstrainedBoxCenterwidgetに変えると、LimitedBoxはその制限を適用しなくなり(その制約はLimitedBoxが無限の制約を受け取ったときのみ適用されるため)、Containerの幅は、100を超えて拡大できる。

これは、LimitedBoxConstrainedBoxの違いを説明している。

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.'),
   )
)

しかし、FittedBoxCenterwidgetの中に入れると何が起こるだろう?
Centerを使うと、FittedBoxはスクリーンのサイズまでの任意のサイズになることができるようになる。

FittedBoxは自身をTextと大きさを合わせ、Textが任意のサイズとなれるようにしている。
FittedBoxTextが両方とも同じ大きさであるため、拡大は起こらない。

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.'),
   )
)

しかし、FittedBoxCenterwidgetの中にあるが、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 wont 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に、同じ幅を持つよう強制する)
しかし、ExpandedFlexibleは両方とも、自身のサイズ変更時にchildrenの幅を無視する。

Note: つまり、Rowの子をサイズに比例して拡張することはできない。
Rowは、正確なchildの幅を使うか、ExpandedやFlexible`を使う時は完全に無視するかである。

Example 28

Scaffold(
   body: Container(
      color: blue,
      child: Column(
         children: [
            Text('Hello!'),
            Text('Goodbye!'),
         ]
      )))

スクリーンはScaffoldにスクリーンと全く同じサイズであることを強制しているので、Scaffoldはスクリーンを満たしている。
ScaffoldContainerに、スクリーンよりは大きくなれないけど、任意のサイズで良いとと伝えている。

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を画面より小さくすることを許していることがわかる。
もちろん、CenterContainerに緩い制約を渡すことでそれを行っている。
結局のところ、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に移動するだろう。ColumnFlexの拡張なので、Flexのソースコードに移動する(basic.dartの中)。
  • createRenderObject()と呼ばれる関数を見つけるまでスクロールする。見るとわかるように、この関数はRenderFlexを返す。これはColumnのレンダーオブジェクトである。ここで、RenderFlexのソースコードに移動する。flex.dartファイルに移動するだろう。
  • performLayout()関数を見つけるまでスクロールする。これは、Columnのレイアウトを行う関数である。
13
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?