老孟導(dǎo)讀:Flutter中布局組件有水平 / 垂直布局組件( Row 和 Column )俐末、疊加布局組件( Stack 和 IndexedStack )帽驯、流式布局組件( Wrap )和 自定義布局組件(Flow)。
水平、垂直布局組件
Row 是將子組件以水平方式布局的組件, Column 是將子組件以垂直方式布局的組件。項(xiàng)目中 90% 的頁面布局都可以通過 Row 和 Column 來實(shí)現(xiàn)逢净。
將3個組件水平排列:
Row(
children: <Widget>[
Container(
height: 50,
width: 100,
color: Colors.red,
),
Container(
height: 50,
width: 100,
color: Colors.green,
),
Container(
height: 50,
width: 100,
color: Colors.blue,
),
],
)
將3個組件垂直排列:
Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
height: 50,
width: 100,
color: Colors.red,
),
Container(
height: 50,
width: 100,
color: Colors.green,
),
Container(
height: 50,
width: 100,
color: Colors.blue,
),
],
)
在 Row 和 Column 中有一個非常重要的概念:主軸( MainAxis ) 和 交叉軸( CrossAxis ),主軸就是與組件布局方向一致的軸歼指,交叉軸就是與主軸方向垂直的軸爹土。
具體到 Row 組件,主軸 是水平方向踩身,交叉軸 是垂直方向胀茵。而 Column 與 Row 正好相反,主軸 是垂直方向挟阻,交叉軸 是水平方向琼娘。
明白了 主軸 和 交叉軸 概念,我們來看下 mainAxisAlignment 屬性附鸽,此屬性表示主軸方向的對齊方式脱拼,默認(rèn)值為 start,表示從組件的開始處布局坷备,此處的開始位置和 textDirection 屬性有關(guān)熄浓,textDirection 表示文本的布局方向,其值包括 ltr(從左到右) 和 rtl(從右到左)省撑,當(dāng) textDirection = ltr 時赌蔑,start 表示左側(cè)俯在,當(dāng) textDirection = rtl 時,start 表示右側(cè)娃惯,
Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: Row(
children: <Widget>[
Container(
height: 50,
width: 100,
color: Colors.red,
),
Container(
height: 50,
width: 100,
color: Colors.green,
),
Container(
height: 50,
width: 100,
color: Colors.blue,
),
],
),
)
黑色邊框是Row控件的范圍跷乐,默認(rèn)情況下Row鋪滿父組件。
主軸對齊方式有6種石景,效果如下圖:
spaceAround 和 spaceEvenly 區(qū)別是:
- spaceAround :第一個子控件距開始位置和最后一個子控件距結(jié)尾位置是其他子控件間距的一半劈猿。
- spaceEvenly : 所有間距一樣。
和主軸對齊方式相對應(yīng)的就是交叉軸對齊方式 crossAxisAlignment 潮孽,交叉軸對齊方式默認(rèn)是居中揪荣。Row控件的高度是依賴子控件高度,因此子控件高都一樣時往史,Row的高和子控件高相同仗颈,此時是無法體現(xiàn)交叉軸對齊方式,修改3個顏色塊高分別為50椎例,100挨决,150,這樣Row的高是150订歪,代碼如下:
Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Container(
height: 50,
width: 100,
color: Colors.red,
),
Container(
height: 100,
width: 100,
color: Colors.green,
),
Container(
height: 150,
width: 100,
color: Colors.blue,
),
],
),
)
主軸對齊方式效果如下圖:
mainAxisSize 表示主軸尺寸脖祈,有 min 和 max 兩種方式,默認(rèn)是 max刷晋。min 表示盡可能小盖高,max 表示盡可能大。
Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: Row(
mainAxisSize: MainAxisSize.min,
...
)
)
看黑色邊框眼虱,正好包裹子組件喻奥,而 max 效果如下:
textDirection 表示子組件主軸布局方向,值包括 ltr(從左到右) 和 rtl(從右到左)
Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: Row(
textDirection: TextDirection.rtl,
children: <Widget>[
...
],
),
)
verticalDirection 表示子組件交叉軸布局方向:
- up :從底部開始捏悬,并垂直堆疊到頂部撞蚕,對齊方式的 start 在底部,end 在頂部过牙。
- down: 與 up 相反甥厦。
Container(
decoration: BoxDecoration(border: Border.all(color: Colors.black)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
verticalDirection: VerticalDirection.up,
children: <Widget>[
Container(
height: 50,
width: 100,
color: Colors.red,
),
Container(
height: 100,
width: 100,
color: Colors.green,
),
Container(
height: 150,
width: 100,
color: Colors.blue,
),
],
),
)
想一想這種效果完全可以通過對齊方式實(shí)現(xiàn),那么為什么還要有 textDirection 和 verticalDirection 這兩個屬性寇钉,官方API文檔已經(jīng)解釋了這個問題:
This is also used to disambiguate start and end values (e.g. [MainAxisAlignment.start] or [CrossAxisAlignment.end]).
用于消除 MainAxisAlignment.start 和 CrossAxisAlignment.end 值的歧義的矫渔。
疊加布局組件
疊加布局組件包含 Stack 和 IndexedStack,Stack 組件將子組件疊加顯示摧莽,根據(jù)子組件的順利依次向上疊加,用法如下:
Stack(
children: <Widget>[
Container(
height: 200,
width: 200,
color: Colors.red,
),
Container(
height: 170,
width: 170,
color: Colors.blue,
),
Container(
height: 140,
width: 140,
color: Colors.yellow,
)
],
)
Stack 對未定位(不被 Positioned 包裹)子組件的大小由 fit 參數(shù)決定顿痪,默認(rèn)值是 StackFit.loose 镊辕,表示子組件自己決定油够,StackFit.expand 表示盡可能的大,用法如下:
Stack(
fit: StackFit.expand,
children: <Widget>[
Container(
height: 200,
width: 200,
color: Colors.red,
),
Container(
height: 170,
width: 170,
color: Colors.blue,
),
Container(
height: 140,
width: 140,
color: Colors.yellow,
)
],
)
效果只有黃色(最后一個組件的顏色)征懈,并不是其他組件沒有繪制石咬,而是另外兩個組件被黃色組件覆蓋。
Stack 對未定位(不被 Positioned 包裹)子組件的對齊方式由 alignment 控制卖哎,默認(rèn)左上角對齊鬼悠,用法如下:
Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
Container(
height: 200,
width: 200,
color: Colors.red,
),
Container(
height: 170,
width: 170,
color: Colors.blue,
),
Container(
height: 140,
width: 140,
color: Colors.yellow,
)
],
)
通過 Positioned 定位的子組件:
Stack(
alignment: AlignmentDirectional.center,
children: <Widget>[
Container(
height: 200,
width: 200,
color: Colors.red,
),
Container(
height: 170,
width: 170,
color: Colors.blue,
),
Positioned(
left: 30,
right: 40,
bottom: 50,
top: 60,
child: Container(
color: Colors.yellow,
),
)
],
)
top 、bottom 亏娜、left 焕窝、right 四種定位屬性,分別表示距離上下左右的距離维贺。
如果子組件超過 Stack 邊界由 overflow 控制它掂,默認(rèn)是裁剪,下面設(shè)置總是顯示的用法:
Stack(
overflow: Overflow.visible,
children: <Widget>[
Container(
height: 200,
width: 200,
color: Colors.red,
),
Positioned(
left: 100,
top: 100,
height: 150,
width: 150,
child: Container(
color: Colors.green,
),
)
],
)
IndexedStack 是 Stack 的子類溯泣,Stack 是將所有的子組件疊加顯示虐秋,而 IndexedStack 通過 index 只顯示指定索引的子組件,用法如下:
class IndexedStackDemo extends StatefulWidget {
@override
_IndexedStackDemoState createState() => _IndexedStackDemoState();
}
class _IndexedStackDemoState extends State<IndexedStackDemo> {
int _index = 0;
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
SizedBox(height: 50,),
_buildIndexedStack(),
SizedBox(height: 30,),
_buildRow(),
],
);
}
_buildIndexedStack() {
return IndexedStack(
index: _index,
children: <Widget>[
Center(
child: Container(
height: 300,
width: 300,
color: Colors.red,
alignment: Alignment.center,
child: Icon(
Icons.fastfood,
size: 60,
color: Colors.blue,
),
),
),
Center(
child: Container(
height: 300,
width: 300,
color: Colors.green,
alignment: Alignment.center,
child: Icon(
Icons.cake,
size: 60,
color: Colors.blue,
),
),
),
Center(
child: Container(
height: 300,
width: 300,
color: Colors.yellow,
alignment: Alignment.center,
child: Icon(
Icons.local_cafe,
size: 60,
color: Colors.blue,
),
),
),
],
);
}
_buildRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
IconButton(
icon: Icon(Icons.fastfood),
onPressed: () {
setState(() {
_index = 0;
});
},
),
IconButton(
icon: Icon(Icons.cake),
onPressed: () {
setState(() {
_index = 1;
});
},
),
IconButton(
icon: Icon(Icons.local_cafe),
onPressed: () {
setState(() {
_index = 2;
});
},
),
],
);
}
}
流式布局組件
Wrap 為子組件進(jìn)行水平或者垂直方向布局垃沦,且當(dāng)空間用完時客给,Wrap 會自動換行,也就是流式布局肢簿。
創(chuàng)建多個子控件做為 Wrap 的子控件靶剑,代碼如下:
Wrap(
children: List.generate(10, (i) {
double w = 50.0 + 10 * i;
return Container(
color: Colors.primaries[i],
height: 50,
width: w,
child: Text('$i'),
);
}),
)
direction 屬性控制布局方向,默認(rèn)為水平方向译仗,設(shè)置方向?yàn)榇怪贝a如下:
Wrap(
direction: Axis.vertical,
children: List.generate(4, (i) {
double w = 50.0 + 10 * i;
return Container(
color: Colors.primaries[i],
height: 50,
width: w,
child: Text('$i'),
);
}),
)
alignment 屬性控制主軸對齊方式抬虽,crossAxisAlignment 屬性控制交叉軸對齊方式,對齊方式只對有剩余空間的行或者列起作用纵菌,例如水平方向上正好填充完整阐污,則不管設(shè)置主軸對齊方式為什么,看上去的效果都是鋪滿咱圆。
說明 :主軸就是與當(dāng)前組件方向一致的軸笛辟,而交叉軸就是與當(dāng)前組件方向垂直的軸,如果Wrap的布局方向?yàn)樗椒较?Axis.horizontal,那么主軸就是水平方向序苏,反之布局方向?yàn)榇怪狈较?Axis.vertical 手幢,主軸就是垂直方向。
Wrap(
alignment: WrapAlignment.spaceBetween,
...
)
主軸對齊方式有6種忱详,效果如下圖:
spaceAround 和 spaceEvenly 區(qū)別是:
- spaceAround:第一個子控件距開始位置和最后一個子控件距結(jié)尾位置是其他子控件間距的一半围来。
- spaceEvenly:所有間距一樣。
設(shè)置交叉軸對齊代碼如下:
Wrap(
crossAxisAlignment: WrapCrossAlignment.center,
...
)
如果 Wrap 的主軸方向?yàn)樗椒较颍徊孑S方向則為垂直方向监透,如果想要看到交叉軸對齊方式的效果需要設(shè)置子控件的高不一樣桶错,代碼如下:
Wrap(
spacing: 5,
runSpacing: 3,
crossAxisAlignment: WrapCrossAlignment.center,
children: List.generate(10, (i) {
double w = 50.0 + 10 * i;
double h = 50.0 + 5 * i;
return Container(
color: Colors.primaries[i],
height: h,
alignment: Alignment.center,
width: w,
child: Text('$i'),
);
}),
)
runAlignment 屬性控制 Wrap 的交叉抽方向上每一行的對齊方式,下面直接看 runAlignment 6中方式對應(yīng)的效果圖胀蛮,
runAlignment 和 alignment 的區(qū)別:
- alignment :是主軸方向上對齊方式院刁,作用于每一行。
- runAlignment :是交叉軸方向上將每一行看作一個整體的對齊方式粪狼。
spacing 和 runSpacing 屬性控制Wrap主軸方向和交叉軸方向子控件之間的間隙退腥,代碼如下:
Wrap(
spacing: 5,
runSpacing: 2,
...
)
textDirection 屬性表示 Wrap 主軸方向上子組件的方向,取值范圍是 ltr(從左到右) 和 rtl(從右到左)再榄,下面是從右到左的代碼:
Wrap(
textDirection: TextDirection.rtl,
...
)
verticalDirection 屬性表示 Wrap 交叉軸方向上子組件的方向狡刘,取值范圍是 up(向上) 和 down(向下),設(shè)置代碼如下:
Wrap(
verticalDirection: VerticalDirection.up,
...
)
注意:文字為0的組件是在下面的不跟。
自定義布局組件
大部分情況下颓帝,不會使用到 Flow ,但 Flow 可以調(diào)整子組件的位置和大小窝革,結(jié)合Matrix4繪制出各種酷炫的效果购城。
Flow 組件對使用轉(zhuǎn)換矩陣操作子組件經(jīng)過系統(tǒng)優(yōu)化,性能非常高效虐译。
基本用法如下:
Flow(
delegate: SimpleFlowDelegate(),
children: List.generate(5, (index) {
return Container(
height: 100,
color: Colors.primaries[index % Colors.primaries.length],
);
}),
)
delegate 控制子組件的位置和大小瘪板,定義如下 :
class SimpleFlowDelegate extends FlowDelegate {
@override
void paintChildren(FlowPaintingContext context) {
for (int i = 0; i < context.childCount; ++i) {
context.paintChild(i);
}
}
@override
bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
return false;
}
}
delegate 要繼承 FlowDelegate,重寫 paintChildren 和 shouldRepaint 函數(shù)漆诽,上面直接繪制子組件侮攀,效果如下:
只看到一種顏色并不是只繪制了這一個,而是疊加覆蓋了厢拭,和 Stack 類似兰英,下面讓每一個組件有一定的偏移,SimpleFlowDelegate 修改如下:
class SimpleFlowDelegate extends FlowDelegate {
@override
void paintChildren(FlowPaintingContext context) {
for (int i = 0; i < context.childCount; ++i) {
context.paintChild(i,transform: Matrix4.translationValues(0,i*30.0,0));
}
}
@override
bool shouldRepaint(SimpleFlowDelegate oldDelegate) {
return false;
}
}
每一個子組件比上一個組件向下偏移30供鸠。
仿 掘金-我的效果
效果如下:
到拿到一個頁面時畦贸,先要將其拆分,上面的效果拆分如下:
總體分為3個部分楞捂,水平布局薄坏,紅色區(qū)域圓形頭像代碼如下:
_buildCircleImg() {
return Container(
height: 60,
width: 60,
decoration: BoxDecoration(
shape: BoxShape.circle,
image: DecorationImage(image: AssetImage('assets/images/logo.png'))),
);
}
藍(lán)色區(qū)域代碼如下:
_buildCenter() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('老孟Flutter', style: TextStyle(fontSize: 20),),
Text('Flutter、Android', style: TextStyle(color: Colors.grey),)
],
);
}
綠色區(qū)域是一個圖標(biāo)寨闹,代碼如下:
Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),
將這3部分組合在一起:
Container(
color: Colors.grey.withOpacity(.5),
alignment: Alignment.center,
child: Container(
height: 100,
color: Colors.white,
child: Row(
children: <Widget>[
SizedBox(
width: 15,
),
_buildCircleImg(),
SizedBox(
width: 25,
),
Expanded(
child: _buildCenter(),
),
Icon(Icons.arrow_forward_ios,color: Colors.grey,size: 14,),
SizedBox(
width: 15,
),
],
),
),
)
最終的效果就是開始我們看到的效果圖胶坠。
水平展開/收起菜單
使用Flow實(shí)現(xiàn)水平展開/收起菜單的功能,代碼如下:
class DemoFlowPopMenu extends StatefulWidget {
@override
_DemoFlowPopMenuState createState() => _DemoFlowPopMenuState();
}
class _DemoFlowPopMenuState extends State<DemoFlowPopMenu>
with SingleTickerProviderStateMixin {
//動畫必須要with這個類
AnimationController _ctrlAnimationPopMenu; //定義動畫的變量
IconData lastTapped = Icons.notifications;
final List<IconData> menuItems = <IconData>[
//菜單的icon
Icons.home,
Icons.new_releases,
Icons.notifications,
Icons.settings,
Icons.menu,
];
void _updateMenu(IconData icon) {
if (icon != Icons.menu) {
setState(() => lastTapped = icon);
} else {
_ctrlAnimationPopMenu.status == AnimationStatus.completed
? _ctrlAnimationPopMenu.reverse() //展開和收攏的效果
: _ctrlAnimationPopMenu.forward();
}
}
@override
void initState() {
super.initState();
_ctrlAnimationPopMenu = AnimationController(
//必須初始化動畫變量
duration: const Duration(milliseconds: 250), //動畫時長250毫秒
vsync: this, //SingleTickerProviderStateMixin的作用
);
}
//生成Popmenu數(shù)據(jù)
Widget flowMenuItem(IconData icon) {
final double buttonDiameter =
MediaQuery.of(context).size.width * 2 / (menuItems.length * 3);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: RawMaterialButton(
fillColor: lastTapped == icon ? Colors.amber[700] : Colors.blue,
splashColor: Colors.amber[100],
shape: CircleBorder(),
constraints: BoxConstraints.tight(Size(buttonDiameter, buttonDiameter)),
onPressed: () {
_updateMenu(icon);
},
child: Icon(icon, color: Colors.white, size: 30.0),
),
);
}
@override
Widget build(BuildContext context) {
return Center(
child: Flow(
delegate: FlowMenuDelegate(animation: _ctrlAnimationPopMenu),
children: menuItems
.map<Widget>((IconData icon) => flowMenuItem(icon))
.toList(),
),
);
}
}
FlowMenuDelegate 定義如下:
class FlowMenuDelegate extends FlowDelegate {
FlowMenuDelegate({this.animation}) : super(repaint: animation);
final Animation<double> animation;
@override
void paintChildren(FlowPaintingContext context) {
double x = 50.0; //起始位置
double y = 50.0; //橫向展開,y不變
for (int i = 0; i < context.childCount; ++i) {
x = context.getChildSize(i).width * i * animation.value;
context.paintChild(
i,
transform: Matrix4.translationValues(x, y, 0),
);
}
}
@override
bool shouldRepaint(FlowMenuDelegate oldDelegate) =>
animation != oldDelegate.animation;
}
半圓菜單展開/收起
代碼如下:
import 'dart:math';
import 'package:flutter/material.dart';
class DemoFlowMenu extends StatefulWidget {
@override
_DemoFlowMenuState createState() => _DemoFlowMenuState();
}
class _DemoFlowMenuState extends State<DemoFlowMenu>
with TickerProviderStateMixin {
//動畫需要這個類來混合
//動畫變量,以及初始化和銷毀
AnimationController _ctrlAnimationCircle;
@override
void initState() {
super.initState();
_ctrlAnimationCircle = AnimationController(
//初始化動畫變量
lowerBound: 0,
upperBound: 80,
duration: Duration(milliseconds: 300),
vsync: this);
_ctrlAnimationCircle.addListener(() => setState(() {}));
}
@override
void dispose() {
_ctrlAnimationCircle.dispose(); //銷毀變量,釋放資源
super.dispose();
}
//生成Flow的數(shù)據(jù)
List<Widget> _buildFlowChildren() {
return List.generate(
5,
(index) => Container(
child: Icon(
index.isEven ? Icons.timer : Icons.ac_unit,
color: Colors.primaries[index % Colors.primaries.length],
),
));
}
@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
Positioned.fill(
child: Flow(
delegate: FlowAnimatedCircle(_ctrlAnimationCircle.value),
children: _buildFlowChildren(),
),
),
Positioned.fill(
child: IconButton(
icon: Icon(Icons.menu),
onPressed: () {
setState(() {
//點(diǎn)擊后讓動畫可前行或回退
_ctrlAnimationCircle.status == AnimationStatus.completed
? _ctrlAnimationCircle.reverse()
: _ctrlAnimationCircle.forward();
});
},
),
),
],
);
}
}
FlowAnimatedCircle 代碼如下:
class FlowAnimatedCircle extends FlowDelegate {
final double radius; //綁定半徑,讓圓動起來
FlowAnimatedCircle(this.radius);
@override
void paintChildren(FlowPaintingContext context) {
if (radius == 0) {
return;
}
double x = 0; //開始(0,0)在父組件的中心
double y = 0;
for (int i = 0; i < context.childCount; i++) {
x = radius * cos(i * pi / (context.childCount - 1)); //根據(jù)數(shù)學(xué)得出坐標(biāo)
y = radius * sin(i * pi / (context.childCount - 1)); //根據(jù)數(shù)學(xué)得出坐標(biāo)
context.paintChild(i, transform: Matrix4.translationValues(x, -y, 0));
} //使用Matrix定位每個子組件
}
@override
bool shouldRepaint(FlowDelegate oldDelegate) => true;
}
交流
老孟Flutter博客地址(330個控件用法):http://laomengit.com
歡迎加入Flutter交流群(微信:laomengit)繁堡、關(guān)注公眾號【老孟Flutter】: