Flutter學習篇(四)—— 尺寸解惑

少年阿濤發表於2019-08-06

導航

前言

最近,筆者在寫佈局的時候,發現諸如此類的報錯:

Vertical viewport was given unbounded height.

Vertical viewport was given unbounded width.

複製程式碼

大概意思就是指沒有限定檢視的高度,寬度。典型的場景如下:

Column(
        children: <Widget>[
          ListView(
            children: <Widget>[
              Container(color:Colors.red, child:Text("1")),
              Container(color:Colors.orange, child:Text("2")),
            ],
          ),
        ],
      )
複製程式碼

在列檢視的子檢視新增了ListView, 就會報上面的錯:
Vertical viewport was given unbounded height.

分析

對於Column來講,主軸長度即垂直方向的高度是由MainAxisSize決定的,MainAxisSize有兩種型別,分別是min和max,我們到原始碼看看他們的描述:

Flutter學習篇(四)—— 尺寸解惑
注意紅色部分,如果Column的child的constraint是unbounded的話,就無法給出真實大小。而對於ListView,垂直高度為double.infinity,即無限制,所以ListView的constraint是unbounded的。那麼有什麼辦法可以解決呢,我們到ListView的原始碼去一探究竟吧。

在ListView的原始碼搜尋unbound關鍵字,很快發現有這麼一個屬性shrinkWrap

Flutter學習篇(四)—— 尺寸解惑
其中有一句很關鍵的註釋, If the scroll view has unbounded constraints in the [scrollDirection], then [shrinkWrap] must be true. 。如果在滾動方向上,約束沒有限制的話,那麼shrinkWrap應該設定為true, 回頭看看,ListView的外層是Column,同樣在垂直高度也為unbounded,所以Column的constraint也為unbounded,所以我們按照提示把ListView的shrinkWrap設定為true。結果不言而喻,自然是顯示正常。
那為什麼把屬性設定為true就可以呢,繼續啃原始碼,可以注意到有這麼一段程式碼:

    if (shrinkWrap) {
      return ShrinkWrappingViewport(
        axisDirection: axisDirection,
        offset: offset,
        slivers: slivers,
      );
    }
    return Viewport(
      axisDirection: axisDirection,
      offset: offset,
      slivers: slivers,
      cacheExtent: cacheExtent,
      center: center,
      anchor: anchor,
    );
複製程式碼

原來是根據shrinkWrap來選擇不同的ViewPort。

  • shrikWrap為true的情況:
    進入ViewPort之後,發現真實的渲染物件為RenderViewPort,so,繼續前進。
  @override
  RenderViewport createRenderObject(BuildContext context) {
    return (
      axisDirection: axisDirection,
      crossAxisDirection: crossAxisDirection ?? Viewport.getDefaultCrossAxisDirection(context, axisDirection),
      anchor: anchor,
      offset: offset,
      cacheExtent: cacheExtent,
    );
  }
複製程式碼

因為是在測量階段,所以我們先找到performResize,如下:

Flutter學習篇(四)—— 尺寸解惑
猜猜我們看到了什麼,前言提到的報錯 Vertical viewport was given unbounded height.。接著再看判斷條件的程式碼:

  bool get hasBoundedHeight => maxHeight < double.infinity;
複製程式碼

由於ListView的maxHeight為double.infinity,所以自然返回了false,所以才會導致上述的報錯。

  • shrikWrap為true的情況:
    可以看到使用了RenderShrinkWrappingViewport這個類,這個類的官方介紹部分如下:
A render object that is bigger on the inside and shrink wraps its children in the main axis.

複製程式碼

收縮主軸上的子檢視,看到這應該就可以大概理解了,ListView原來的主軸即垂直方向是unbounded的,而RenderShrinkWrappingViewport通過把主軸進行收縮,這樣一來就可以使得主軸方向是確定的,從而解決問題。

當然除了這種方式,我們還可以直接通過在ListView巢狀一層確定高度的佈局來解決這個問題:

Column(
      mainAxisSize: MainAxisSize.min,
    children: <Widget>[
      Container(
      height: 100.0, child: ListView(
            shrinkWrap: false,
            children: <Widget>[
              Container(color:Colors.red, child:Text("1")),
              Container(color:Colors.orange, child:Text("2")),
            ],
          )
        )]
      ))
複製程式碼

所以本質上,解決問題的關鍵就在於:確定ListView的高度, shrinkWrap也好,container也好,都是為了給出ListView的具體高度。

其實上面分析了那麼多,牽涉到Flutter的一個基本概念,那就是Constraint,意為約束。 Flutter的約束是從父節點傳到子節點,子節點根據約束重新調整自身的大小。舉個最簡單例子 :

   return Scaffold(
        appBar: AppBar(title: Text("佈局測試")),
        backgroundColor: Colors.green,
        body: Container(
            color: Colors.amber,
            child: Column(
              mainAxisSize: MainAxisSize.max,
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                Container(
                  color: Colors.blue,
                  height: 300.0,
                  child: Row(mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: <Widget>[
                    Container(color: Colors.teal, width: 100, height: 100),
                    Container(color: Colors.purple, width: 100, height: 100),
                  ]),
                ),

                Container(
                  height: 300,
                  width: 200,
                  color: Colors.lime,
                  child: Column(
                    children: <Widget>[
                      Container(color: Colors.purple, width: 100, height: 100),
                      Container(color: Colors.teal, width: 100, height: 100),
                    ],
                  ),
                )
              ],
            )));
  }
複製程式碼

效果如下:

Flutter學習篇(四)—— 尺寸解惑

對於最外層的Column而言,它的父節點約束是螢幕,加上它的垂直方向是max,可以看到最外層的Column高度是整個螢幕高度(橙色部分),而裡面的Column的約束則是height為300的Container,所以它的高度是300(黃色部分)。 而對於Row,它的約束條件也是高度300,雖然在水平方向是無限制,但是由於水平方向用了min,所以Row的寬度跟隨了子節點的總寬度,即200。如果水平方向用了max,那麼Row的寬度則為螢幕寬度。

總結

出了問題,善從原始碼找出為什麼??。

參考

Flutter盒約束
Flutter 約束知識

倉庫

點選flutter_demo,檢視完整程式碼。

相關文章