一,概述
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(按寬高自動換行布局)
二,其它布局處理
- Transform(矩陣轉換)
- 介紹
Transform在介紹Container的時候有提到過,就是做矩陣變換的。Container中矩陣變換就是使用的Transform。
- 布局行為
有過其他平台經驗的,對Transform應該不會陌生。可以對child做平移、旋轉、縮放等操作。
- 繼承關系
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Transform
- 構造函數
const Transform({ Key key, @required this.transform, this.origin, this.alignment, this.transformHitTests = true, Widget child, })
上面是其默認的構造函數,Transform也提供下面三種構造函數:
Transform.rotate Transform.translate Transform.scale
- 參數含義
- transform:一個4x4的矩陣。不難發現,其他平台的變換矩陣也都是四階的。一些復合操作,僅靠三維是不夠的,必須采用額外的一維來補充,感興趣的同學可以自行搜索了解。
-
origin:旋轉點,相對於左上角頂點的偏移。默認旋轉點事左上角頂點。
-
alignment:對齊方式。
-
transformHitTests:點擊區域是否也做相應的改變。
- transform:一個4x4的矩陣。不難發現,其他平台的變換矩陣也都是四階的。一些復合操作,僅靠三維是不夠的,必須采用額外的一維來補充,感興趣的同學可以自行搜索了解。
- 介紹
- Baseline(基准線布局)
- 介紹
Baseline這個控件,做過移動端開發的都會了解過,一般文字排版的時候,可能會用到它。它的作用很簡單,根據child的baseline,來調整child的位置。例如兩個字號不一樣的文字,希望底部在一條水平線上,就可以使用這個控件,是一個非常基礎的控件。
關於字符的Baseline,可以看下下面這張圖,這具體就涉及到了字體排版,感興趣的同學可以自行了解。
- 布局行為
Baseline控件布局行為分為兩種情況:
- 如果child有baseline,則根據child的baseline屬性,調整child的位置;
- 如果child沒有baseline,則根據child的bottom,來調整child的位置。
- 繼承關系
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Baseline
- 構造函數
const Baseline({ Key key, @required this.baseline, @required this.baselineType, Widget child })
- 參數含義
baseline:baseline數值,必須要有,從頂部算。
baselineType:bseline類型,也是必須要有的,目前有兩種類型:
- alphabetic:對齊字符底部的水平線;
- ideographic:對齊表意字符的水平線。
- 介紹
- Offstage(控制是否顯示組件)
- 介紹
Offstage的作用很簡單,通過一個參數,來控制child是否顯示,日常使用中也算是比較常用的控件。 - 布局行為
Offstage的布局行為完全取決於其offstage參數
- 當offstage為true,當前控件不會被繪制在屏幕上,不會響應點擊事件,也不會占用空間;
- 當offstage為false,當前控件則跟平常用的控件一樣渲染繪制;
另外,當Offstage不可見的時候,如果child有動畫,應該手動停掉,Offstage並不會停掉動畫。
- 繼承關系
Object > Diagnosticable > DiagnosticableTree > Widget > RenderObjectWidget > SingleChildRenderObjectWidget > Offstage
- 構造函數
const Offstage(
{
Key key,
this.offstage = true,
Widget child
}
) - 參數含義
offstage:默認為true,也就是不顯示,當為flase的時候,會顯示該控件。
- 介紹
- Wrap(按寬高自動換行布局)
- 介紹
其實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({ 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相關屬性介紹。
- direction:主軸(mainAxis)的方向,默認為水平。
三,使用實例
- Transform(矩陣轉換)
Center( child: Transform( transform: Matrix4.rotationZ(0.3), child: Container( color: Colors.blue, width: 100.0, height: 100.0, ), ), )
將Container繞z軸旋轉了
效果圖:
源碼解析:我們來看看它的繪制代碼:
if (child != null) { final Matrix4 transform = _effectiveTransform; final Offset childOffset = MatrixUtils.getAsTranslation(transform); if (childOffset == null) context.pushTransform(needsCompositing, offset, transform, super.paint); else super.paint(context, offset + childOffset); }
整個繪制代碼不復雜,如果child有偏移的話,則將兩個偏移相加,進行繪制。如果child沒有偏移的話,則按照設置的offset、transform進行繪制。
使用場景:這個控件算是較常見的控件,很多平移、旋轉、縮放都可以使用的到。如果只是單純的進行變換的話,用Transform比用Container效率會更高。
- Baseline(基准線布局)
new Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ new Baseline( baseline: 50.0, baselineType: TextBaseline.alphabetic, child: new Text( 'TjTjTj', style: new TextStyle( fontSize: 20.0, textBaseline: TextBaseline.alphabetic, ), ), ), new Baseline( baseline: 50.0, baselineType: TextBaseline.alphabetic, child: new Container( width: 30.0, height: 30.0, color: Colors.red, ), ), new Baseline( baseline: 50.0, baselineType: TextBaseline.alphabetic, child: new Text( 'RyRyRy', style: new TextStyle( fontSize: 35.0, textBaseline: TextBaseline.alphabetic, ), ), ), ], )
效果圖:
源碼解析:
我們來看看源碼中具體計算尺寸的這段代碼
child.layout(constraints.loosen(), parentUsesSize: true); final double childBaseline = child.getDistanceToBaseline(baselineType); final double actualBaseline = baseline; final double top = actualBaseline - childBaseline; final BoxParentData childParentData = child.parentData; childParentData.offset = new Offset(0.0, top); final Size childSize = child.size; size = constraints.constrain(new Size(childSize.width, top + childSize.height));
getDistanceToBaseline這個函數是獲取baseline數值的,存在的話,就取這個值,不存在的話,則取其高度。
整體的計算過程:
(1)獲取child的 baseline 值;
(2)計算出top值,其為 baseline - childBaseline,這個值有可能為負數;
(3)計算出Baseline控件尺寸,寬度為child的,高度則為 top + childSize.height。 - Offstage(控制是否顯示組件)
Column( children: <Widget>[ new Offstage( offstage: offstage, child: Container(color: Colors.blue, height: 100.0), ), new CupertinoButton( child: Text("點擊切換顯示"), onPressed: () { setState(() { offstage = !offstage; }); }, ), ], )
源碼解析:
我們先來看下Offstage的computeIntrinsicSize相關的方法:
@override double computeMinIntrinsicWidth(double height) { if (offstage) return 0.0; return super.computeMinIntrinsicWidth(height); }
可以看到,當offstage為true的時候,自身的最小以及最大寬高都會被置為0.0。
接下來我們來看下其hitTest方法:
@override bool hitTest(HitTestResult result, { Offset position }) { return !offstage && super.hitTest(result, position: position); }
當offstage為true的時候,也不會去執行。
最后我們來看下其paint方法:
@override void paint(PaintingContext context, Offset offset) { if (offstage) return; super.paint(context, offset); }
當offstage為true的時候直接返回,不繪制了。
到此,跟上面所說的布局行為對應上了。我們一定要清楚一件事情,Offstage並不是通過插入或者刪除自己在widget tree中的節點,來達到顯示以及隱藏的效果,而是通過設置自身尺寸、不響應hitTest以及不繪制,來達到展示與隱藏的效果。
- 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'), ), ], )
效果圖:
源碼解析:我們來看下其布局代碼。
第一步,如果第一個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為null,則將Wrap設置為最小尺寸,布局結束; 根據direction、textDirection以及verticalDirection屬性,計算出mainAxis、crossAxis是否需要調整方向; 計算出主軸以及交叉軸的區域大小; 根據direction設置Wrap的尺寸; 根據runAlignment計算出每一個run之間的距離; 根據alignment計算出每一個run中child的主軸方向上的間距 調整每一個child的位置。
四,參考