You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

274 lines
15 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 14 | 经典布局:如何定义子控件在父容器中排版的位置?
你好,我是陈航。
在前面两篇文章中我们一起学习了构建视图的基本元素文本、图片和按钮用于展示一组连续视图元素的ListView以及处理多重嵌套的可滚动视图的CustomScrollView。
在Flutter中一个完整的界面通常就是由这些小型、单用途的基本控件元素依据特定的布局规则堆砌而成的。那么今天我就带你一起学习一下在Flutter中搭建出一个漂亮的布局我们需要了解哪些布局规则以及这些规则与其他平台类似概念的差别在哪里。希望这样的设计可以帮助你站在已有经验的基础上去高效学习Flutter的布局规则。
我们已经知道在Flutter中一切皆Widget那么布局也不例外。但与基本控件元素不同布局类的Widget并不会直接呈现视觉内容而是作为承载其他子Widget的容器。
这些布局类的Widget内部都会包含一个或多个子控件并且都提供了摆放子控件的不同布局方式可以实现子控件的对齐、嵌套、层叠和缩放等。而我们要做的就是通过各种定制化的参数将其内部的子Widget依照自己的布局规则放置在特定的位置上最终形成一个漂亮的布局。
Flutter提供了31种[布局Widget](https://flutter.dev/docs/development/ui/widgets/layout)对布局控件的划分非常详细一些相同或相似的视觉效果可以通过多种布局控件实现因此布局类型相比原生Android、iOS平台多了不少。比如Android布局一般就只有FrameLayout、LinearLayout、RelativeLayout、GridLayout和TableLayout这5种而iOS的布局更少只有Frame布局和自动布局两种。
为了帮你建立起对布局类Widget的认知了解基本布局类Widget的布局特点和用法从而学以致用快速上手开发在今天的这篇文章中我特意挑选了几类在开发Flutter应用时最常用也最有代表性的布局Widget包括单子Widget布局、多子Widget布局、层叠Widget布局与你展开介绍。
掌握了这些典型的Widget你也就基本掌握了构建一个界面精美的App所需要的全部布局方式了。接下来我们就先从单子Widget布局聊起吧。
## 单子Widget布局Container、Padding与Center
单子Widget布局类容器比较简单一般用来对其唯一的子Widget进行样式包装比如限制大小、添加背景色样式、内间距、旋转变换等。这一类布局Widget包括Container、Padding与Center三种。
Container是一种允许在其内部添加其他控件的控件也是UI框架中的一个常见概念。
在Flutter中Container本身可以单独作为控件存在比如单独设置背景色、宽高也可以作为其他控件的父级存在Container可以定义布局过程中子Widget如何摆放以及如何展示。与其他框架不同的是**Flutter的Container仅能包含一个子Widget**。
所以对于多个子Widget的布局场景我们通常会这样处理先用一个根Widget去包装这些子Widget然后把这个根Widget放到Container中再由Container设置它的对齐alignment、边距padding等基础属性和样式属性。
接下来我通过一个示例与你演示如何定义一个Container。
在这个示例中我将一段较长的文字包装在一个红色背景、圆角边框的、固定宽高的Container中并分别设置了Container的外边距距离其父Widget的边距和内边距距离其子Widget的边距
```
Container(
child: Text('Container容器在UI框架中是一个很常见的概念Flutter也不例外。'),
padding: EdgeInsets.all(18.0), // 内边距
margin: EdgeInsets.all(44.0), // 外边距
width: 180.0,
height:240,
alignment: Alignment.center, // 子Widget居中对齐
decoration: BoxDecoration( //Container样式
color: Colors.red, // 背景色
borderRadius: BorderRadius.circular(10.0), // 圆角边框
),
)
```
![](https://static001.geekbang.org/resource/image/fa/f7/fad72eb6917be0062df5a46a104f3ff7.png)
图1 Container示例
如果我们只需要将子Widget设定间距则可以使用另一个单子容器控件Padding进行内容填充
```
Padding(
padding: EdgeInsets.all(44.0),
child: Text('Container容器在UI框架中是一个很常见的概念Flutter也不例外。'),
);
```
![](https://static001.geekbang.org/resource/image/24/ae/24a18be98054d93ddf8989aac1a5a5ae.png)
图2 Padding示例
在需要设置内容间距时我们可以通过EdgeInsets的不同构造函数分别制定四个方向的不同补白方式如均使用同样数值留白、只设置左留白或对称方向留白等。如果你想更深入地了解这部分内容可以参考这个[API文档](https://api.flutter.dev/flutter/painting/EdgeInsets-class.html#constructors)。
接下来我们再来看看单子Widget布局容器中另一个常用的容器Center。正如它的名字一样Center会将其子Widget居中排列。
比如我们可以把一个Text包在Center里实现居中展示
```
Scaffold(
body: Center(child: Text("Hello")) // This trailing comma makes auto-formatting nicer for build methods.
);
```
![](https://static001.geekbang.org/resource/image/c5/65/c500408d7df10249494a2a90cb815a65.png)
图3 Center示例
需要注意的是为了实现居中布局Center所占据的空间一定要比其子Widget要大才行这也是显而易见的如果Center和其子Widget一样大自然就不需要居中也没空间居中了。因此Center通常会结合Container一起使用。
现在我们结合Container一起看看Center的具体使用方法吧。
```
Container(
child: Center(child: Text('Container容器在UI框架中是一个很常见的概念Flutter也不例外。')),
padding: EdgeInsets.all(18.0), // 内边距
margin: EdgeInsets.all(44.0), // 外边距
width: 180.0,
height:240,
decoration: BoxDecoration( //Container样式
color: Colors.red, // 背景色
borderRadius: BorderRadius.circular(10.0), // 圆角边框
),
);
```
可以看到我们通过Center容器实现了Container容器中**alignment: Alignment.center**的效果。
事实上为了达到这一效果Container容器与Center容器底层都依赖了同一个容器Align通过它实现子Widget的对齐方式。Align的使用也比较简单如果你想深入了解的话可以参考[官方文档](https://api.flutter.dev/flutter/widgets/Align-class.html),这里我就不再过多介绍了。
接下来我们再看看多子Widget布局的三种方式即Row、Column与Expanded。
## 多子Widget布局Row、Column与Expanded
对于拥有多个子Widget的布局类容器而言其布局行为无非就是两种规则的抽象水平方向上应该如何布局、垂直方向上应该如何布局。
如同Android的LinearLayout、前端的Flex布局一样Flutter中也有类似的概念即将子Widget按行水平排列的Row按列垂直排列的Column以及负责分配这些子Widget在布局方向行/列中剩余空间的Expanded。
Row与Column的使用方法很简单我们只需要将各个子Widget按序加入到children数组即可。在下面的代码中我们把4个分别设置了不同的颜色和宽高的Container加到Row与Column中
```
//Row的用法示范
Row(
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
);
//Column的用法示范
Column(
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
);
```
![](https://static001.geekbang.org/resource/image/90/72/909ad17a65cad573bca0c84c09b7fc72.png)
(a)Row示例
![](https://static001.geekbang.org/resource/image/9a/86/9a1bd0067d1bbb03d5ab74f411afae86.png)
(b)Column示例
图4 Row与Column示例
可以看到单纯使用Row和Column控件在子Widget的尺寸较小时无法将容器填满视觉样式比较难看。对于这样的场景我们可以通过Expanded控件来制定分配规则填满容器的剩余空间。
比如我们希望Row组件或Column组件中的绿色容器与黄色容器均分剩下的空间于是就可以设置它们的弹性系数参数flex都为1这两个Expanded会按照其flex的比例即11来分割剩余的Row横向Column纵向空间
```
Row(
children: <Widget>[
Expanded(flex: 1, child: Container(color: Colors.yellow, height: 60)), //设置了flex=1因此宽度由Expanded来分配
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Expanded(flex: 1, child: Container(color: Colors.green,height: 60),)/设置了flex=1因此宽度由Expanded来分配
],
);
```
![](https://static001.geekbang.org/resource/image/0e/04/0ed3fba81215150607edddaafbe9b304.png)
图5 Expanded控件示例
于Row与Column而言Flutter提供了依据坐标轴的布局对齐行为即根据布局方向划分出主轴和纵轴主轴表示容器依次摆放子Widget的方向纵轴则是与主轴垂直的另一个方向。
![](https://static001.geekbang.org/resource/image/61/09/610157c35f4457a7fffa2005ea144609.png)
图6 Row和Column控件的主轴与纵轴
我们可以根据主轴与纵轴设置子Widget在这两个方向上的对齐规则mainAxisAlignment与crossAxisAlignment。比如主轴方向start表示靠左对齐、center表示横向居中对齐、end表示靠右对齐、spaceEvenly表示按固定间距对齐而纵轴方向start则表示靠上对齐、center表示纵向居中对齐、end表示靠下对齐。
下图展示了在Row中设置不同方向的对齐规则后的呈现效果
![](https://static001.geekbang.org/resource/image/9f/87/9f3a8a9e197b350f6c0aad6f5195fc87.png)
图7 Row的主轴对齐方式
![](https://static001.geekbang.org/resource/image/d8/9b/d8fc6d0aa98be8a6b1867b24a833b89b.png)
图8 Row的纵轴对齐方式
Column的对齐方式也是类似的我就不再过多展开了。
这里需要注意的是对于主轴而言Flutter默认是让父容器决定其长度即尽可能大类似Android中的match\_parent。
在上面的例子中Row的宽度为屏幕宽度Column的高度为屏幕高度。主轴长度大于所有子Widget的总长度意味着容器在主轴方向的空间比子Widget要大这也是我们能通过主轴对齐方式设置子Widget布局效果的原因。
如果想让容器与子Widget在主轴上完全匹配我们可以通过设置Row的mainAxisSize参数为MainAxisSize.min由所有子Widget来决定主轴方向的容器长度即主轴方向的长度尽可能小类似Android中的wrap\_content
```
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, //由于容器与子Widget一样宽因此这行设置排列间距的代码并未起作用
mainAxisSize: MainAxisSize.min, //让容器宽度与所有子Widget的宽度一致
children: <Widget>[
Container(color: Colors.yellow, width: 60, height: 80,),
Container(color: Colors.red, width: 100, height: 180,),
Container(color: Colors.black, width: 60, height: 80,),
Container(color: Colors.green, width: 60, height: 80,),
],
)
```
![](https://static001.geekbang.org/resource/image/d8/96/d8d9cc480386bd11e60da51ddc081696.png)
图9 Row 的主轴大小
可以看到我们设置了主轴大小为MainAxisSize.min之后Row的宽度变得和其子Widget一样大因此再设置主轴的对齐方式也就不起作用了。
## 层叠Widget布局Stack与Positioned
有些时候我们需要让一个控件叠加在另一个控件的上面比如在一张图片上放置一段文字又或者是在图片的某个区域放置一个按钮。这时候我们就需要用到层叠布局容器Stack了。
Stack容器与前端中的绝对定位、Android中的Frame布局非常类似子Widget之间允许叠加还可以根据父容器上、下、左、右四个角的位置来确定自己的位置。
**Stack提供了层叠布局的容器而Positioned则提供了设置子Widget位置的能力**。接下来我们就通过一个例子来看一下Stack和Positioned的具体用法吧。
在这个例子中我先在Stack中放置了一块300_300的黄色画布随后在(18,18)处放置了一个50_50的绿色控件然后在(18,70)处放置了一个文本控件。
```
Stack(
children: <Widget>[
Container(color: Colors.yellow, width: 300, height: 300),//黄色容器
Positioned(
left: 18.0,
top: 18.0,
child: Container(color: Colors.green, width: 50, height: 50),//叠加在黄色容器之上的绿色控件
),
Positioned(
left: 18.0,
top:70.0,
child: Text("Stack提供了层叠布局的容器"),//叠加在黄色容器之上的文本
)
],
)
```
试着运行一下可以看到这三个子Widget都按照我们预定的规则叠加在一起了。
![](https://static001.geekbang.org/resource/image/bb/36/bb046cc53ea595a02a564a4387a99c36.png)
图10 Stack与Positioned容器示例
Stack控件允许其子Widget按照创建的先后顺序进行层叠摆放而Positioned控件则用来控制这些子Widget的摆放位置。需要注意的是Positioned控件只能在Stack中使用在其他容器中使用会报错。
## 总结
Flutter的布局容器强大而丰富可以将小型、单用途的基本视觉元素快速封装成控件。今天我选取了Flutter中最具代表性也最常用的几类布局Widget与你介绍了构建一个界面精美的App所需要的布局概念。
接下来,我们简单回顾一下今天的内容,以便加深理解与记忆:
首先我们认识了单子容器Container、Padding与Center。其中Container内部提供了间距、背景样式等基础属性为子Widget的摆放方式及展现样式都提供了定制能力。而Padding与Center提供的功能则正如其名一样简洁就是对齐与居中。
然后我们深入学习了多子Widget布局中的Row和Column各子Widget间对齐的规则以及容器自身扩充的规则以及如何通过Expanded控件使用容器内部的剩余空间
最后我们学习了层叠布局Stack以及与之搭配使用的定位子Widget位置的Positioned容器你可以通过它们实现多个控件堆放的布局效果。
通过今天的文章相信你已经对如何搭建App的界面有了足够的知识储备所以在下一篇文章中我会通过一些实际的例子带你认识在Flutter中如何通过这些基本控件与布局规则实现好看的界面。
## 思考题
最后,我给你留下一道思考题吧。
Row与Column自身的大小是如何决定的当它们嵌套时又会出现怎样的情况呢
欢迎你在评论区给我留言分享你的观点,我会在下一篇文章中等待你!感谢你的收听,也欢迎你把这篇文章分享给更多的朋友一起阅读。