OVERFLOW 是啥?


在〈Expanded 子元件〉、〈使用輸入欄位〉、〈滑動底盤 Bottom Sheet〉等文件中,曾經遇過幾次元件繪製時 OVERFLOW 的情況,也就是出現了黃黑條紋警訊的狀況,雖然通常遇上了,就是簡單地透過 ExpandedSingleChildScrollView 等就可以解決,選用什麼是視需求而定,不過最根本的還是要搞清楚,到底為什麼會 OVERFLOW?如此才能搞清楚,為什麼有些元件就不會 OVERFLOW,有些卻會?

不負責任的答案是,使用 RowColumn 很容易出現 OVERFLOW … XD

負責點但簡單的答案是,元件尺寸超過了被配給的可用空間。不過,可用空間是指什麼?元件尺寸又是什麼呢?元件的 widthheight 嗎?

首先要知道一件事,widthheight 不是元件最後真正的尺寸,基本上只是個參考用的資訊,能不能生效要看父元件給予的約束,也就是子元件可以獲得的最大寬高與最小寬高。

來看看作為 runApp 引數的根元件好了,給予根元件的約束,也就是根元件的最大寬高與最小寬高會是螢幕的尺寸,最大寬高與最小寬高都被限定的情況下,根元件 widthheight 會被忽略,獲得的可用空間也就是螢幕的尺寸,例如,雖然底下的 Container 設定了 widthheight,然而範例運行後,整個螢幕都會是紅色:

import 'package:flutter/material.dart';

void main() => runApp(
  Container(
    color: Colors.red,
    width: 100,
    height: 100,
  )
);

這是因為 widthheight 被忽略了,Container 直接使用整個可用空間(也就是螢幕尺寸)作為其尺寸,想讓 Containerwidthheight 生效?放個 Center

import 'package:flutter/material.dart';

void main() => runApp(
  Center(
    child: Container(
      color: Colors.red,
      width: 100,
      height: 100,
    ),
  )
);

Center 取得整個螢幕作為可用空間,也就是其尺寸會被強制設定螢幕尺寸,Center 也會給予子元件約束,也就是告訴子元件最大寬高是螢幕尺寸,只要不超過,你要多大都可以,因此上面的範例運行後,就會看到一個 100x100 的紅色方塊置中於螢幕,如果你將 Containerwidthheight 設得比螢幕尺寸大,Center 會忽略 widthheight,這時只會看到螢幕都會是紅色,不會 OVERFLOW。

如果不想約束子元件呢?可以使用 UnconstrainedBox

import 'package:flutter/material.dart';

void main() => runApp(
  UnconstrainedBox(
    child: Container(
      color: Colors.red,
      width: 100,
      height: 100,
    ),
  )
);

UnconstrainedBox 預設會有置中有效果,不會對子元件施加約束,就這個例子來說不會有問題,因為尺寸只有 100x100,不比螢幕大,如果設高一點呢?

import 'package:flutter/material.dart';

void main() => runApp(
  UnconstrainedBox(
    child: Container(
      color: Colors.red,
      width: 100,
      height: 1000, // 比螢幕高
    ),
  )
);

UnconstrainedBox 是根元件,取得的可用空間是整個螢幕,能給予子元件的可用空間最多也就是螢幕尺寸,然而子元件的尺寸超出了,UnconstrainedBox 又不約束子元件,這時就會看到 OVERFLOW 了:

OVERFLOW 是啥?

Flutter 的一些元件,可以明確地指定約束,通常是透過 constraints 之類的特性指定 BoxConstraints,也有個 ConstrainedBox 元件,可以對子元件進行約束。例如:

import 'package:flutter/material.dart';

void main() => runApp(
  UnconstrainedBox(
    child: ConstrainedBox(
      constraints: BoxConstraints(
        maxHeight: 300,
      ),
      child: Container(
        color: Colors.red,
        width: 100,
        height: 1000,
      ),
    ),
  )
);

