前言
本文的目的是為了讓讀者掌握不同布局類Widget的布局特點(diǎn)傻盟,分享一些在實(shí)際使用過程遇到的一些問題戈钢,在《Flutter實(shí)戰(zhàn)》這本書中已經(jīng)講解的很詳細(xì)了,本文主要是對(duì)其內(nèi)容的濃縮及實(shí)際遇到的問題的補(bǔ)充。
什么是布局類Widget
布局類Widget就是指直接或間接繼承(包含)MultiChildRenderObjectWidget的Widget彪蓬,它們一般都會(huì)有一個(gè)children屬性用于接收子Widget。在Flutter中Element樹才是最終的繪制樹捺萌,Element樹是通過widget樹來創(chuàng)建的(通過Widget.createElement())档冬,widget其實(shí)就是Element的配置數(shù)據(jù)。它的最終布局桃纯、UI界面渲染都是通過RenderObject對(duì)象來實(shí)現(xiàn)的酷誓,這里的細(xì)節(jié)我就不詳細(xì)描述了,因?yàn)槲乙膊欢埂2贿^感興趣的小伙伴也可以看看本專欄的Flutter視圖的Layout與Paint這篇文章呛牲。
Flutter中主要有以下幾種布局類的Widget:
- 線性布局Row和Column
- 彈性布局Flex
- 流式布局Wrap、Flow
- 層疊布局Stack驮配、Positioned
本文Demo地址
線性布局Row和Column
線性布局其實(shí)是指沿水平或垂直方向排布子Widget娘扩,F(xiàn)lutter中通過Row來實(shí)現(xiàn)水平方向的子Widegt布局着茸,通過Column來實(shí)現(xiàn)垂直方向的子Widget布局。他們都繼承Flex琐旁,所以它們有很多相似的屬性涮阔。
在前端的Flex布局中,默認(rèn)存在兩根軸:水平的主軸(main axis)和垂直的交叉軸(cross axis)灰殴。主軸的開始位置(與邊框的交叉點(diǎn))叫做main start敬特,結(jié)束位置叫做main end;交叉軸的開始位置叫做cross start牺陶,結(jié)束位置叫做cross end伟阔。與Flutter中MainAxisAlignment和CrossAxisAlignment類似,分別代表主軸對(duì)齊和縱軸對(duì)齊掰伸。
源碼屬性解讀
Row({
.....
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List<Widget> children = const <Widget>[],
})
Column({
.....
MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start,
MainAxisSize mainAxisSize = MainAxisSize.max,
CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center,
TextDirection textDirection,
VerticalDirection verticalDirection = VerticalDirection.down,
TextBaseline textBaseline,
List<Widget> children = const <Widget>[],
})
- textDirection:表示水平方向子widget的布局順序(是從左往右還是從右往左)皱炉,默認(rèn)為系統(tǒng)當(dāng)前Locale環(huán)境的文本方向(如中文、英語都是從左往右狮鸭,而阿拉伯語是從右往左)合搅。
- 主軸方向: Row即為水平方向,Column為垂直方向
- mainAxisAlignment 主軸方向歧蕉,對(duì)child起作用
- center:將children放置在主軸的中心
- start:將children放置在主軸的起點(diǎn)
- end:將children放置在主軸的末尾
- spaceAround:將主軸方向上的空白區(qū)域均分灾部,使children之間的空白區(qū)域相等,但是首尾child的靠邊間距為空白區(qū)域?yàn)?/2
- spaceBetween:將主軸方向上的空白區(qū)域均分惯退,使children之間的空白區(qū)域相等赌髓,首尾child靠邊沒有間隙
- spaceEvenly:將主軸方向上的空白區(qū)域均分,使得children之間的空白區(qū)域相等催跪,包括首尾child
- mainAxisSize max表示盡可能占多的控件锁蠕,min會(huì)導(dǎo)致控件聚攏在一起
- crossAxisAlignment 交叉軸方向,對(duì)child起作用
- baseline:使children baseline對(duì)齊
- center:children在交叉軸上居中展示
- end:children在交叉軸上末尾展示
- start:children在交叉軸上起點(diǎn)處展示
- stretch:讓children填滿交叉軸方向
- verticalDirection 叠荠,child的放置順序
- VerticalDirection.down匿沛,在Row中就是從左邊到右邊,Column代表從頂部到底部
- VerticalDirection.up榛鼎,相反
Row
示例代碼
ListView(
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Text("我是Row的子控件 "),
Text("MainAxisAlignment.start")
],
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("我是Row的子控件 "),
Text("MainAxisAlignment.center")
],
),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
Text("我是Row的子控件 "),
Text("MainAxisAlignment.end")
],
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
verticalDirection: VerticalDirection.up,
children: <Widget>[
Text(" Hello World ", style: TextStyle(fontSize: 30.0),),
Text(" I am Jack "),
],
],
)
代碼運(yùn)行效果
前3個(gè)Row很簡(jiǎn)單逃呼,只是設(shè)置了主軸方向的對(duì)齊方式;第四個(gè)Row測(cè)試的是縱軸的對(duì)齊方式者娱,由于兩個(gè)子Text字體不一樣抡笼,所以其高度也不同,我們指定了verticalDirection值為VerticalDirection.up黄鳍,即從低向頂排列推姻,而此時(shí)crossAxisAlignment值為CrossAxisAlignment.start表示底對(duì)齊。大家可以參考上面Row和Column的主側(cè)軸的示意圖框沟,看看布局是不是正確的藏古,還有很多種情況就不一一列舉了增炭。
Column
示例代碼
ListView(children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text("我是Colum的子控件"),
Text("CrossAxisAlignment.start"),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Text("我是Colum的子控件"),
Text("CrossAxisAlignment.center"),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text("我是Colum的子控件"),
Text("CrossAxisAlignment.end"),
],
),
],)
代碼運(yùn)行效果
Column和Row差不多,只是布局方向不一樣而已拧晕,大家可以參考著看隙姿,這里就不再贅述了。
實(shí)際使用
由于篇幅有限厂捞,我就不詳細(xì)講解實(shí)際遇到的問題了输玷,只說現(xiàn)象和解決辦法:
- 如果Row里面嵌套R(shí)ow,或者Column里面再嵌套Column靡馁,那么只有對(duì)最外面的Row或Column會(huì)占用盡可能大的空間欲鹏,里面Row或Column所占用的空間為實(shí)際大小,如果要讓里面的Colum或Row占滿外部Colum或Row臭墨,可以使用Expanded widget
- 如果使用Column發(fā)現(xiàn)超范圍赔嚎,可用SingleChildScrollView包裹,scrollDirection屬性設(shè)置滑動(dòng)方向
- 使用Column嵌套ListView/GridView的時(shí)候裙犹,會(huì)報(bào)異常信息【Viewports expand in the scrolling direction to fill their container...】尽狠,這種情況flutter已給出解決辦法衔憨,將ListView/GridView的 shrinkWrap屬性設(shè)為true
- 有的時(shí)候修改Row/Column的verticalDirection會(huì)得到很好的效果叶圃,比如需要頁面在底部需要幾個(gè)按鍵,也可以用Stack來布局践图,但是相對(duì)麻煩掺冠,而且有時(shí)還需要知道控件的大小,沒有verticalDirection方便
彈性布局
彈性布局是一種允許子widget按照一定比例來分配父容器空間的布局方式码党,如果你知道了它的主軸方向德崭,那就可以用Row或Column了,一般情況下揖盘,可以用Flex的地方都可以用Row或者Column一起使用眉厨,通常配合Expanded Widget來使用,同樣Expanded也不能脫離Flex單獨(dú)創(chuàng)建兽狭。
Expanded
Expanded繼承自Flexible憾股,F(xiàn)lexible是一個(gè)控制Row、Column箕慧、Flex等子組件如何布局的組件服球,它可以按比例“擴(kuò)伸”Row、Column和Flex子widget所占用的空間颠焦。
const Expanded({
int flex = 1,
@required Widget child,
})
flex為彈性系數(shù)斩熊,如果為0或null,則child是沒有彈性的伐庭,即不會(huì)被擴(kuò)伸占用的空間粉渠。如果大于0分冈,所有的Expanded按照其flex的比例來分割主軸的全部空閑空間。
示例代碼
Row(children: <Widget>[
RaisedButton(
onPressed: () {
print('點(diǎn)擊紅色按鈕事件');
},
color: Colors.red,
child: Text('紅色按鈕'),
),
Expanded(
flex: 1,
child: RaisedButton(
onPressed: () {
print('點(diǎn)擊黃色按鈕事件');
},
color: Colors.yellow,
child: Text('黃色按鈕'),
),
),
RaisedButton(
onPressed: () {
print('點(diǎn)擊粉色按鈕事件');
},
color: Colors.green,
child: Text('綠色按鈕'),
),
])
代碼運(yùn)行效果
Flexible和 Expanded的區(qū)別
- Flexible組件必須是Row霸株、Column丈秩、Flex等組件的后裔,并且從Flexible到它封裝的Row淳衙、Column蘑秽、Flex的路徑必須只包括StatelessWidgets或StatefulWidgets組件(不能是其他類型的組件,像RenderObjectWidgets)
- Row箫攀、Column肠牲、Flex會(huì)被Expanded撐開,充滿主軸可用空間靴跛,而Flexible不強(qiáng)制子組件填充可用空間缀雳,這是因?yàn)閒it屬性的值不同,該屬性在Expanded中為FlexFit.tight梢睛,F(xiàn)lexible為FlexFit.loose肥印,區(qū)別在于tight表示強(qiáng)制使子控件填充剩余可用空間,loose表示最多填滿其在父控件所設(shè)置的比例绝葡,所以loose默認(rèn)即為控件的大小
流式布局
流式布局(Liquid)的特點(diǎn)(也叫"Fluid") 是頁面元素的寬度按照屏幕分辨率進(jìn)行適配調(diào)整深碱,但整體布局不變。柵欄系統(tǒng)(網(wǎng)格系統(tǒng))藏畅,用戶標(biāo)簽等敷硅。在Flutter中主要有Wrap和Flow兩種Widget實(shí)現(xiàn)。
Wrap
在介紹Row和Colum時(shí)愉阎,如果子widget超出屏幕范圍绞蹦,則會(huì)報(bào)溢出錯(cuò)誤,在Flutter中通過Wrap和Flow來支持流式布局榜旦,溢出部分則會(huì)自動(dòng)折行幽七。
源碼屬性解讀
Wrap({
...
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>[],
})
上述有很多屬性和Row的相同,其意義其實(shí)也是相同的溅呢,這里我就不一一介紹了澡屡,主要介紹下不同的屬性:
- spacing:主軸方向子widget的間距
- runSpacing:縱軸方向的間距
- runAlignment:縱軸方向的對(duì)齊方式
示例代碼
Wrap(
spacing: 10.0,
direction: Axis.horizontal,
alignment: WrapAlignment.start,
children: <Widget>[
_card('關(guān)注'),
_card('推薦'),
_card('新時(shí)代'),
_card('小視頻'),
_card('黨媒推薦'),
_card('中國新唱將'),
_card('歷史'),
_card('視頻'),
_card('游戲'),
_card('頭條號(hào)'),
_card('數(shù)碼'),
],
)
Widget _card(String title) {
return Card(child: Text(title),);
}
}
運(yùn)行效果
小結(jié)
- 使用Wrap可以很輕松的實(shí)現(xiàn)流式布局效果
- Wrap支持設(shè)置流式布局是縱向顯示或者是橫向顯示
- 可以使用alignment屬性來控制widgets的布局方式
Flow
我們一般很少會(huì)使用Flow,因?yàn)槠溥^于復(fù)雜藕届,需要自己實(shí)現(xiàn)子widget的位置轉(zhuǎn)換挪蹭,在很多場(chǎng)景下首先要考慮的是Wrap是否滿足需求。Flow主要用于一些需要自定義布局策略或性能要求較高(如動(dòng)畫中)的場(chǎng)景休偶。Flow有如下優(yōu)點(diǎn):
- 性能好梁厉;Flow是一個(gè)對(duì)child尺寸以及位置調(diào)整非常高效的控件,F(xiàn)low用轉(zhuǎn)換矩陣(transformation matrices)在對(duì)child進(jìn)行位置調(diào)整的時(shí)候進(jìn)行了優(yōu)化:在Flow定位過后,如果child的尺寸或者位置發(fā)生了變化词顾,在FlowDelegate中的paintChildren()方法中調(diào)用context.paintChild 進(jìn)行重繪八秃,而context.paintChild在重繪時(shí)使用了轉(zhuǎn)換矩陣(transformation matrices),并沒有實(shí)際調(diào)整Widget位置肉盹。
- 靈活昔驱;由于我們需要自己實(shí)現(xiàn)FlowDelegate的paintChildren()方法,所以我們需要自己計(jì)算每一個(gè)widget的位置上忍,因此骤肛,可以自定義布局策略。
缺點(diǎn): - 使用復(fù)雜.
- 不能自適應(yīng)子widget大小,必須通過指定父容器大小或?qū)崿F(xiàn)TestFlowDelegate的getSize返回固定大小。
示例代碼
我們對(duì)六個(gè)色塊進(jìn)行自定義流式布局:
Flow(
delegate: TestFlowDelegate(margin: EdgeInsets.all(10.0)),
children: <Widget>[
new Container(width: 80.0, height:80.0, color: Colors.red,),
new Container(width: 80.0, height:80.0, color: Colors.green,),
new Container(width: 80.0, height:80.0, color: Colors.blue,),
new Container(width: 80.0, height:80.0, color: Colors.yellow,),
new Container(width: 80.0, height:80.0, color: Colors.brown,),
new Container(width: 80.0, height:80.0, color: Colors.purple,),
],
)
實(shí)現(xiàn)TestFlowDelegate:
class TestFlowDelegate extends FlowDelegate {
EdgeInsets margin = EdgeInsets.zero;
TestFlowDelegate({this.margin});
@override
void paintChildren(FlowPaintingContext context) {
var x = margin.left;
var y = margin.top;
//計(jì)算每一個(gè)子widget的位置
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;
//繪制子widget(有優(yōu)化)
context.paintChild(i,
transform: new Matrix4.translationValues(
x, y, 0.0));
x += context.getChildSize(i).width + margin.left + margin.right;
}
}
}
getSize(BoxConstraints constraints){
//指定Flow的大小
return Size(double.infinity,200.0);
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) {
return oldDelegate != this;
}
}
運(yùn)行效果
可以看到我們主要的任務(wù)就是實(shí)現(xiàn)paintChildren泳叠,它的主要任務(wù)是確定每個(gè)子widget位置。由于Flow不能自適應(yīng)子widget的大小咸产,我們通過在getSize返回一個(gè)固定大小來指定Flow的大小,實(shí)現(xiàn)起來還是比較麻煩的。
小結(jié)
- 參數(shù)簡(jiǎn)單,不過需要自己定義delegate
- delegate一般是為了實(shí)現(xiàn)child的繪制絮蒿,就是位置的擺放,不同情況需要定義不同的delegate
- 不同的delegate一般會(huì)提供實(shí)現(xiàn)的幾個(gè)方法:
- getConstraintsForChild: 設(shè)置每個(gè)child的布局約束條件叁鉴,會(huì)覆蓋已有的方式
- getSize:設(shè)置控件的尺寸
- shouldRelayout:表示是否需要重新布局
- 盡可能的用Wrap土涝,畢竟簡(jiǎn)單
層疊布局
層疊布局和Web中的絕對(duì)定位、Android中的Frame布局是相似的亲茅,子widget可以根據(jù)到父容器四個(gè)角的位置來確定本身的位置回铛。絕對(duì)定位允許子widget堆疊(按照代碼中聲明的順序)狗准。Flutter中使用Stack和Positioned來實(shí)現(xiàn)絕對(duì)定位克锣,Stack允許子widget堆疊,而Positioned可以給子widget定位(根據(jù)Stack的四個(gè)角)腔长。
Stack
Stack({
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
- alignment:此參數(shù)決定如何去對(duì)齊沒有定位(沒有使用Positioned)或部分定位的子widget袭祟。所謂部分定位,在這里特指沒有在某一個(gè)軸上定位:left捞附、right為橫軸巾乳,top、bottom為縱軸鸟召,只要包含某個(gè)軸上的一個(gè)定位屬性就算在該軸上有定位胆绊。
- textDirection:和Row、Wrap的textDirection功能一樣欧募,都用于決定alignment對(duì)齊的參考系即:textDirection的值為TextDirection.ltr压状,則alignment的start代表左,end代表右;textDirection的值為TextDirection.rtl种冬,則alignment的start代表右镣丑,end代表左。
- fit:此參數(shù)用于決定沒有定位的子widget如何去適應(yīng)Stack的大小娱两。StackFit.loose表示使用子widget的大小莺匠,StackFit.expand表示擴(kuò)伸到Stack的大小。
- overflow:此屬性決定如何顯示超出Stack顯示空間的子widget十兢,值為Overflow.clip時(shí)趣竣,超出部分會(huì)被剪裁(隱藏),值為Overflow.visible 時(shí)則不會(huì)旱物。
下面是我用Stack實(shí)現(xiàn)的一個(gè)簡(jiǎn)易的loading
class Loading extends StatelessWidget {
/// ProgressIndicator的padding期贫,決定loading的大小
final EdgeInsets padding = EdgeInsets.all(30.0);
/// 文字頂部距菊花的底部的距離
final double margin = 10.0;
/// 圓角
final double cornerRadius = 10.0;
final Widget _child;
final bool _isLoading;
final double opacity;
final Color color;
final String text;
Loading({
Key key,
@required child,
@required isLoading,
this.text,
this.opacity = 0.3,
this.color = Colors.grey,
}) : assert(child != null),
assert(isLoading != null),
_child = child,
_isLoading = isLoading,
super(key: key);
@override
Widget build(BuildContext context) {
List<Widget> widgetList = List<Widget>();
widgetList.add(_child);
if (_isLoading) {
final loading = [
Opacity(
opacity: opacity,
child: ModalBarrier(dismissible: false, color: color),
),
_buildProgressIndicator()
];
widgetList.addAll(loading);
}
return Stack(
children: widgetList,
);
}
Widget _buildProgressIndicator() {
return Center(
child: Container(
padding: padding,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
CupertinoActivityIndicator(),
Padding(
padding: EdgeInsets.only(top: margin),
child: Text(text ?? '加載中...')),
],
),
decoration: BoxDecoration(
borderRadius: BorderRadius.all(Radius.circular(cornerRadius)),
color: Colors.white),
),
);
}
}
顯示效果
本控件使用Stack封裝,你傳入的主視圖在最下面一層异袄,背景層在中間通砍,最上面一層為菊花和文字loading,用isLoading控制顯示
Positioned
const Positioned({
Key key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
@required Widget child,
})
left烤蜕、top 封孙、right、 bottom分別代表離Stack左讽营、上虎忌、右、底四邊的距離橱鹏。width和height用于指定定位元素的寬度和高度膜蠢,注意,此處的width莉兰、height 和其它地方的意義稍微有點(diǎn)區(qū)別挑围,此處用于配合left、top 糖荒、right杉辙、 bottom來定位widget,舉個(gè)例子捶朵,在水平方向時(shí)蜘矢,你只能指定left、right综看、width三個(gè)屬性中的兩個(gè)品腹,如指定left和width后,right會(huì)自動(dòng)算出(left+width)红碑,如果同時(shí)指定三個(gè)屬性則會(huì)報(bào)錯(cuò)舞吭,垂直方向同理。
示例代碼
//通過ConstrainedBox來確保Stack占滿屏幕
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Stack(
alignment:Alignment.center , //指定未定位或部分定位widget的對(duì)齊方式
children: <Widget>[
Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
left: 18.0,
child: Text("I am Jack"),
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
);
運(yùn)行效果:
由于第一個(gè)子widget Text("Hello world")沒有指定定位,并且alignment值為Alignment.center镣典,所以兔毙,它會(huì)居中顯示。第二個(gè)子widget Text("I am Jack")只指定了水平方向的定位(left)兄春,所以屬于部分定位澎剥,即垂直方向上沒有定位,那么它在垂直方向?qū)R方式則會(huì)按照alignment指定的對(duì)齊方式對(duì)齊赶舆,即垂直方向居中哑姚。對(duì)于第三個(gè)子widget Text("Your friend"),和第二個(gè)Text原理一樣芜茵,只不過是水平方向沒有定位叙量,則水平方向居中。