1.单子布局

单子布局组件的含义是其只有一个子组件,可以通过设置一些属性设置该子组件所在的位置信息等。
比较常用的单子布局组件有:Align、Center、Padding、Container。

1.1 Align

1
2
3
4
5
6
7
const Align({
super.key,
this.alignment = Alignment.center, // 对齐方式
this.widthFactor, // 宽度因子,不设置的话尽可能大
this.heightFactor, //高度因子,不设置的话尽可以大
super.child, // 子widget
})
  • 因为子组件在父组件中的对齐方式必须有一个前提,就是父组件得知道自己的范围(宽度和高度);
  • 如果widthFactorheightFactor不设置,那么默认Align会尽可能的大(尽可能占据自己所在的父组件);
  • 我们也可以对他们进行设置,比如widthFactor设置为3,那么相对于Align的宽度是子组件跨度的3倍;

简单操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
return Container(
color: Colors.amber,
width: 100,
height: 100,
child: const Align(
alignment: Alignment.bottomRight,
widthFactor: 3,
heightFactor: 3,
child: Icon(
Icons.add_a_photo,
size: 36,
color: Colors.red,
),
),
);
}
}

image.png|500

1.2 Center

1
2
3
4
5
6
7
8
9
class Center extends Align {
/// Creates a widget that centers its child.
const Center({
super.key,
super.widthFactor,
super.heightFactor,
super.child
});
}
  • Center组件继承自Align
  • alignment设置为Alignment.center

简单操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
return Container(
color: Colors.amber,
width: 100,
height: 100,
child: const Center(
widthFactor: 3,
heightFactor: 3,
child: Icon(
Icons.add_a_photo,
size: 36,
color: Colors.red,
),
),
);
}
}

image.png|500

1.3 Padding

Padding 不是一个简单的属性,在这也是一个Widget,有点像是web中的内边距,但是没有margin这种widget,因为也可以使用Padding替代,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
return Container(
color: Colors.amber,
child: const Padding(
padding: EdgeInsets.all(20),
child: Text(
'测试,测试,测试,测试,测试,测试,测试,测试,测试,测试,测试,测试',
style: TextStyle(color: Colors.red, fontSize: 20),
)));
}
}

image.png|500

1.4 Container

  • 类似 iOS中的UIView
  • 可以设置背景色、图像、边框、圆角、固定尺寸、内边距、对齐方式
  • 使用的频率会比较高
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Container({
super.key,
this.alignment, //对齐方式
this.padding, //容器内补白,属于decoration的装饰范围
this.color, //背景色
this.decoration, //背景装饰
this.foregroundDecoration, //前景装饰
double? width, //宽度
double? height, //高度
BoxConstraints? constraints, //容器大小限制条件
this.margin, //外边距,不属于decoration的装饰范围
this.transform, //变形
this.transformAlignment, //变形对齐
this.child, //子widget
this.clipBehavior = Clip.none,
})

其中 decoration 是比较重要的属性

  • 它的属性是Decoration,是一个抽象类
  • 实现类BoxDecoration来进行实例化
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const BoxDecoration({
    this.color, //背景颜色,和Container.color 冲突,同时使用会报错
    this.image, //背景图片
    this.border, // 边框
    this.borderRadius, //圆角
    this.boxShadow, //阴影效果
    this.gradient, //渐变色
    this.backgroundBlendMode, //背景混合
    this.shape = BoxShape.rectangle,
    })

简单操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
return Center(
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.black,
border: Border.all(width: 10, color: Colors.purple),
borderRadius: BorderRadius.circular(20),
boxShadow: const [
BoxShadow(offset: Offset(5, 5), color: Colors.black, blurRadius: 5)
],
gradient: const LinearGradient(colors: [Colors.yellow, Colors.red])),
// color: Colors.amber,
child: const Text('测试,测试,测试,测试,测试,测试,测试,测试,测试,测试,测试,测试',
style: TextStyle(color: Colors.black, fontSize: 20)),
));
}
}

image.png|500

2.多子布局

在开发中,我们经常需要将多个Widget放在一起进行布局,比如水平方向、垂直方向排列,甚至有时候需要他们进行层叠,比如图片上面放一段文字等;