這次故意在 UnconstrainedBox 中設定 ConstrainedBox 作為子元件,BoxConstraints 可以設定 maxHeightminHeightmaxWidthminWidth,也就是最大、最小的寬高約束,這次就不會 OVERFLOW 了:

OVERFLOW 是啥?

要注意的是,ConstrainedBox 是根據 constraints,對子元件施加額外限制,例如:

import 'package:flutter/material.dart';

void main() => runApp(
  UnconstrainedBox(
    child: ConstrainedBox(
      constraints: BoxConstraints(
        maxHeight: 150,
      ),
      child: ConstrainedBox(
        constraints: BoxConstraints(
          maxHeight: 300,
        ),
        child: Container(
          color: Colors.red,
          width: 100,
          height: 1000,
        ),
      ),
    ),
  )
);

上面的範例中,ConstrainedBox 中還有 ConstrainedBox,雖然內層的 maxHeight 是 300,然而外層的 maxHeight 已設定為 150,內層並不能大於外層,這時內層的 maxHeight 就會被忽略:

OVERFLOW 是啥?

相對應地,如果外層設定了 minWidthminHeight,內層的 ConstrainedBox 設定的 minWidthminHeight,就不能小於外層。

因此,若 ConstrainedBox 作為 runApp 的根元件,constraints 只會被忽略,例如,底下也是整個螢幕都是紅色的:

import 'package:flutter/material.dart';

void main() => runApp(
  ConstrainedBox(
    constraints: BoxConstraints(
      maxHeight: 300,
    ),
    child: Container(
      color: Colors.red,
      width: 100,
      height: 1000,
    ),
  )
);

因為 ConstrainedBox 作為根元件,最大、最小寬高被約束為螢幕尺寸,這時等於你怎麼設 ConstrainedBoxmaxHeightminHeightmaxWidthminWidth 來約束都沒有用,因為不能違反螢幕尺寸的最大、最小寬高約束。

為什麼 RowColumn 會容易出現 OVERFLOW 呢?RowColumn 預設不會對子元件作出約束,像是〈Expanded 子元件〉中第一個範例,Column 取得整個螢幕作為可用空間,子元件在沒有約束的情況下垂直排列,超出螢幕空間,也就 OVERFLOW 了。

結論就是,元件是否會 OVERFLOW,雖然主要是在於,元件尺寸是否超過了被配給的可用空間,然而可用空間有多少,必須搞清楚父子元件之間是否有約束關係。

另外,約束關係往往在決定了元件的 widthheight 之類的設定是否生效,如果你設定了 widthheight 卻沒有生效,也許就是父元件做出了約束。

另外,有些元件的 widthheight 設定,往往也會被拿來作為約束的參考,Container 就是個例子,它本身也可以設定 constraints,與 widthheight 的關係可以從建構式的原始碼中得知:

Container({
  Key key,
  this.alignment,
  this.padding,
  this.color,
  this.decoration,
  this.foregroundDecoration,
  double width,
  double height,
  BoxConstraints constraints,
  this.margin,
  this.transform,
  this.child,
  this.clipBehavior = Clip.none,
}) : assert(margin == null || margin.isNonNegative),
     assert(padding == null || padding.isNonNegative),
     assert(decoration == null || decoration.debugAssertIsValid()),
     assert(constraints == null || constraints.debugAssertIsValid()),
     assert(clipBehavior != null),
     assert(color == null || decoration == null,
       'Cannot provide both a color and a decoration\n'
       'To provide both, use "decoration: BoxDecoration(color: color)".'
     ),
     constraints = // width 與 height 會作為約束的來源資料
      (width != null || height != null)
        ? constraints?.tighten(width: width, height: height)
          ?? BoxConstraints.tightFor(width: width, height: height)
        : constraints,
     super(key: key);

有些 Flutter 的內建元件,在內部的組建過程中,可能各自有 AlignContainerConstrainedBox 之類的元件組成,因此情況可能更複雜,搞清楚這一切最好的方式,其實就在於 API 文件與原始碼,當然,如果想要有個比較簡單的開始,除了這邊的整理之外,也可以參考官方文件〈Understanding constraints〉。