在Flutter中,號稱一切皆widget,手勢是Widget,動畫是Widget,UI更是Widget,今天我們就來說說Widgets裡比較特殊的一個,Container。
1. 參考文獻
Container初用起來很簡單,但是裡面的邏輯又有些複雜,我也不敢說完全吃透,所以本文初期版本還是以總結網上各種文章為主,再加上自己的理解,如果有不對的地方,請一定指出
Dealing with box constraints in Flutter
Flutter — Container Cheat Sheet
What is a Container in Flutter?
Container Widget with example Flutter Tutorial
Understanding Flutter Layout (Box)Constraints
2. 介紹
在flutter中,所有的功能都被分散成單一功能的widget,比如居中有Center,邊框有Padding,文字是Text,手勢是GestureDetector,他們各自維護一個功能,但是我們商業App的UI都很精美,如果要實現一個很好的佈局,需要巢狀非常多的佈局Widget,所以Container應運而生:
A convenience widget that combines common painting, positioning, and sizing widgets.
官方文件一語道破了container複雜的原因,它是一個便利部件,融合了繪圖、定位和大小部件,據我所知,可以設定大小,背景顏色,邊框,圓角,陰影,漸變,而且大小可以有很多種情況,時而依賴於父部件,時而依賴於子部件,時而依賴於自己,所以我們稍後會重點說一說Container的佈局(尺寸規則,寬高)。
3. Widget渲染流程
flutter是樹狀渲染結構,首先從根結點開始渲染,從上到下傳遞約束,直到最終的葉子節點(沒有子節點了),然後葉子節點根據約束確定自身大小,然後將大小返回給上級結點,然後上一級根據葉子節點的尺寸,決定自己的大小,再返回上一級,最終根節點確定了大小。之後,根結點逐級往下襬放子節點的位置(根據子節點及子節點兄弟節點的大小和偏移量)。4. Container渲染流程
根據官網的介紹,Container會首先使用設定的padding來圍繞子部件,然後對padding的大小新增額外的約束(如果非空),然後容器被外部的空白區域(margin)包圍。在繪製過程中,Container首先應用變換(transform),然後繪製裝飾(decoration)來填充區域,接著繪製子部件,最後繪製前景裝飾(foregroundDecoration),同時填充該區域。
decoration和foregroundDecoration是填充配置,前者是在子部件之下,後者是子部件之上,可以設定填充顏色,邊框,填充形狀,陰影,漸變色,背景圖片等。
如果Container的約束是有限制的,那麼沒有子部件的Container會嘗試儘可能大,如果Container的約束是沒有限制的(unbounded),它就會盡可能小。
有子部件的Container,根據子部件確定自己的大小。
5. 有無限制約束
到底什麼是有限制約束和無限制約束呢,各種部件又都是哪種約束呢?
在flutter中,widgets由底層的RenderBox渲染盒渲染,父元件向渲染盒提供約束條件,然後渲染盒用這些約束調整自己的尺寸,約束(Constraint)由最大和最小的寬高組成,尺寸(Size)由特定的寬高組成。
通常來說,有三種處理約束的盒子:
- 儘可能大:Center和ListView等
- 和子部件一樣大:Transform和Opacity等
- 特定大小:Image和Text等
- 特殊情況:Row和Column由其給定的約束決定,Container由其建構函式的引數決定
但約束有時會變得緊湊,意思是它沒有留給渲染盒子自行決定尺寸的餘地(例如最大寬度和最小寬度相等,那允許的寬度是個固定值),例如App這個Widget,它的約束被設定為固定的應用程式內容大小(也就是整個螢幕)。而在Flutter中,很多的widget,尤其是隻能有一個子部件的widget,會傳遞自己的約束到子部件。也就是說,如果你在App根渲染樹裡巢狀了一系列widget,他們將會因為緊湊的約束,一級級地貼著。
但是有些widget會使約束寬鬆,意思是最大的約束保留,但是最小的約束移除了,比如Center。
5.1 無限制約束
- 在某些情況下,賦給widget的約束是無限制(unbounded)的,或者說是無限(infinite)的。也就是說,最大寬度和最大高度,都是double.INFINITY。
- 一個嘗試儘可能大的widget,遇到無限制約束的時候,是不會起作用的,因為它不知道到底該有多大,在debug模式下,就會丟擲異常。
- 最常見的擁有無限制約束的情況,就是嵌入在彈性盒子裡面,比如Row,Column或者可以滾動的區域(ListView或者其他ScrollView子類)。
需要指出的是,ListView會在其交叉方向擴張到父部件邊界,例如一個縱向滾動的列表,在橫向會盡量和父部件一樣寬。 當你在橫向滾動列表裡,嵌入一個縱向滾動列表的時候,縱向列表會盡可能寬,也就是無限寬,因為橫向列表是無盡寬的,這就會異常。
另外,彈性盒子(Row和Column)在有限制和無限制約束的情況下,表現出來的行為也不同。
- 在有限制的約束時,他們會在其方向上儘可能大。
- 在無限制約束時,他們會在其方向上適應他們子元件(包住子元件)。這種情況下,你不能在彈性盒子裡用Expanded,因為這是將無法確定部件大小。
6. Container佈局(尺寸規則)
因為Container集合了其他部件的功能,所以它的佈局有些複雜,簡而言之,按照順序,Container會:
- 遵循對齊規則
- 為子部件調整自身大小
- 遵循寬高和約束
- 然後Container嘗試儘可能小。
6.1 來看看官方文件的解釋(這個解釋看不懂就算了,有點囉嗦):
- 如果Container沒有子部件,沒有寬高,沒有約束,並且父部件提供了無限制約束(unbounded constraints),Container會盡可能小。
- 如果Container沒有子部件,沒有對齊規則,但是提供了高度、寬度或者約束,那麼Container會在遵循寬、高、約束和父部件約束的情況下,儘可能小。
- 如果Container沒有子部件,沒有寬高,沒有約束,沒有對齊,但是父部件提供了有限制約束,那麼Container會擴張以適應(fit)父部件約束
- 如果Container有一個對齊規則,並且父部件提供了無限制約束,那麼Container會嘗試調整自己來包圍子部件
- 如果Container有一個對齊規則,而且父部件提供了有限制約束,那麼Container會嘗試擴張以適應(fit)父部件,然後根據對齊方式,將子部件置於其內
- 另外,Container有子部件,但是Container沒有寬高、約束、對齊規則,那麼Container會傳遞父部件的約束到其子部件,然後調整自身來匹配子部件。
- margin和padding也會影響佈局,decoration會隱性增加padding(比如設定border)。
- 預設是儘可能大
6.2 我們來總結一下:
maxWidth | maxHeight | 約束 | 有無子元件 | 佈局規則 |
---|---|---|---|---|
有值 | 有值 | 有限制 | 無 | 儘可能大 |
有值 | 無值 | 高度無限制,寬度有限制 | 無 | 高度儘可能小,寬度儘可能大 |
無值 | 有值 | 高度有限制,寬度無限制 | 無 | 高度儘可能大,寬度儘可能小 |
無值 | 無值 | 無限制 | 無 | 儘可能小 |
都行 | 都行 | 都行 | 有 | 在滿足約束的前提下儘可能小 |
6.3 例子
沒有子元件,有約束,儘可能大↓
沒有子元件,父元件約束最大寬度是螢幕寬度,最大高度是無限,自己的約束最小寬度是100,最小高度是100,則高度儘可能小到100,寬度儘可能大到螢幕寬度↓
父元件最大約束是螢幕寬高,自己是固定寬度100,則寬度是100,高度儘可能大到螢幕高度↓
自己沒有設定約束,有子元件,所以自己包住子元件↓
自己設定固定高度100,寬度沒有約束,有子元件,則高度為100,寬度包住子元件↓
有子元件,自己的高度是固定100,寬度設定為無限,則高度為100,寬度是父元件的約束螢幕寬度↓
有子元件,設定最小寬高為無限,則大小為螢幕大小↓7. 總結
Container應該是flutter中最靈活的佈局widget,大家一定要善用Container,用巧妙的方式處理佈局,否則可能會讓程式碼可讀性變差,難以維護