这个时候我们需要使用多子布局组件(Multi-child layout widgets)

比较常用的多子布局组件是Row、Column、Stack,我们来学习一下他们的使用。

2.1 Flex组件

类似iOS中的UIStackView,布局的思想应该是借鉴web flex布局,不过只能在单行、单列上排版,
其中主要认识两个方向,主轴和交叉轴

  • Row:横向是主轴,纵向是交叉轴
  • Column:纵向是主轴,横向是交叉轴
1
2
3
4
5
6
7
8
9
10
11
12
const Flex({
super.key,
required this.direction,
this.mainAxisAlignment = MainAxisAlignment.start,
this.mainAxisSize = MainAxisSize.max,
this.crossAxisAlignment = CrossAxisAlignment.center,
this.textDirection,
this.verticalDirection = VerticalDirection.down,
this.textBaseline, // NO DEFAULT: we don't know what the text's baseline should be
this.clipBehavior = Clip.none,
super.children,
})
  • direction:设置主轴方向,Axis.horizontal 和 Axis.vertical

2.1.1 Row组件

Row 继承自 Flex,如果是直接确定横向布局,可以直接使用Row

1
2
3
4
5
6
7
8
9
10
const Row({
super.key,
super.mainAxisAlignment, //主轴(横向)对齐方式
super.mainAxisSize, //主轴(横向)占用空间
super.crossAxisAlignment, //交叉轴(纵向)对齐方式
super.textDirection, //固定横向子widget方向,左到右、右到左
super.verticalDirection, //固定纵向的子widget方向,down、up
super.textBaseline,
super.children,
})
  • mainAxisSize
    1
    2
    3
    4
    enum MainAxisSize { 
    min, // 横向宽度是子widget的总和
    max, // 尽可能大,横向宽度是父容器的大小
    }
  • mainAxisAlignment

    1
    2
    3
    4
    5
    6
    7
    8
    enum MainAxisAlignment {
    start, // 从头部开始排版,textDirection=.ltr从左开始,textDirection=.rtl从右开始
    end, // 从尾部开始排版,textDirection=.ltr从右开始,textDirection=.rtl从左开始
    center, //居中紧挨着
    spaceBetween,
    spaceAround,
    spaceEvenly,
    }
  • textDirection

    1
    2
    3
    4
    enum TextDirection {
    rtl, // 子widget,横向方向,右到左
    ltr, // 子widget,横向方向,左到右
    }
  • crossAxisAlignment

    1
    2
    3
    4
    5
    6
    7
    enum CrossAxisAlignment {
    start,
    end,
    center,
    stretch,
    baseline, //设置为 baseline 时,必须设置 textBaseline 属性的值
    }

简单操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
lass MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
return Flex(direction: Axis.vertical, children: [
const Align(
alignment: Alignment.centerLeft,
child: Text(
'TextDirection.ltr',
style: TextStyle(fontSize: 20),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
textDirection: TextDirection.ltr,
verticalDirection: VerticalDirection.down,
children: <Widget>[
Container(color: Colors.red, width: 60, height: 60),
Container(color: Colors.blue, width: 80, height: 80),
Container(color: Colors.green, width: 70, height: 70),
Container(color: Colors.orange, width: 100, height: 100),
],
),
const Align(
alignment: Alignment.centerRight,
child: Text(
'TextDirection.rtl',
style: TextStyle(fontSize: 20),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.max,
textDirection: TextDirection.rtl,
verticalDirection: VerticalDirection.down,
children: <Widget>[
Container(color: Colors.red, width: 60, height: 60),
Container(color: Colors.blue, width: 80, height: 80),
Container(color: Colors.green, width: 70, height: 70),
Container(color: Colors.orange, width: 100, height: 100),
],
),
]);
}
}

image.png|400

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
var widgets = [
Container(color: Colors.red, width: 60, height: 40),
Container(color: Colors.blue, width: 80, height: 60),
Container(color: Colors.green, width: 70, height: 70),
Container(color: Colors.orange, width: 100, height: 50),
];

