基础组件大多数都是一个个单个的控件,控件在View树中处于叶子节点,单个的控件对我们UI的搭建显然是不够的,还需要布局来摆放它们,今天介绍布局与容器,它们其实都算是布局,只不过容器的自定义性比布局低一些,上次的Chip
也算是一种容器,它包含了一组摆放固定位置的子控件
文章目录
一、布局
布局是和基础组件(控件)同样重要的组件,它们能帮助我们规定如何摆放其他组件,Flutter中固定布局都使用的盒子模型,后续会在约束布局中讲到Box
类布局
1.线性布局(Row、Column)
Row
和Column
为一对线性布局组件,Row
对应横向摆放组件,Column
对应纵向摆放组件,我们只需要对其中一个熟悉后,另一个自然就会使用了,下面以Column
为例子
Colum
构造如下:
Column({
super.key,
super.mainAxisAlignment,// 横向子组件对齐方式
super.mainAxisSize,// 横向宽度方式,同match_parent wrap_content
super.crossAxisAlignment,// 纵向子组件对齐方式
super.textDirection,// 子组件文本方向
super.verticalDirection,// 子组件纵向摆放方向
super.textBaseline,// 对齐基于哪种文本基线
super.children,// 子组件集合
}
下面组合使用Row
和Column
:
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,// 水平均分
crossAxisAlignment: CrossAxisAlignment.center,// 垂直居中
children: [
Column(
mainAxisSize: MainAxisSize.min, //Column的宽度按子组件最小宽度
children: const [
Text("column hi"),
ElevatedButton(onPressed: null, child: Text("click me")),
Icon(Icons.h_mobiledata),
],
),
const Text("row hi"),
],
);
效果:
2.弹性布局(Flex)
弹性布局Flex
支持按自定义比例来分配布局控件,在安卓中类似于layout_weight
,Row
和Column
都继承至Flex
,所以线性布局本身也支持比例摆放
如果要按比例摆放,还需要使用子组件Expanded
,Expanded
只能用在Flex
中,其flex
属性用来定义比例大小
Flex(
direction: Axis.horizontal,
children: [
Expanded(
flex: 1,
child: TextField(
decoration: InputDecoration(
labelText: "hi1",
hintText: "input1",
),
),
),
Expanded(
flex: 2,
child: TextField(
decoration: InputDecoration(
labelText: "hi2",
hintText: "input2",
),
),
),
],
);
效果:
3.流式布局(Wrap)
有时我们想水平摆放一些子组件,但希望快要超出屏幕时之后的子组件能够自动换行,这就是流式布局
Wrap
是Flutter中的流式布局,构造如下:
Wrap({
super.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,
this.clipBehavior = Clip.none,
super.children,
})
使用:
Wrap(
alignment: WrapAlignment.center,
spacing: 10,
runAlignment: WrapAlignment.center,
runSpacing: 20,
children: [
Chip(avatar: Text("R"), label: Text("Row")),
Chip(avatar: Text("C"), label: Text("Column")),
Chip(avatar: Text("F"), label: Text("Flex")),
Chip(avatar: Text("E"), label: Text("ElevatedButton")),
Chip(avatar: Text("I"), label: Text("Image")),
Chip(avatar: Text("S"), label: Text("Switch")),
Chip(avatar: Text("T"), label: Text("TextField")),
],
);
效果:
4.约束容器
这边说的约束为对子组件添加一些约束,不要和安卓中的约束布局搞混,简单来说就是使宽高固定的一个容器,这些约束会传递给子组件,并保持其约束条件,容器和布局的区别:布局内可以有任意多个子组件,而容器内为一个或固定几个子组件。由于下面我们要对约束有一个概念,所以在布局中我们先把约束容器介绍完
4.1 ConstrainedBox
ConstrainedBox
必传一个constraints
参数作为约束条件,类型为BoxConstraints
,BoxConstraints
构造如下:
const BoxConstraints({
this.minWidth = 0.0,// 最小宽度
this.maxWidth = double.infinity,// 最大宽度
this.minHeight = 0.0,// 最小高度
this.maxHeight = double.infinity,// 最大高度
})
此外它还有命名式构造:最大约束BoxConstraints.tight
、最小约束BoxConstraints.loose
等,约束会对子组件其效果,如我们约束一个最大宽度和最大高度为50
,子组件利用文本量很多的一个Text
ConstrainedBox(
constraints: const BoxConstraints(maxHeight: 50, maxWidth: 50),
child: Text(
"hi," * 100,
),
);
效果可以看到Text
的宽高被约束在了50*50的区域:
4.2 DecoratedBox
DecoratedBox
必传一个Decoration
(抽象类),可以修饰边框,形状,颜色等等,Decoration
常用的实现类BoxDecoration
构造如下:
const BoxDecoration({
this.color,
this.image,
this.border,
this.borderRadius,
this.boxShadow,
this.gradient,
this.backgroundBlendMode,
this.shape = BoxShape.rectangle,
})
使用:
ConstrainedBox(
constraints: BoxConstraints.tight(Size(50, 50)),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.blue),
child: Text("hi"),
),
);
效果:
4.3 SizedBox
SizedBox
为固定宽高的布局,实际上就是ConstrainedBox
的变种
SizedBox(
width: 50,
height: 50,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.cyan),
),
);
效果:
5.层叠布局(Stack)
5.1 Stack
Stack
为子组件可以重叠的布局,Positioned
是专门为其设计的子组件,Positioned
可以固定在Stack
布局的任意位置,构造如下:
const Positioned({
super.key,
this.left,// 距离Stack左边的距离
this.top,// 距离Stack顶边的距离
this.right,// 距离Stack右边的距离
this.bottom,// 距离Stack底边的距离
this.width,// 固定宽
this.height,// 固定高
required super.child,
})
Stack
构造如下:
Stack({
super.key,
this.alignment = AlignmentDirectional.topStart,// 非Positioned子组件的位置
this.textDirection,
this.fit = StackFit.loose,// 非Positioned子组件如何去适应Stack的大小。StackFit.loose:子组件的大小,StackFit.expand:扩伸到Stack的大小。
this.clipBehavior = Clip.hardEdge,// 超出部分裁剪方式
super.children,
})
我们在四个角和中央上分别固定上文本:
SizedBox(
width: 200,
height: 200,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.amberAccent),
child: Stack(
alignment: Alignment.center,
children: [
Positioned(
left: 0,
top: 0,
child: Text("hi"),
),
Positioned(
right: 0,
top: 0,
child: Text("hi"),
),
Positioned(
left: 0,
bottom: 0,
child: Text("hi"),
),
Positioned(
right: 0,
bottom: 0,
child: Text("hi"),
),
Text("hi"),
],
),
),
);
效果:
5.2 IndexedStack
IndexedStack
只会显示一个子组件,我们可以通过index
参数来选择性的显示哪个子组件
IndexedStack(
index: 2,
children: [
Text("hi1"),
Text("hi2"),
Text("hi3"),
],
);
效果:
6.表格布局(Table)
Table
为表格布局,需要和子组件TableRow
行元素配合使用,TableRow
又和TableCell
单元格配合使用用于单元格对齐方式
SizedBox(
width: 400,
child: Table(
columnWidths: const <int, TableColumnWidth>{
0: IntrinsicColumnWidth(), // 自适应宽度
1: FlexColumnWidth(), // 按比例宽度,默认权重为1
2: FixedColumnWidth(64), // 按固定64的宽度
}, // 定义单元格宽
border: TableBorder.all(),
defaultVerticalAlignment:
TableCellVerticalAlignment.middle, // 单元格内容居中
children: [
TableRow(
children: [
Container(
height: 32,
width: 50,
color: Colors.green,
),
TableCell(
verticalAlignment: TableCellVerticalAlignment.bottom,
child: Container(
height: 32,
width: 32,
color: Colors.red,
)),
Container(
height: 64,
color: Colors.blue,
),
],
),
],
),
);
效果:
7.LayoutBuilder
LayoutBuilder
可以拿到父布局的约束信息,通过约束信息做不同的处理,比如根据不同的宽度显示不同布局
下面根据宽度是否大于600来显示不同大小的Box:
LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
if (constraints.maxWidth > 600) {
return SizedBox(
width: 300,
height: 100,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
),
);
} else {
return SizedBox(
width: 100,
height: 100,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.red),
),
);
}
});
效果:
二、容器
上面提到容器为一个或固定几个子组件的布局,一般就是为了给子组件一些约束
1.边距(Padding)
有时我们希望组件距离别的组件或屏幕边缘有个边距,那么我们可以使用Padding
为其设置上下左右的外边距,边距padding
参数为单独的抽象类EdgeInsetsGeometry
,我们通常使用其实现类EdgeInsets
,含有多种方式的命名式构造:EdgeInsets.fromLTRB
、EdgeInsets.all
、**EdgeInsets.only
**等,其使用也比较简单:
Padding(
padding: EdgeInsets.all(20),// 上下左右各20的边距
child: Text("hi"),
);
效果:
2.相对定位(Align)
有时我们仅仅有一个组件需要相对父组件进行定位,这时就使用Stack
比较复杂,而Align
满足这种需要,在使用上它足够简单,我们之前使用过的Center
组件就是继承至Align
,其构造如下:
const Align({
super.key,
this.alignment = Alignment.center,// 相对父组件的位置
this.widthFactor,// 宽度,widthFactor*子组件宽
this.heightFactor,// 高度,heightFactor*子组件高
super.child,
})
使用:
SizedBox(
width: 200,
height: 200,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.amberAccent),
child: Align(
alignment: Alignment.bottomCenter,
child: Text("hi"),
),
),
);
效果:
2.1 Alignment
Flutter中Alignment
是一个类,继承至抽象类AlignmentGeometry
,它是以中心点为(0,0)起始点,的一个x,y坐标系,比较特殊的是x轴箭头朝右,y轴箭头朝下。除了Alignment
外,还有对齐方式AlignmentDirectional
也是相同的,所以我们也可以方便的自定义位置
SizedBox(
width: 200,
height: 200,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.amberAccent),
child: Align(
alignment: Alignment(1.0, 1.0),
child: Text("hi"),
),
),
);
我在图上标明了坐标轴:
2.2 FractionalOffset
FractionalOffset
是以左上角为(0,0)起始点,x向左,y向下的坐标轴。和安卓中View的坐标系相同,使用FractionalOffset
居中的代码如下:
SizedBox(
width: 200,
height: 200,
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.amberAccent),
child: Align(
alignment: FractionalOffset(0.5, 0.5),
child: Text("hi"),
),
),
);
效果:
3.变换(Transform)
Transform
通过矩阵变换,可以使子组件达到各种变换效果,Flutter内置Matrix4
类,用于各种变换,这些效果只是达到视觉的效果,实际子组件的位置和大小并不会发生变化
Padding(
padding: EdgeInsets.all(100),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.black),
child: Transform(
alignment: Alignment.topRight,
transform: Matrix4.skewY(0.3)..rotateZ(-pi / 12.0),
child: Container(
padding: const EdgeInsets.all(8.0),
color: const Color(0xFFE8581C),
child: const Text('Apartment for rent!'),
),
),
),
);
效果:
3.1 平移(Transform.translate)
除了手动指定矩阵变换外,Transform
还提供了常用的几种变换效果的命名式构造,**Transform.translate
**是平移效果的构造:
Padding(
padding: EdgeInsets.all(100),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.black),
child: Transform.translate(
offset: Offset(20, 20),
child: Container(
padding: const EdgeInsets.all(8.0),
color: const Color(0xFFE8581C),
child: const Text('Apartment for rent!'),
),
),
),
);
效果:
3.2 缩放(Transform.scale)
**Transform.scale
**为缩放的变换效果,参数scale
为等比例缩放,也可以通过指定不同的scaleX
和scaleY
来实现水平垂直不同比例缩放:
Padding(
padding: EdgeInsets.all(100),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.black),
child: Transform.scale(
scaleX: 2,
scaleY: 3,
child: Container(
padding: const EdgeInsets.all(8.0),
color: const Color(0xFFE8581C),
child: const Text('Apartment for rent!'),
),
),
),
);
效果:
3.3 旋转(Transform.rotate)
**Transform.rotate
**需要传入一个弧度值:
Padding(
padding: EdgeInsets.all(100),
child: DecoratedBox(
decoration: BoxDecoration(color: Colors.black),
child: Transform.rotate(
angle: pi/2,
child: Container(
padding: const EdgeInsets.all(8.0),
color: const Color(0xFFE8581C),
child: const Text('Apartment for rent!'),
),
),
),
);
效果:
4.容器(Container)
Container
为一个组合了ConstrainedBox
、DecorateBox
、Transform
、Padding
、Align
的容器组件,之前我们定义一个有颜色的固定大小的Box,需要将SizedBox
和DecorateBox
组合使用,Container
帮我们进行了封装后,就只需要使用Container
就可以实现,其构造如下
Container({
super.key,
this.alignment,// 子组件对齐方式
this.padding,// padding 内边距
this.color,// 容器颜色
this.decoration,// 容器背景修饰
this.foregroundDecoration,// 前景修饰
double? width,// 固定宽度
double? height,// 固定高度
BoxConstraints? constraints,// 传递约束
this.margin,// 外边距
this.transform,// transform 变换效果
this.transformAlignment,// 变换时原点相对于容器位置,默认为容器左上角
this.child,
this.clipBehavior = Clip.none,
})
简单使用:
Container(
width: 100,
height: 30,
color: Colors.red,
padding: EdgeInsets.all(100),
transform: Transform
.rotate(angle: pi / 2)
.transform,
transformAlignment: Alignment.center,
);
效果:
5.其他Box类型容器
除了上面介绍的ConstrainedBox
、DecorateBox
、SizedBox
,还有几个Box
类型的容器
5.1 自适应容器(FittedBox)
FittedBox
允许子组件打破约束,和Transform
一样,组件的实际大小并不会发生变化,仅仅是视觉上的改变。通过fit
参数指定BoxFit
模式,来达到不同的效果,默认等比例缩放到合适的宽高后,显示在父容器中
下面我们固定一个50*50的Box,使用FittedBox
的子组件却是60*70的大小:
Container(
width: 50,
height: 50,
margin: EdgeInsets.all(20),
color: Colors.red,
child: FittedBox(
child: Container(
width: 60,
height: 70,
color: Colors.amber,
),
),
);
效果:
我们将模式改为BoxFit.none
,该模式会无视父组件传递的约束:
Container(
width: 50,
height: 50,
margin: EdgeInsets.all(20),
color: Colors.red,
child: FittedBox(
fit: BoxFit.none,
child: Container(
width: 60,
height: 70,
color: Colors.amber,
),
),
);
效果:
5.2 权重容器(FractionallySizedBox)
FractionallySizedBox
可以指定宽和高占整个父容器的比例
Container(
width: 300,
height: 300,
color: Colors.black12,
child: FractionallySizedBox(
widthFactor: 0.5,
heightFactor: 0.5,
alignment: FractionalOffset.center,
child: DecoratedBox(
decoration: BoxDecoration(
border: Border.all(
color: Colors.blue,
width: 4,
),
),
),
),
);
效果:
5.3 旋转容器(RotatedBox)
RotatedBox
就是一个带旋转的Box,参数quarterTurns
表示子组件应该顺时针旋转四分之一圈的次数
RotatedBox(
quarterTurns: 1,
child: Container(
width: 100,
height: 50,
color: Colors.red,
),
);
效果:
5.4 重置约束容器(OverflowBox)
OverflowBox
可以将父容器的约束取消,重新设置子组件的约束:
Container(
width: 130,
height: 130,
color: Colors.black12,
child: OverflowBox(
maxWidth: 150,
maxHeight: 110,
minWidth: 150,
minHeight: 110,
child: Container(
color: Colors.amber,
),
),
);
效果:
6.隐藏容器(Offstage)
Offstage
是一个可以隐藏子组件的容器,但组件的位置还是在的,通过offstage
参数来是否显示子组件
return Column(
children: [
Container(
color: Colors.black12,
width: 100,
height: 100,
),
Expanded(
child: Offstage(
child: Container(
color: Colors.amber,
width: 100,
),
),
),
Container(
color: Colors.black12,
width: 100,
height: 100,
),
],
);
效果:
7.骨架(Scaffold)
Scaffold
定义了一个APP显示的骨架,它内置了一些固定的槽位(位置)来摆放特定的组件,构造如下:
const Scaffold({
super.key,
this.appBar,// 顶部导航
this.body,// 主体
this.floatingActionButton,// FAB
this.floatingActionButtonLocation,// fab位置
this.floatingActionButtonAnimator,// fab位置改变时的移动动画
this.persistentFooterButtons,// 底部按钮组
this.persistentFooterAlignment = AlignmentDirectional.centerEnd,// 底部按钮组对齐方式
this.drawer,// 抽屉栏
this.onDrawerChanged,// 抽屉展开和收起的监听
this.endDrawer,// 反向的抽屉
this.onEndDrawerChanged,
this.bottomNavigationBar,// 底部导航栏
this.bottomSheet,// 底部组件
this.backgroundColor,// 背景
this.resizeToAvoidBottomInset,// 键盘弹出时,是否调整组件位置
this.primary = true,// 是否显示在屏幕顶层
this.drawerDragStartBehavior = DragStartBehavior.start,
this.extendBody = false,// 主体是否延伸到底部 默认不延伸
this.extendBodyBehindAppBar = false,// 主体是否延伸到appbar的顶部,默认不延伸
this.drawerScrimColor,// 当抽屉打开时,用于遮盖主要内容的幕布颜色
this.drawerEdgeDragWidth,// 水平滑动多少距离打开抽屉
this.drawerEnableOpenDragGesture = true,// 是否支持拖拽手势打开抽屉
this.endDrawerEnableOpenDragGesture = true,// 是否支持拖拽手势打开反向抽屉
this.restorationId,
})
下面是使用一些槽位的Scaffold
:
// 主函数
void main() {
// 调用runApp 构建app
runApp(
MaterialApp(
title: 'Flutter Tutorial',
home: Scaffold(
appBar: AppBar(// 导航栏
title: Text("hi"),
actions: <Widget>[
//导航栏右侧菜单
IconButton(icon: Icon(Icons.settings), onPressed: () {}),
],
),
floatingActionButton: FloatingActionButton(
// fab
onPressed: () {},
child: Icon(Icons.add, semanticLabel: "add"),
),
drawer: MyDrawer(), // 抽屉栏
bottomNavigationBar: MyBottomNavigationBar(),
),
),
);
}
效果:
抽屉栏和底部导航的代码如下:
// 抽屉栏
class MyDrawer extends StatelessWidget {
const MyDrawer({super.key});
Widget build(BuildContext context) {
return Drawer(
child: Text("hi"),
);
}
}
// 底部导航
class MyBottomNavigationBar extends StatefulWidget {
const MyBottomNavigationBar({super.key});
State<StatefulWidget> createState() {
return _MyBottomNavigationBar();
}
}
class _MyBottomNavigationBar extends State<MyBottomNavigationBar> {
int _selectedIndex = 0;
Widget build(BuildContext context) {
return BottomNavigationBar(
// 底部导航
items: <BottomNavigationBarItem>[
BottomNavigationBarItem(icon: Icon(Icons.home), label: "Home"),
BottomNavigationBarItem(icon: Icon(Icons.business), label: "Business"),
BottomNavigationBarItem(icon: Icon(Icons.school), label: "School"),
],
currentIndex: _selectedIndex,
fixedColor: Colors.blue,
onTap: (index) {
setState(() {
_selectedIndex = index;
});
},
);
}
}