本文主要介紹Flutter佈局中的Flow、Table、Wrap控件,詳細介紹了其佈局行爲以及使用場景,並對源碼進行了分析。html
A widget that implements the flow layout algorithm.前端
Flow按照解釋的那樣,是一個實現流式佈局算法的控件。流式佈局在大前端是很常見的佈局方式,可是通常使用Flow不多,由於其過於複雜,不少場景下都會去使用Wrap。git
Flow官方介紹是一個對child尺寸以及位置調整很是高效的控件,主要是得益於其FlowDelegate。另外Flow在用轉換矩陣(transformation matrices)對child進行位置調整的時候進行了優化。github
Flow以及其child的一些約束都會受到FlowDelegate的控制,例如重寫FlowDelegate中的geiSize,能夠設置Flow的尺寸,重寫其getConstraintsForChild方法,能夠設置每一個child的佈局約束條件。算法
Flow之因此高效,是由於其在定位事後,若是使用FlowDelegate中的paintChildren改變child的尺寸或者位置,只是重繪,並無實際調整其位置。dom
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Flow
const width = 80.0; const height = 60.0; Flow( delegate: TestFlowDelegate(margin: EdgeInsets.fromLTRB(10.0, 10.0, 10.0, 10.0)), children: <Widget>[ new Container(width: width, height: height, color: Colors.yellow,), new Container(width: width, height: height, color: Colors.green,), new Container(width: width, height: height, color: Colors.red,), new Container(width: width, height: height, color: Colors.black,), new Container(width: width, height: height, color: Colors.blue,), new Container(width: width, height: height, color: Colors.lightGreenAccent,), ], ) class TestFlowDelegate extends FlowDelegate { EdgeInsets margin = EdgeInsets.zero; TestFlowDelegate({this.margin}); @override void paintChildren(FlowPaintingContext context) { var x = margin.left; var y = margin.top; for (int i = 0; i < context.childCount; i++) { var w = context.getChildSize(i).width + x + margin.right; if (w < context.size.width) { context.paintChild(i, transform: new Matrix4.translationValues( x, y, 0.0)); x = w + margin.left; } else { x = margin.left; y += context.getChildSize(i).height + margin.top + margin.bottom; context.paintChild(i, transform: new Matrix4.translationValues( x, y, 0.0)); x += context.getChildSize(i).width + margin.left + margin.right; } } } @override bool shouldRepaint(FlowDelegate oldDelegate) { return oldDelegate != this; } }
樣例其實並不複雜,FlowDelegate須要本身實現child的繪製,其實大多數時候就是位置的擺放。上面例子中,對每一個child按照給定的margin值,進行排列,若是超出一行,則在下一行進行佈局。ide
另外,對這個例子多作一個說明,對於上述child寬度的變化,這個例子是沒問題的,若是每一個child的高度不一樣,則須要對代碼進行調整,具體的調整是換行的時候,須要根據上一行的最大高度來肯定下一行的起始y座標。函數
構造函數以下:佈局
Flow({ Key key, @required this.delegate, List<Widget> children = const <Widget>[], })
delegate:影響Flow具體佈局的FlowDelegate。學習
其中FlowDelegate包含以下幾個方法:
其中,咱們平時使用的時候,通常會使用到paintChildren以及shouldRepaint兩個方法。
咱們先來看一下Flow的佈局代碼
Size _getSize(BoxConstraints constraints) { assert(constraints.debugAssertIsValid()); return constraints.constrain(_delegate.getSize(constraints)); } @override void performLayout() { size = _getSize(constraints); int i = 0; _randomAccessChildren.clear(); RenderBox child = firstChild; while (child != null) { _randomAccessChildren.add(child); final BoxConstraints innerConstraints = _delegate.getConstraintsForChild(i, constraints); child.layout(innerConstraints, parentUsesSize: true); final FlowParentData childParentData = child.parentData; childParentData.offset = Offset.zero; child = childParentData.nextSibling; i += 1; } }
能夠看到Flow尺寸的取值,直接來自於delegate的getSize方法。對於每個child,則是將delegate中的getConstraintsForChild設置的約束條件,設置在child上。
Flow佈局上的表現,受Delegate中getSize以及getConstraintsForChild兩個方法的影響。第一個方法設置其尺寸,第二個方法設置其children的佈局約束條件。
接下來咱們來看一下其繪製方法。
void _paintWithDelegate(PaintingContext context, Offset offset) { _lastPaintOrder.clear(); _paintingContext = context; _paintingOffset = offset; for (RenderBox child in _randomAccessChildren) { final FlowParentData childParentData = child.parentData; childParentData._transform = null; } try { _delegate.paintChildren(this); } finally { _paintingContext = null; _paintingOffset = null; } }
它的繪製方法很是的簡單,先將上次設置的參數都初始化,而後調用delegate中的paintChildren進行繪製。在paintChildren中會調用paintChild方法去繪製每一個child,咱們接下來看下其代碼。
@override void paintChild(int i, { Matrix4 transform, double opacity = 1.0 }) { transform ??= new Matrix4.identity(); final RenderBox child = _randomAccessChildren[i]; final FlowParentData childParentData = child.parentData; _lastPaintOrder.add(i); childParentData._transform = transform; if (opacity == 0.0) return; void painter(PaintingContext context, Offset offset) { context.paintChild(child, offset); } if (opacity == 1.0) { _paintingContext.pushTransform(needsCompositing, _paintingOffset, transform, painter); } else { _paintingContext.pushOpacity(_paintingOffset, _getAlphaFromOpacity(opacity), (PaintingContext context, Offset offset) { context.pushTransform(needsCompositing, offset, transform, painter); }); } }
paitChild函數首先會將transform值設在child上,而後根據opacity值,決定其繪製的表現。
至於其爲何高效,主要是由於它的佈局函數不牽涉到child的佈局,而在繪製的時候,則根據delegate中的策略,進行有效的繪製。
Flow在一些定製化的流式佈局中,有可用場景,可是通常寫起來比較複雜,但勝在靈活性以及其高效。
A widget that uses the table layout algorithm for its children.
每一種移動端佈局中都會有一種table佈局,這種控件太常見了。至於其表現形式,徹底能夠借鑑其餘移動端的,通俗點講,就是表格。
表格的每一行的高度,由其內容決定,每一列的寬度,則由columnWidths屬性單獨控制。
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > 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'), ], ), ], )
一個三行四列的表格,第一三行寬度爲50,第二四行寬度爲100。
構造函數以下:
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種:
textBaseline:defaultVerticalAlignment爲baseline的時候,會用到這個屬性。
咱們直接來看其佈局源碼:
第一步,當行或者列爲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));
最後梳理一下整個的佈局流程:
若是常常關注系列文章的讀者,可能會發現,佈局控件的佈局流程基本上跟上述流程是類似的。
在一些須要表格展現的場景中,可使用Table控件。
A widget that displays its children in multiple horizontal or vertical runs.
看簡介,其實Wrap實現的效果,Flow能夠很輕鬆,並且能夠更加靈活的實現出來。
Flow能夠很輕易的實現Wrap的效果,可是Wrap更多的是在使用了Flex中的一些概念,某種意義上說是跟Row、Column更加類似的。
單行的Wrap跟Row表現幾乎一致,單列的Wrap則跟Row表現幾乎一致。但Row與Column都是單行單列的,Wrap則突破了這個限制,mainAxis上空間不足時,則向crossAxis上去擴展顯示。
從效率上講,Flow確定會比Wrap高,可是Wrap使用起來會方便一些。
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > MultiChildRenderObjectWidget > Wrap
從繼承關係上看,Wrap與Flow都是繼承自MultiChildRenderObjectWidget,Flow能夠實現Wrap的效果,可是二者倒是單獨實現的,說明二者有很大的不一樣。
Wrap( spacing: 8.0, // gap between adjacent chips runSpacing: 4.0, // gap between lines children: <Widget>[ Chip( avatar: CircleAvatar( backgroundColor: Colors.blue.shade900, child: new Text('AH', style: TextStyle(fontSize: 10.0),)), label: Text('Hamilton'), ), Chip( avatar: CircleAvatar( backgroundColor: Colors.blue.shade900, child: new Text('ML', style: TextStyle(fontSize: 10.0),)), label: Text('Lafayette'), ), Chip( avatar: CircleAvatar( backgroundColor: Colors.blue.shade900, child: new Text('HM', style: TextStyle(fontSize: 10.0),)), label: Text('Mulligan'), ), Chip( avatar: CircleAvatar( backgroundColor: Colors.blue.shade900, child: new Text('JL', style: TextStyle(fontSize: 10.0),)), label: Text('Laurens'), ), ], )
示例代碼直接使用的官方文檔上的,效果跟Flow的例子中類似。
構造函數以下:
Wrap({ Key key, this.direction = Axis.horizontal, this.alignment = WrapAlignment.start, this.spacing = 0.0, this.runAlignment = WrapAlignment.start, this.runSpacing = 0.0, this.crossAxisAlignment = WrapCrossAlignment.start, this.textDirection, this.verticalDirection = VerticalDirection.down, List<Widget> children = const <Widget>[], })
direction:主軸(mainAxis)的方向,默認爲水平。
alignment:主軸方向上的對齊方式,默認爲start。
spacing:主軸方向上的間距。
runAlignment:run的對齊方式。run能夠理解爲新的行或者列,若是是水平方向佈局的話,run能夠理解爲新的一行。
runSpacing:run的間距。
crossAxisAlignment:交叉軸(crossAxis)方向上的對齊方式。
textDirection:文本方向。
verticalDirection:定義了children擺放順序,默認是down,見Flex相關屬性介紹。
咱們來看下其佈局代碼。
第一步,若是第一個child爲null,則將其設置爲最小尺寸。
RenderBox child = firstChild; if (child == null) { size = constraints.smallest; return; }
第二步,根據direction、textDirection以及verticalDirection屬性,計算出相關的mainAxis、crossAxis是否須要調整方向,以及主軸方向上的限制。
double mainAxisLimit = 0.0; bool flipMainAxis = false; bool flipCrossAxis = false; switch (direction) { case Axis.horizontal: childConstraints = new BoxConstraints(maxWidth: constraints.maxWidth); mainAxisLimit = constraints.maxWidth; if (textDirection == TextDirection.rtl) flipMainAxis = true; if (verticalDirection == VerticalDirection.up) flipCrossAxis = true; break; case Axis.vertical: childConstraints = new BoxConstraints(maxHeight: constraints.maxHeight); mainAxisLimit = constraints.maxHeight; if (verticalDirection == VerticalDirection.up) flipMainAxis = true; if (textDirection == TextDirection.rtl) flipCrossAxis = true; break; }
第三步,計算出主軸以及交叉軸的區域大小。
while (child != null) { child.layout(childConstraints, parentUsesSize: true); final double childMainAxisExtent = _getMainAxisExtent(child); final double childCrossAxisExtent = _getCrossAxisExtent(child); if (childCount > 0 && runMainAxisExtent + spacing + childMainAxisExtent > mainAxisLimit) { mainAxisExtent = math.max(mainAxisExtent, runMainAxisExtent); crossAxisExtent += runCrossAxisExtent; if (runMetrics.isNotEmpty) crossAxisExtent += runSpacing; runMetrics.add(new _RunMetrics(runMainAxisExtent, runCrossAxisExtent, childCount)); runMainAxisExtent = 0.0; runCrossAxisExtent = 0.0; childCount = 0; } runMainAxisExtent += childMainAxisExtent; if (childCount > 0) runMainAxisExtent += spacing; runCrossAxisExtent = math.max(runCrossAxisExtent, childCrossAxisExtent); childCount += 1; final WrapParentData childParentData = child.parentData; childParentData._runIndex = runMetrics.length; child = childParentData.nextSibling; }
第四步,根據direction設置Wrap的尺寸。
switch (direction) { case Axis.horizontal: size = constraints.constrain(new Size(mainAxisExtent, crossAxisExtent)); containerMainAxisExtent = size.width; containerCrossAxisExtent = size.height; break; case Axis.vertical: size = constraints.constrain(new Size(crossAxisExtent, mainAxisExtent)); containerMainAxisExtent = size.height; containerCrossAxisExtent = size.width; break; }
第五步,根據runAlignment計算出每個run之間的距離,幾種屬性的差別,以前文章介紹過,在此就不作詳細闡述。
final double crossAxisFreeSpace = math.max(0.0, containerCrossAxisExtent - crossAxisExtent); double runLeadingSpace = 0.0; double runBetweenSpace = 0.0; switch (runAlignment) { case WrapAlignment.start: break; case WrapAlignment.end: runLeadingSpace = crossAxisFreeSpace; break; case WrapAlignment.center: runLeadingSpace = crossAxisFreeSpace / 2.0; break; case WrapAlignment.spaceBetween: runBetweenSpace = runCount > 1 ? crossAxisFreeSpace / (runCount - 1) : 0.0; break; case WrapAlignment.spaceAround: runBetweenSpace = crossAxisFreeSpace / runCount; runLeadingSpace = runBetweenSpace / 2.0; break; case WrapAlignment.spaceEvenly: runBetweenSpace = crossAxisFreeSpace / (runCount + 1); runLeadingSpace = runBetweenSpace; break; }
第六步,根據alignment計算出每個run中child的主軸方向上的間距。
switch (alignment) { case WrapAlignment.start: break; case WrapAlignment.end: childLeadingSpace = mainAxisFreeSpace; break; case WrapAlignment.center: childLeadingSpace = mainAxisFreeSpace / 2.0; break; case WrapAlignment.spaceBetween: childBetweenSpace = childCount > 1 ? mainAxisFreeSpace / (childCount - 1) : 0.0; break; case WrapAlignment.spaceAround: childBetweenSpace = mainAxisFreeSpace / childCount; childLeadingSpace = childBetweenSpace / 2.0; break; case WrapAlignment.spaceEvenly: childBetweenSpace = mainAxisFreeSpace / (childCount + 1); childLeadingSpace = childBetweenSpace; break; }
最後一步,調整child的位置。
while (child != null) { final WrapParentData childParentData = child.parentData; if (childParentData._runIndex != i) break; final double childMainAxisExtent = _getMainAxisExtent(child); final double childCrossAxisExtent = _getCrossAxisExtent(child); final double childCrossAxisOffset = _getChildCrossAxisOffset(flipCrossAxis, runCrossAxisExtent, childCrossAxisExtent); if (flipMainAxis) childMainPosition -= childMainAxisExtent; childParentData.offset = _getOffset(childMainPosition, crossAxisOffset + childCrossAxisOffset); if (flipMainAxis) childMainPosition -= childBetweenSpace; else childMainPosition += childMainAxisExtent + childBetweenSpace; child = childParentData.nextSibling; } if (flipCrossAxis) crossAxisOffset -= runBetweenSpace; else crossAxisOffset += runCrossAxisExtent + runBetweenSpace;
咱們大體梳理一下佈局的流程。
對於一些須要按寬度或者高度,讓child自動換行佈局的場景,可使用,可是Wrap能夠知足的場景,Flow必定能夠實現,只不過會複雜不少,可是相對的會靈活以及高效不少。
筆者建了一個Flutter學習相關的項目,Github地址,裏面包含了筆者寫的關於Flutter學習相關的一些文章,會按期更新,也會上傳一些學習Demo,歡迎你們關注。