Flutter 9种布局组件(带详细案例)
界面布局主要分为两大类:布局类组件和定位装饰权重组件,我们布局的时候基本都是相互嵌套的
- 1、布局类组件
- 1、线性布局组件
- 2、弹性布局组件
- 3、流式布局组件
- 4、叠加布局组件
- 2、定位装饰权重组件
- 1、Container
- 2、SizedBox
- 3、宽高低:AspectRatio
- 4、FittedBox&FractionallySizedBox&ContainedBox
- 5、Expanded、Flexible 和 Spacer
以下是这个 UI 的 widget 树形图:
我们这里讲由浅及深的聊一下Flutter布局问题
1、Container
Container是flutter中广泛使用的容器类组件
构造函数
Container({
this.alignment,
this.padding, //容器内补白,属于decoration的装饰范围
Color color, // 背景色
Decoration decoration, // 背景装饰
Decoration foregroundDecoration, //前景装饰
double width,//容器的宽度
double height, //容器的高度
BoxConstraints constraints, //容器大小的限制条件
this.margin,//容器外补白,不属于decoration的装饰范围
this.transform, //变换
this.child,
})
属性
- alignment:child对齐的属性,可以设置居中、居左、居右、居上、居下等等。
- padding:内边距。width和height包含padding值。
- color:背景颜色。
- decoration:设置装饰,比如边框、圆角、背景图片等。不能给Container同时设置decoration和color属性,如果要同时设置,给decoration设置color就可以。
- foregroundDecoration,设置前景装饰。
- width:宽度。
- height:高度。
- constraints:大小范围约束,constraints有四个属性:minWidth、minHeight、maxWidth、maxHeight。
- margin: 外边距。
- transform:变换效果,比如翻转、旋转、变形等。
- child:子组件,可以不设置。
Container自身尺寸的调节分两种情况:
- Container在没有子节点(children)的时候,会试图去变得足够大。除非constraints是unbounded限制,在这种情况下,Container会试图去变得足够小。
- 带子节点的Container,会根据子节点尺寸调节自身尺寸,但是Container构造器中如果包含了width、height以及constraints,则会按照构造器中的参数来进行尺寸的调节。
Container(
child: Text(
"Hello Flutter", // 文字内容
style: TextStyle(fontSize: 20.0,color: Colors.amber), // 字体样式 字体大小
),
alignment: Alignment.topLeft, // 字内容的对齐方式 center居中,centerLeft居中左侧 centerRight居中右侧
// bottomCenter 下居中对齐 ,bottomLeft 下左对齐,bottomRight 下右对齐
// topCenter 上居中对齐,topLeft 上左对齐,topRight 上右对齐
width: 200, // 宽
height: 200, // 高
color: Colors.red, //颜色 color和decoration不可以同时存在
padding: const EdgeInsets.fromLTRB(20.0,20.0,20.0,20.0), // 边距 all 包括上下左右 fromLTRB 上下左右分别设置边距fromLTRB(20.0,20.0,20.0,20.0)
margin: const EdgeInsets.all(30.0), // 外间距
);
Decoration(装饰器)
decoration的属性很强大,可以支持背景图线性或者径向的渐变,边框,圆角,阴影等属性
Flutter的Decoration可以设置:背景色 背景图 边框 圆角 阴影 渐变色 的等属性,Decoration 是基类,它的子类有下面这些
- BoxDecoration:实现边框、圆角、阴影、形状、渐变、背景图像
- ShapeDecoration:实现四边分别指定颜色和宽度、底部线、矩形边色、圆形边色、体育场(竖向椭圆)、 角形(八边角)边色
- FlutterLogoDecoration:Flutter图片
- UnderlineTabindicator:下划线
const BoxDecoration({
this.color,//背景色
this.image,//图片
this.border,//描边
this.borderRadius,//圆角大小
this.boxShadow,//阴影
this.gradient,//渐变色
this.backgroundBlendMode,//图像混合模式
this.shape = BoxShape.rectangle,//形状,BoxShape.circle和borderRadius不能同时使用
})
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
margin: EdgeInsets.only(top: 50.0, left: 120.0), //容器外填充
constraints: BoxConstraints.tightFor(width: 200.0, height: 150.0), //卡片大小
decoration: BoxDecoration(//背景装饰
gradient: RadialGradient( //背景径向渐变
colors: [Colors.red, Colors.orange],
center: Alignment.topLeft,
radius: .98
),
boxShadow: [ //卡片阴影
BoxShadow(
color: Colors.black54,
offset: Offset(2.0, 2.0),
blurRadius: 4.0
)
]
),
transform: Matrix4.rotationZ(.2), //卡片倾斜变换
alignment: Alignment.center, //卡片内文字居中
child: Text( //卡片文字
"Flutter Demo",
style: TextStyle(color: Colors.white, fontSize: 40.0),
),
);
}
}
2、SizedBox
SizedBox: 两种用法:一是可用来设置两个widget之间的间距,二是可以用来限制子组件的大小。
Column(
children: <Widget>[
SizedBox(height: 30,),
SizedBox(width: 200,height: 200,
child: Container(
width: 200,
height: 200,
color: Colors.red,
),
),
SizedBox(height: 30,),
SizedBox(width: 100,height: 100,
child: Container(
width: 200,
height: 200,
color: Colors.red,
),
)
],
);
可以看到,代码中共有4个SizedBox组件,两个是设置间距的功能,两个是具有设置约束的功能
3、线性布局
所谓线性布局,即指沿水平或垂直方向排布子组件。Flutter中通过Row和Column来实现线性布局。Row和Column都继承自Flex,我们将在弹性布局一节中详细介绍Flex。
- Row 是将子组件以水平方式布局的组件,
- Column 是将子组件以垂直方式布局的组件
项目中 90% 的页面布局都可以通过 Row 和 Column 来实现。
1、row
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,
),
],
);
2、column
Column(
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、主轴( MainAxis ) 和 交叉轴( CrossAxis )
在 Row 和 Column 中有一个非常重要的概念:主轴( MainAxis ) 和 交叉轴( CrossAxis ),主轴就是与组件布局方向一致的轴,交叉轴就是与主轴方向垂直的轴。
具体到 Row 组件,主轴 是水平方向,交叉轴 是垂直方向。而 Column 与 Row 正好相反,主轴 是 垂直方向,交叉轴 是水平方向。
明白了 主轴 和 交叉轴 概念,我们来看下 mainAxisAlignment 属性,此属性表示主轴方向的对齐方式,默认值为 start,表示从组件的开始处布局,此处的开始位置和 textDirection 属性有关,textDirection 表示文本的布局方向,其值包括 ltr(从左到右) 和 rtl(从右到左),当 textDirection = ltr 时,start 表示左侧,当 textDirection = rtl 时,start 表示右侧,
spaceAround 和 spaceEvenly 区别是:
- spaceAround :第一个子控件距开始位置和最后一个子控件距结尾位置是其他子控件间距的一半。
- spaceEvenly : 所有间距一样。
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,
),
],
),
)
4、弹性布局(Flex)
Row与Column是继承自Flex的,Flex的大部分功能都在上一个线性布局中介绍过了。我们这里在补充几个知识点。
- 1、Flexible:Flexible是一个控制Row、Column、Flex等子组件如何布局的组件。
- 2、Expanded:Expanded 继承字 Flexible,fit 参数固定为 FlexFit.tight,也就是说 Expanded 必须(强制)填满剩余空间
- 3、Spacer:Spacer 的本质也是 Expanded 的实现的,和Expanded的区别是:Expanded 可以设置子控件,而 Spacer 的子控件尺寸是0,因此Spacer适用于撑开 Row、Column、Flex 的子控件的空隙
1、Flexible
Flexible 组件可以控制 Row、Column、Flex 的子控件占满父组件,比如,Row 中有3个子组件,两边的宽是100,中间的占满剩余的空间
Row(
children: <Widget>[
Container(
color: Colors.blue,
height: 50,
width: 100,
),
Flexible(
child: Container(
color: Colors.red,
height: 50,
)
),
Container(
color: Colors.blue,
height: 50,
width: 100,
),
],
)
还是有3个子组件,第一个占1/6,第二个占2/6,第三个占3/6,代码如下
Column(
children: <Widget>[
Flexible(
flex: 1,
child: Container(
color: Colors.blue,
alignment: Alignment.center,
child: Text('1 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
),
),
Flexible(
flex: 2,
child: Container(
color: Colors.red,
alignment: Alignment.center,
child: Text('2 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
),
),
Flexible(
flex: 3,
child: Container(
color: Colors.green,
alignment: Alignment.center,
child: Text('3 Flex/ 6 Total',style: TextStyle(color: Colors.white),),
),
),
],
)
子组件占比 = 当前子控件 flex / 所有子组件 flex 之和。 Flexible中 fit 参数表示填满剩余空间的方式,说明如下:
- tight:必须(强制)填满剩余空间。
- loose:尽可能大的填满剩余空间,但是可以不填满。
这2个看上去不是很好理解啊,什么叫尽可能大的填满剩余空间?什么时候填满,看下面的例子
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Container(
color: Colors.blue,
height: 50,
width: 100,
),
Flexible(
child: Container(
color: Colors.red,
height: 50,
child: Text('Container',style: TextStyle(color: Colors.white),),
)
),
Container(
color: Colors.blue,
height: 50,
width: 100,
),
],
);
}
}
这段代码是在最上面代码的基础上给中间的红色Container添加了Text子控件,此时红色Container就不在充满空间,再给Container添加对齐方式,代码如下:
class MyWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Row(
children: <Widget>[
Container(
color: Colors.blue,
height: 50,
width: 100,
),
Flexible(
child: Container(
color: Colors.red,
height: 50,
alignment: Alignment.center,
child: Text('Container',style: TextStyle(color: Colors.white),),
)
),
Container(
color: Colors.blue,
height: 50,
width: 100,
),
],
);
}
}
2、Expanded
class Expanded extends Flexible {
/// Creates a widget that expands a child of a [Row], [Column], or [Flex]
/// so that the child fills the available space along the flex widget's
/// main axis.
const Expanded({
Key key,
int flex = 1,
@required Widget child,
}) : super(key: key, flex: flex, fit: FlexFit.tight, child: child);
}
Expanded 继承字 Flexible,fit 参数固定为 FlexFit.tight,也就是说 Expanded 必须(强制)填满剩余空间
3、Spacer
Spacer 也是一个权重组件,源代码如下:
@override
Widget build(BuildContext context) {
return Expanded(
flex: flex,
child: const SizedBox.shrink(),
);
}
Spacer 的本质也是 Expanded 的实现的,和Expanded的区别是:Expanded 可以设置子控件,而 Spacer 的子控件尺寸是0,因此Spacer适用于撑开 Row、Column、Flex 的子控件的空隙,用法如下
Row(
children: <Widget>[
Container(width: 100,height: 50,color: Colors.green,),
Spacer(flex: 2,),
Container(width: 100,height: 50,color: Colors.blue,),
Spacer(),
Container(width: 100,height: 50,color: Colors.red,),
],
)
三个权重组建总结如下:
- Spacer 是通过 Expanded 实现的,Expanded继承自Flexible。
- 填满剩余空间直接使用Expanded更方便。
- Spacer 用于撑开 Row、Column、Flex 的子组件的空隙。
4、流式布局(Wrap)
Wrap 为子组件进行水平或者垂直方向布局,且当空间用完时,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'),
);
}),
)
我们查看Wrap源码,我们发现了一个runAlignment属性,感觉和alignment好像一样。
runAlignment 属性控制 Wrap 的交叉抽方向上每一行的对齐方式,下面直接看 runAlignment 6中方式对应的效果图
runAlignment 和 alignment 的区别:
- alignment :是主轴方向上对齐方式,作用于每一行。
- runAlignment :是交叉轴方向上将每一行看作一个整体的对齐方式。
spacing 和 runSpacing 属性控制Wrap主轴方向和交叉轴方向子控件之间的间隙
Wrap(
spacing: 30,
runSpacing: 10,
children: List.generate(10, (i) {
double w = 50.0 + 10 * i;
return Container(
color: Colors.primaries[i],
height: 50,
width: w,
alignment: Alignment.center,
child: Text('$i'),
);
}),
)
5、叠加布局(Stack)
叠加布局组件包含 Stack 和 IndexedStack,Stack 组件将子组件叠加显示,根据子组件的顺利依次向上叠加
Stack({
Key key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
- alignment : 指的是子Widget的对其方式,默认情况是以左上角为开始点 ,这个属性是最难理解的,它区分为使用了Positioned和未使用Positioned定义两种情况,没有使用Positioned情况还是比较好理解的,下面会详细讲解的
- fit :用来决定没有Positioned方式时候子Widget的大小,StackFit.loose 指的是子Widget 多大就多大,StackFit.expand使子Widget的大小和父组件一样大
- overflow :指子Widget 超出Stack时候如何显示,默认值是Overflow.clip,子Widget超出Stack会被截断,Overflow.visible超出部分还会显示的
Stack(
children: <Widget>[
Container(
height: 300,
width: 300,
color: Colors.red,
),
Container(
height: 200,
width: 200,
color: Colors.blue,
),
Container(
height: 100,
width: 100,
color: Colors.yellow,
)
],
);
IndexedStack
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: Text('1'),
),
),
Center(
child: Container(
height: 300,
width: 300,
color: Colors.green,
alignment: Alignment.center,
child: Text('2'),
),
),
Center(
child: Container(
height: 300,
width: 300,
color: Colors.yellow,
alignment: Alignment.center,
child: Text('3'),
),
),
],
);
}
_buildRow() {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RaisedButton(
child: Text('1'),
onPressed: (){
setState(() {
_index = 0;
});
},
),
RaisedButton(
child: Text('2'),
onPressed: (){
setState(() {
_index = 1;
});
},
),
RaisedButton(
child: Text('3'),
onPressed: (){
setState(() {
_index = 2;
});
},
),
],
);
}
}
6、宽高比(AspectRatio)
AspectRatio 是固定宽高比的组件
Container(
height: 300,
width: 300,
color: Colors.blue,
alignment: Alignment.center,
child: AspectRatio(
aspectRatio: 2 / 1,
child: Container(color: Colors.red,),
),
)
7、布局约束
1、FittedBox(缩放布局)
FittedBox 组件主要做两件事,缩放(Scale)和位置调整(Position)。
FittedBox 会在自己的尺寸范围内缩放并调整 child 的位置,使 child 适合其尺寸。FittedBox 和 Android 中的 ImageView 有些类似,将图片在其范围内按照规则进行缩放和位置调整。
布局分为两种情况:
- 外部有约束的话,按照外部约束调整自身尺寸,然后缩放调整 child ,按照指定条件进行布局。
- 没有外部约束条件,则跟 child 尺寸一样,指定的缩放和位置属性将不起作用。
const FittedBox({
Key key,
this.fit = BoxFit.contain,
this.alignment = Alignment.center,
this.clipBehavior = Clip.hardEdge,
Widget child,
})
这里有一个新的属性fit
- 1、BoxFit.none,没有任何填充模式
- 2、BoxFit.fill:不按宽高比例填充,内容不会超过容器范围
- 3、BoxFit.contain:按照宽高比等比模式填充,内容不会超过容器范围
- 4、BoxFit.cover:按照原始尺寸填充整个容器模式。内容可能回超过容器范围
- 5、BoxFit.scaleDown:会根据情况缩小范围
Container(
color: Colors.amberAccent,
width: 300.0,
height: 300.0,
child: FittedBox(
fit: BoxFit.none,
alignment: Alignment.topLeft,
child: new Container(
color: Colors.red,
child: new Text("FittedBox"),
)
),
);
2、FractionallySizedBox
FractionallySizedBox 是一个相对父组件尺寸的组件,用途是基于宽度缩放因子和高度缩放因子来调整布局大小,大小可能超过父组件位置。
const FractionallySizedBox({
Key key,
this.alignment = Alignment.center,
this.widthFactor,
this.heightFactor,
Widget child,
})
-
1.widthFactor:FractionallySizedBox组件的宽度因子
- widthFactor != null时,子组件的宽度等于FractionallySizedBox组件的约束的最大宽度乘以widthFractor
- widthFractor == null时,FractionallySizedBox组件的宽度约束会原封不动的传递给子组件
-
2.heightFractor: FractionallySizedBox组件的高度因子
- heightFractor != null时,子组件的高度等于FractionallySizedBox组件的约束的最大高度乘以heightFractor
- heightFractor == null时,FractionallySizedBox组件的高度约束会原封不动的传递给子组件
Container(
height: 200,
width: 200,
color: Colors.blue,
child: FractionallySizedBox(
widthFactor: .8,
heightFactor: .3,
child: Container(
color: Colors.red,
),
),
)
3、ContainedBox
ContainedBox是一种有约束限制的布局,在其约定的范围内,比如最大高度,最小宽度,其子组件是不能逾越的
ConstrainedBox({
Key key,
@required this.constraints,
Widget child,
})
constraints:添加到child上的额外限制条件,其类型为BoxConstraints。BoxConstraints的作用是干啥的呢?其实很简单,就是限制各种最大最小宽高。说到这里插一句,double.infinity在widget布局的时候是合法的,也就说,例如想最大的扩展宽度,可以将宽度值设为double.infinity。
8、案例
这个案例来自:flutter.cn/docs/develo…
我们准备做一下这个界面
具体代码如下
class demoWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 300,
child: Card(
color: Colors.white,
margin: EdgeInsets.fromLTRB(10, 0, 10, 0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
topWidget(),
starWidget(),
iconWidget()],
),
)
);
}
}
class topWidget extends StatelessWidget {
final mainImage = Container(
margin: EdgeInsets.fromLTRB(10, 10, 0, 0),
child: Image.asset(
'images/pavlova.jpg',
fit: BoxFit.fill,
width: 130,
height: 130,
),
);
@override
Widget build(BuildContext context) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
mainImage,
Flexible(
child: rightWidget()
),
],
);
}
}
class rightWidget extends StatelessWidget {
final titleText = Container(
padding: EdgeInsets.fromLTRB(10, 15, 0, 0),
child: Text(
'Strawberry Pavlova',
style: TextStyle(
letterSpacing: 0.5,
fontSize: 17,
),
),
);
final subTitle = Container(
padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
child: Text(
'Pavlova is a meringue-based dessert named after the Russian ballerina '
'Anna Pavlova. Pavlova features a crisp crust and soft, light inside, '
'topped with fruit and whipped cream.',
style: TextStyle(
fontSize: 12,
),
),
);
@override
Widget build(BuildContext context) {
return Container(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [titleText,subTitle],
),
);
}
}
class starWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.fromLTRB(10, 10, 10, 10),
child: Row(
children: [
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.green[500]),
Icon(Icons.star, color: Colors.black),
Icon(Icons.star, color: Colors.black),
],
),
);
}
}
class iconWidget extends StatelessWidget {
final descTextStyle = TextStyle(
color: Colors.black,
fontSize: 18,
height: 1.2,
);
@override
Widget build(BuildContext context) {
return Container(
child: Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Icon(Icons.kitchen, color: Colors.green[500]),
Text('PREP:',style: descTextStyle),
Text('25 min',style: descTextStyle),
],
),
Column(
children: [
Icon(Icons.timer, color: Colors.green[500]),
Text('COOK:',style: descTextStyle),
Text('1 hr',style: descTextStyle),
],
),
Column(
children: [
Icon(Icons.restaurant, color: Colors.green[500]),
Text('FEEDS:',style: descTextStyle),
Text('4-6',style: descTextStyle),
],
),
],
),
),
);
}
}
转载自:https://juejin.cn/post/6919653632468221966