一,概述
Flutter
中擁有30多種預定義的布局widget
,常用的有Container
、Padding
、Center
、Flex
、Row
、Colum
、ListView
、GridView
。按照《Flutter技術入門與實戰》上面來說的話,大概分為四類
- 基礎布局組件:Container(容器布局),Center(居中布局),Padding(填充布局),Align(對齊布局),Colum(垂直布局),Row(水平布局),Expanded(配合Colum,Row使用),FittedBox(縮放布局),Stack(堆疊布局),overflowBox(溢出父視圖容器)。
- 寬高尺寸處理:SizedBox(設置具體尺寸),ConstrainedBox(限定最大最小寬高布局),LimitedBox(限定最大寬高布局),AspectRatio(調整寬高比),FractionallySizedBox(百分比布局)
- 列表和表格處理:ListView(列表),GridView(網格),Table(表格)
- 其它布局處理:Transform(矩陣轉換),Baseline(基准線布局),Offstage(控制是否顯示組件),Wrap(按寬高自動換行布局)
二,列表和表格處理布局組件
- ListView(列表)
- 介紹
ListView是一個非常常用的控件,涉及到數據列表展示的,一般情況下都會選用該控件。ListView跟GridView相似,基本上是一個slivers里面只包含一個SliverList的CustomScrollView。 - 布局行為
ListView在主軸方向可以滾動,在交叉軸方向,則是填滿ListView。 - 繼承關系
Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > ListView
看繼承關系可知,這是一個組合控件。ListView跟GridView類似,都是繼承自BoxScrollView。
- 構造函數
ListView({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, this.itemExtent, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, double cacheExtent, List<Widget> children = const <Widget>[], })
同時也提供了如下額外的三種構造方法,方便開發者使用。
ListView.builder ListView.separated ListView.custom
-
使用場景
ListView使用場景太多了,一般涉及到列表展示的,一般都會選擇ListView。
但是需要注意一點,ListView的標准構造函數適用於數目比較少的場景,如果
數目比較多
的話,最好使用ListView.builder
。ListView的標准構造函數會將所有item一次性創建,而ListView.builder會創建滾動到屏幕上顯示的item。
- 參數解析
ListView大部分屬性同GridView,想了解的讀者可以看一下下面所寫的GridView相關的內容。這里只介紹一個屬性
itemExtent:ListView在滾動方向上每個item所占的高度值。
- 介紹
- GridView(網格)
- 介紹
GridView在移動端上非常的常見,就是一個滾動的多列列表,實際的使用場景也非常的多。
- 布局行為
GridView的布局行為不復雜,本身是盡量占滿空間區域,布局行為上完全繼承自ScrollView。
- 繼承關系
Object > Diagnosticable > DiagnosticableTree > Widget > StatelessWidget > ScrollView > BoxScrollView > GridView
- 構造函數
GridView({ Key key, Axis scrollDirection = Axis.vertical, bool reverse = false, ScrollController controller, bool primary, ScrollPhysics physics, bool shrinkWrap = false, EdgeInsetsGeometry padding, @required this.gridDelegate, bool addAutomaticKeepAlives = true, bool addRepaintBoundaries = true, double cacheExtent, List<Widget> children = const <Widget>[], })
同時也提供了如下額外的四種構造方法,方便開發者使用。
GridView.builder GridView.custom GridView.count GridView.extent
- 參數解析
scrollDirection:滾動的方向,有垂直和水平兩種,默認為垂直方向(Axis.vertical)。
reverse:默認是從上或者左向下或者右滾動的,這個屬性控制是否反向,默認值為false,不反向滾動。
controller:控制child滾動時候的位置。
primary:是否是與父節點的PrimaryScrollController所關聯的主滾動視圖。
physics:滾動的視圖如何響應用戶的輸入。
shrinkWrap:滾動方向的滾動視圖內容是否應該由正在查看的內容所決定。
padding:四周的空白區域。
gridDelegate:控制GridView中子節點布局的delegate。
cacheExtent:緩存區域。
- 介紹
- Table(表格)
- 介紹
每一種移動端布局中都會有一種table布局,這種控件太常見了。至於其表現形式,完全可以借鑒其他移動端的,通俗點講,就是表格。
- 布局行為
表格的每一行的高度,由其內容決定,每一列的寬度,則由columnWidths屬性單獨控制。
- 繼承關系
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > Table
- 構造函數
Table({ Key key, this.children = const <TableRow>[], this.columnWidths, this.defaultColumnWidth = const FlexColumnWidth(1.0), this.textDirection, this.border, this.defaultVerticalAlignment = TableCellVerticalAlignment.top, this.textBaseline, })
- 參數解析
columnWidths:設置每一列的寬度。
defaultColumnWidth:默認的每一列寬度值,默認情況下均分。
textDirection:文字方向,一般無需考慮。
border:表格邊框。
defaultVerticalAlignment:每一個cell的垂直方向的alignment。
總共包含5種:
- top:被放置在的頂部;
- middle:垂直居中;
- bottom:放置在底部;
- baseline:文本baseline對齊;
- fill:充滿整個cell。
textBaseline:defaultVerticalAlignment為baseline的時候,會用到這個屬性。
- 介紹
三,常用示例
- ListView(列表)
/** * ListView * 第一個展示四行文字 */ class MyListView extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return new ListView( shrinkWrap: true, padding: EdgeInsets.all(20.0), children: <Widget>[ new Text('I\m dedicationg every day to you'), new Text('Domestic life was never quite my style'), new Text('When you smile, you knock me out, I fall apart'), new Text('And I thought I was so smart') ], ); } }
效果圖:
源碼解析:@override Widget buildChildLayout(BuildContext context) { if (itemExtent != null) { return new SliverFixedExtentList( delegate: childrenDelegate, itemExtent: itemExtent, ); } return new SliverList(delegate: childrenDelegate); }
ListView標准構造布局代碼如上所示,底層是用到的SliverList去實現的。ListView是一個slivers里面只包含一個SliverList的CustomScrollView。源碼這塊兒可以參考GridView,在此不做更多的說明。
- GridView(網格)
/** * GridView * 代碼直接用了Creating a Grid List中的例子,創建了一個2列總共100個子節點的列表。 */ class MyGridView extends StatelessWidget { @override Widget build(BuildContext context) { // TODO: implement build return new GridView.count( crossAxisCount: 2, children: List.generate(100, (index){ return new Center( child: new Text( 'Item $index', style: Theme.of(context).textTheme.headline, ), ); }, ), ); } }
效果圖
源碼解析:
@override Widget build(BuildContext context) { final List<Widget> slivers = buildSlivers(context); final AxisDirection axisDirection = getDirection(context); final ScrollController scrollController = primary ? PrimaryScrollController.of(context) : controller; final Scrollable scrollable = new Scrollable( axisDirection: axisDirection, controller: scrollController, physics: physics, viewportBuilder: (BuildContext context, ViewportOffset offset) { return buildViewport(context, offset, axisDirection, slivers); }, ); return primary && scrollController != null ? new PrimaryScrollController.none(child: scrollable) : scrollable; }
上面這段代碼是ScrollView的build方法,GridView就是一個特殊的ScrollView。GridView本身代碼沒有什么,基本上都是ScrollView上的東西,主要會涉及到Scrollable、Sliver、Viewport等內容,這些內容比較多,因此源碼就先略了,后面單獨出一篇文章對ScrollView進行分析吧。
- Table(表格)
Table( columnWidths: const <int, TableColumnWidth>{ 0: FixedColumnWidth(50.0), 1: FixedColumnWidth(100.0), 2: FixedColumnWidth(50.0), 3: FixedColumnWidth(100.0), }, border: TableBorder.all(color: Colors.red, width: 1.0, style: BorderStyle.solid), children: const <TableRow>[ TableRow( children: <Widget>[ Text('A1'), Text('B1'), Text('C1'), Text('D1'), ], ), TableRow( children: <Widget>[ Text('A2'), Text('B2'), Text('C2'), Text('D2'), ], ), TableRow( children: <Widget>[ Text('A3'), Text('B3'), Text('C3'), Text('D3'), ], ), ], )
效果圖:
(1)樣例其實並不復雜,FlowDelegate需要自己實現child的繪制,其實大多數時候就是位置的擺放。上面例子中,對每個child按照給定的margin值,進行排列,如果超出一行,則在下一行進行布局。
(2)另外,對這個例子多做一個說明,對於上述child寬度的變化,這個例子是沒問題的,如果每個child的高度不同,則需要對代碼進行調整,具體的調整是換行的時候,需要根據上一行的最大高度來確定下一行的起始y坐標。
源碼解析:我們直接來看其布局源碼:
第一步,當行或者列為0的時候,將自身尺寸設為0x0。
if (rows * columns == 0) { size = constraints.constrain(const Size(0.0, 0.0)); return; }
第二步,根據textDirection值,設置方向,一般在阿拉伯語系中,一些文本都是從右往左現實的,平時使用時,不需要去考慮這個屬性。
switch (textDirection) { case TextDirection.rtl: positions[columns - 1] = 0.0; for (int x = columns - 2; x >= 0; x -= 1) positions[x] = positions[x+1] + widths[x+1]; _columnLefts = positions.reversed; tableWidth = positions.first + widths.first; break; case TextDirection.ltr: positions[0] = 0.0; for (int x = 1; x < columns; x += 1) positions[x] = positions[x-1] + widths[x-1]; _columnLefts = positions; tableWidth = positions.last + widths.last; break; }
第三步,設置每一個cell的尺寸。
for (int x = 0; x < columns; x += 1) { final int xy = x + y * columns; final RenderBox child = _children[xy]; if (child != null) { final TableCellParentData childParentData = child.parentData; childParentData.x = x; childParentData.y = y; switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { case TableCellVerticalAlignment.baseline: child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); final double childBaseline = child.getDistanceToBaseline(textBaseline, onlyReal: true); if (childBaseline != null) { beforeBaselineDistance = math.max(beforeBaselineDistance, childBaseline); afterBaselineDistance = math.max(afterBaselineDistance, child.size.height - childBaseline); baselines[x] = childBaseline; haveBaseline = true; } else { rowHeight = math.max(rowHeight, child.size.height); childParentData.offset = new Offset(positions[x], rowTop); } break; case TableCellVerticalAlignment.top: case TableCellVerticalAlignment.middle: case TableCellVerticalAlignment.bottom: child.layout(new BoxConstraints.tightFor(width: widths[x]), parentUsesSize: true); rowHeight = math.max(rowHeight, child.size.height); break; case TableCellVerticalAlignment.fill: break; } } }
第四步,如果有baseline則進行相關設置。
if (haveBaseline) { if (y == 0) _baselineDistance = beforeBaselineDistance; rowHeight = math.max(rowHeight, beforeBaselineDistance + afterBaselineDistance); }
第五步,根據alignment,調整child的位置。
for (int x = 0; x < columns; x += 1) { final int xy = x + y * columns; final RenderBox child = _children[xy]; if (child != null) { final TableCellParentData childParentData = child.parentData; switch (childParentData.verticalAlignment ?? defaultVerticalAlignment) { case TableCellVerticalAlignment.baseline: if (baselines[x] != null) childParentData.offset = new Offset(positions[x], rowTop + beforeBaselineDistance - baselines[x]); break; case TableCellVerticalAlignment.top: childParentData.offset = new Offset(positions[x], rowTop); break; case TableCellVerticalAlignment.middle: childParentData.offset = new Offset(positions[x], rowTop + (rowHeight - child.size.height) / 2.0); break; case TableCellVerticalAlignment.bottom: childParentData.offset = new Offset(positions[x], rowTop + rowHeight - child.size.height); break; case TableCellVerticalAlignment.fill: child.layout(new BoxConstraints.tightFor(width: widths[x], height: rowHeight)); childParentData.offset = new Offset(positions[x], rowTop); break; } } }
最后一步,則是根據每一行的寬度以及每一列的高度,設置Table的尺寸。
size = constraints.constrain(new Size(tableWidth, rowTop));
最后梳理一下整個的布局流程:
當行或者列為0的時候,將自身尺寸設為0x0; 根據textDirection進行相關設置; 設置cell的尺寸; 如果設置了baseline,則進行相關設置; 根據alignment設置cell垂直方向的位置; 設置Table的尺寸。 如果經常關注系列文章的讀者,可能會發現,布局控件的布局流程基本上跟上述流程是相似的。
四,參考