1. Flutter UI架構

Flutter將視圖數據抽象成為三個部分,即Widget樹、Element樹和RenderObject樹。
- Widget樹:控件的配置信息,不涉及渲染,更新代價極低。
- RenderObject樹:真正的UI渲染樹,負責渲染UI,更新代價極大。
- Element樹:Widget樹和RenderObject樹之間的粘合劑,負責將Widget樹的變更以最低的代價映射到RenderObject樹上。
2. Flutte布局原理
1. 源碼分析
RenderObject是Flutter真正的UI渲染樹,負責界面的測量,布局和繪制,Flutter頁面布局相關的源碼都在RenderObject及其子類中。

上圖是RenderObject類中和布局相關的屬性和方法.
- constrainst屬性由直接父布局提供的布局約束(最大高度,最小高度,最大寬度,最小寬度),
- layout方法由父RenderObject調用,計算當前RenderObject的布局,父RenderObject傳遞給子RenderObject約束信息,子RenderObject必須服從約束信息。如果父RenderObject需要子RenderObject的布局信息,parentUsesSize參數應該傳true,否則傳false。
- performLayout:子類必須重寫的方法,計算當前RenderObject和子RenderObject的布局信息。
- performResize:子類必須重寫的方法,僅使用約束更新RenderObject的布局。RenderConstrainedBox是ConstrainedBox對應的RenderObject,最多有一個子RenderObject, 如果子RenderoObject不為空,則對子RenderObject進行布局,并計算布局的大小。RenderFlex是Column和Row對應的RenderObject,可以包含多個子RenderObject, 如果子RenderoObject不為空,則對子RenderObject依次進行布局,并計算布局的大小。
2. Flutte布局原理總結
- RenderObject會通過它的父級獲得自身的約束。 約束實際上就是4個浮點類型的集合:最大/最小寬度,以及最大/最小高度。
- 然后,這個RenderObject將會逐個遍歷它的children列表。向子級傳遞約束子級之間的約束可能會有所不同),然后詢問它的每一個子級需要用于布局的大小。
- 然后,這個RenderObject就會對它子級的children逐個進行布局。(水平方向是x軸,豎直是y軸)
- 最后,RenderObject將會把它的大小信息向上傳遞至父RenderObject(包括其原始約束條件)。
3. Flutter布局限制
由于父RenderObject傳遞給子RenderObject約束信息,子RenderObject傳遞給父RenderObject大小信息,Flutter布局存在一些限制:
- 一個RenderObject僅在其父級給其約束的情況下才能決定自身的大小。 這意味著RenderObject通常情況下不能任意獲得其想要的大小。
- 一個RenderObject無法知道,也不需要決定其在屏幕中的位置。 因為它的位置是由其父級決定的。
- 當輪到父級決定其大小和位置的時候,同樣的也取決于它自身的父級,所以,在不考慮整棵樹的情況下,幾乎不可能精確定義任何RenderObject的大小和位置。
4. Flutter布局實戰
1.線性布局
線性布局的子布局在主軸上長度不能設置無限大(double.infinity),因為線性布局給子布局在主軸上的最大長度的約束就是無限大(double.infinity),會導致無限大的子布局無法計算當前布局的大小,布局失敗。如下:
// 第一個子Container的高度設置為無限大,導致布局失敗Column( children: [ Container( width: 200.0, height: double.infinity, color: Colors.blue, ), Container( width: 200.0, height: 200.0, color: Colors.red, ), ],);// 子Column的高度為無限大,導致布局失敗Column( children: [ Column( children: [ Container( width: 200.0, height: 200.0, color: Colors.blue, ), ], ), Container( width: 200.0, height: 200.0, color: Colors.red, ), ],);
如果想要讓子布局占滿全屏可以增加Expanded布局。如下:
Column( children: [ Expanded( child: Container( width: 200.0, height: double.infinity, color: Colors.blue, ), ), Container( width: 200.0, height: 200.0, color: Colors.red, ), ],);Column( children: [ Expanded( child: Column( children: [ Container( width: 200.0, height: 200.0, color: Colors.blue, ), ], ), ), Container( width: 200.0, height: 200.0, color: Colors.red, ), ],);
2. 棧布局
棧布局的子布局設置相對位置推薦使用Padding代替Position,并把fit屬性設置StackFit.loose,構建時Stack會自適應大小,否則需要使用Container設置Stack和Position的大小;并且Padding默認對上下左右都有相對距離(0),但Position沒有默認的相對布局,如果忘記設置,會使Position的子布局無法自動換行或布局不顯示。
不推薦:
// Text無法自動換行// Container布局不顯示Stack( children: [ Positioned( top: 100.0, child: Text( '測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試'), ), Positioned( top: 0.0, child: Column( children: [ Container( height: 100.0, width: double.infinity, color: Colors.red, ), ], ), ) ],);
推薦:
Stack( children: [ Padding( padding: const EdgeInsets.only(top: 10.0), child: Text( '測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試測試'), ), Padding( padding: const EdgeInsets.only(top: 0.0), child: Column( children: [ Container( height: 100.0, width: double.infinity, color: Colors.red, ), ], ), ) ],);
3. 可滾動布局
可滾動布局在主軸方向上的長度是無限大,父布局需要轉遞給可滾動布局最大長度的約束。
Column( children: [ Expanded( child: ListView( children: [ Text('測試'), ], ), ), ],);
橫向可滾動布局必須設置高度。
Column( children: [ Container( height: 200.0, child: ListView( scrollDirection: Axis.horizontal, children: [ Text('測試'), ], ), ), ],);
可滾動布局嵌套可滾動布局被嵌套布局的shrinkWrap屬性必須設置為true,并且可以將physics屬性設置為NeverScrollableScrollPhysics()來解決滑動沖突。
ListView( children: [ ListView( shrinkWrap: true, physics: NeverScrollableScrollPhysics(), children: [ Text('測試'), ], ), ],);
5. 總結
本文章通過源碼分析講述Flutter布局的過程,分析實戰中布局不顯示的原因,并給出Flutter布局的一些建議。