return Flex(
direction: Axis.vertical,
children: [
const Spacer(
flex: 1,
),
const Text(
'MainAxisAlignment.start',
style: TextStyle(fontSize: 20),
),
Container(
decoration: BoxDecoration(
border: Border.all(width: 3, color: Colors.black),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: widgets,
),
),
const Spacer(
flex: 1,
),
const Text(
'MainAxisAlignment.end',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: widgets,
),
),
const Spacer(
flex: 1,
),
const Text(
'MainAxisAlignment.center',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: widgets,
),
),
const Spacer(
flex: 1,
),
const Text(
'MainAxisAlignment.spaceAround',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: widgets,
),
),
const Spacer(
flex: 1,
),
const Text(
'MainAxisAlignment.spaceBetween',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: widgets,
),
),
const Spacer(
flex: 1,
),
const Text(
'MainAxisAlignment.spaceEvenly',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: widgets,
),
),
],
);
}
}

image.png|300

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
var widgets = [
Container(
color: Colors.red, width: 60, height: 40, child: const Text('A')),
Container(
color: Colors.blue, width: 80, height: 60, child: const Text('B')),
Container(
color: Colors.green, width: 70, height: 70, child: const Text('C')),
Container(
color: Colors.orange, width: 100, height: 50, child: const Text('D')),
];

return Flex(
direction: Axis.vertical,
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
const Text(
'CrossAxisAlignment.start',
style: TextStyle(fontSize: 20),
),
Container(
decoration: BoxDecoration(
border: Border.all(width: 3, color: Colors.black),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: widgets,
),
),
const Text(
'CrossAxisAlignment.end',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: widgets,
),
),
const Text(
'CrossAxisAlignment.center',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: widgets,
),
),
const Text(
'CrossAxisAlignment.spaceAround',
style: TextStyle(fontSize: 20),
),
Container(
height: 100,
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: widgets,
),
),
const Text(
'CrossAxisAlignment.spaceAround',
style: TextStyle(fontSize: 20),
),
Container(
decoration:
BoxDecoration(border: Border.all(width: 3, color: Colors.black)),
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
children: widgets,
),
)
],
);
}
}

image.png|300

2.1.2 Column

参考Row操作

2.2 Wrap

和 Flex 不同的是,支持换行

2.3 Stack

实现组件重叠显示,web中使用绝对定位,iOS使用frame

1
2
3
4
5
6
7
8
const Stack({
super.key,
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.clipBehavior = Clip.hardEdge,
super.children,
});
  • alignment:此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子widget。所谓部分定位,在这里特指没有在某一个轴上定位:left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。
  • textDirection:和Row、Wrap的textDirection功能一样,都用于决定alignment对齐的参考系即:textDirection的值为TextDirection.ltr,则alignment的start代表左,end代表右;textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左。
  • fit:此参数用于决定没有定位的子widget如何去适应Stack的大小。StackFit.loose表示使用子widget的大小,StackFit.expand表示扩伸到Stack的大小。
  • overflow:此属性决定如何显示超出Stack显示空间的子widget,值为Overflow.clip时,超出部分会被剪裁(隐藏),值为Overflow.visible 时则不会。

Stack会经常和Positioned一起来使用

  • 决定组件在Stack中的位置,类似iOS中设置 UIEdgeInsert,设置上下左右的边距
  • 设置宽、高
  • Positioned只能在Stack中使用
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    const Positioned({
    super.key,
    this.left,
    this.top,
    this.right,
    this.bottom,
    this.width,
    this.height,
    required super.child,
    })

简单操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});

@override
Widget build(BuildContext context) {
return Stack(
children: [
Container(width: 300, height: 300, color: Colors.lightBlue),
const Positioned(
top: 10,
left: 20,
child: Text(
'标题',
style: TextStyle(fontSize: 30),
),
),
const Positioned(
top: 100,
left: 100,
bottom: 100,
right: 100,
child: Text(
'内容',
style: TextStyle(fontSize: 50),
)),
const Positioned(
right: 40,
bottom: 10,
child: Icon(
Icons.face,
size: 100,
))
],
);
}
}

image.png|500