前言
容器类Widget和布局类Widget都作用于其子Widget,不同的是:
布局类Widget一般都需要接收一个widget数组(children),他们直接或间接继承自(或包含)MultiChildRenderObjectWidget ;
而容器类Widget一般只需要接收一个子Widget(child),他们直接或间接继承自(或包含)SingleChildRenderObjectWidget。
布局类Widget是按照一定的排列方式来对其子Widget进行排列;
而容器类Widget一般只是包装其子Widget,对其添加一些修饰(补白或背景色等)、变换(旋转或剪裁等)、或限制(大小等)。
Padding(填充)
Padding
可以给其子节点添加填充(留白),和边距效果类似。我们在前面很多示例中都已经使用过它了,现在来看看它的定义:
1 | Padding({ |
EdgeInsetsGeometry
是一个抽象类,开发中,我们一般都使用EdgeInsets
类,它是EdgeInsetsGeometry
的一个子类,定义了一些设置填充的便捷方法。
EdgeInsets
我们看看EdgeInsets
提供的便捷方法:
fromLTRB(double left, double top, double right, double bottom)
:分别指定四个方向的填充。all(double value)
: 所有方向均使用相同数值的填充。only({left, top, right ,bottom })
:可以设置具体某个方向的填充(可以同时指定多个方向)。symmetric({ vertical, horizontal })
:用于设置对称方向的填充,vertical
指top
和bottom
,horizontal
指left
和right
。
尺寸限制类容器
尺寸限制类容器用于限制容器大小,Flutter中提供了多种这样的容器,如
ConstrainedBox
SizedBox
UnconstrainedBox
AspectRatio
本节将介绍一些常用的。
ConstrainedBox
ConstrainedBox
用于对子组件添加额外的约束。
例如,如果你想让子组件的最小高度是80像素,你可以使用const BoxConstraints(minHeight: 80.0)
作为子组件的约束。
我们实现一个最小高度为50,宽度尽可能大的红色容器。
1 | ConstrainedBox( |
多重限制
如果某一个组件有多个父级ConstrainedBox
限制,那么最终会是哪个生效?我们看一个例子:
1 | ConstrainedBox( |
上面我们有父子两个ConstrainedBox
,他们的限制条件不同
最终显示效果是宽90,高60,也就是说是子ConstrainedBox
的minWidth
生效,而minHeight
是父ConstrainedBox
生效。
BoxConstraints
BoxConstraints用于设置限制条件,它的定义如下:
1 | const BoxConstraints({ |
BoxConstraints还定义了一些便捷的构造函数,用于快速生成特定限制规则的BoxConstraints,
如
BoxConstraints.tight(Size size)
,它可以生成给定大小的限制;const BoxConstraints.expand()
可以生成一个尽可能大的用以填充另一个容器的BoxConstraints。
SizedBox
SizedBox
用于给子元素指定固定的宽高,如:
1 | SizedBox( |
实际上SizedBox
只是ConstrainedBox
的一个定制,上面代码等价于:
1 | ConstrainedBox( |
而BoxConstraints.tightFor(width: 80.0,height: 80.0)
等价于:
1 | BoxConstraints(minHeight: 80.0,maxHeight: 80.0,minWidth: 80.0,maxWidth: 80.0) |
UnconstrainedBox
UnconstrainedBox
不会对子组件产生任何限制,它允许其子组件按照其本身大小绘制。一般情况下,我们会很少直接使用此组件,但在”去除”多重限制的时候也许会有帮助,我们看下下面的代码:
1 | ConstrainedBox( |
上面代码中,如果没有中间的UnconstrainedBox
,那么根据上面所述的多重限制规则,那么最终将显示一个90×100的红色框。但是由于UnconstrainedBox
“去除”了父ConstrainedBox
的限制,则最终会按照子ConstrainedBox
的限制来绘制redBox
,即90×20
但是,读者请注意,UnconstrainedBox
对父组件限制的“去除”并非是真正的去除:上面例子中虽然红色区域大小是90×20,但上方仍然有80的空白空间。也就是说父限制的minHeight
(100.0)仍然是生效的,只不过它不影响最终子元素redBox
的大小,但仍然还是占有相应的空间,可以认为此时的父ConstrainedBox
是作用于子UnconstrainedBox
上,而redBox
只受子ConstrainedBox
限制,这一点请读者务必注意。
DecoratedBox(装饰容器)
DecoratedBox
可以在其子组件绘制前(或后)绘制一些装饰(Decoration),如背景、边框、渐变等。DecoratedBox
定义如下:
1 | const DecoratedBox({ |
decoration
:代表将要绘制的装饰,它的类型为Decoration
。Decoration
是一个抽象类,它定义了一个接口createBoxPainter()
,子类的主要职责是需要通过实现它来创建一个画笔,该画笔用于绘制装饰。position :此属性决定在哪里绘制Decoration,它接收DecorationPosition的枚举类型,该枚举类有两个值:
background
:在子组件之后绘制,即背景装饰。foreground
:在子组件之上绘制,即前景。
BoxDecoration
我们通常会直接使用BoxDecoration
类,它是一个Decoration的子类,实现了常用的装饰元素的绘制。
1 | BoxDecoration({ |
各个属性名都是自解释的,详情读者可以查看API文档。下面我们实现一个带阴影的背景色渐变的按钮:
1 | DecoratedBox( |
Transform(变换)
Transform
可以在其子组件绘制时对其应用一些矩阵变换来实现一些特效。Matrix4
是一个4D矩阵,通过它我们可以实现各种矩阵操作,下面是一个例子:
1 | Container( |
平移
Transform.translate
接收一个offset
参数,可以在绘制时沿x
、y
轴对子组件平移指定的距离。
1 | DecoratedBox( |
旋转
Transform.rotate
可以对子组件进行旋转变换,如:
1 | DecoratedBox( |
注意:要使用
math.pi
需先进行如下导包。
1
2 > import 'dart:math' as math;
>
Container(全能)
这是 Container 的三个主要表现:
- 当没有子 widgets 且没有指定 constraints 时,尽可能地充满可用空间
- 如果有 constraints 就以 constraints 为准(除非跟 parent constraints 冲突)
- 如果有子 widgets 则以 children 的 size 为准;可以设置 width, height,constraints 来约束 size
Container
是一个组合类容器,它本身不对应具体的RenderObject
,它是DecoratedBox
、ConstrainedBox、Transform
、Padding
、Align
等组件组合的一个多功能容器,所以我们只需通过一个Container
组件可以实现同时需要装饰、变换、限制的场景。下面是Container
的定义:
1 | Container({ |
注意点
容器的大小可以通过
width
、height
属性来指定,也可以通过constraints
来指定;如果它们同时存在时,
width
、height
优先。实际上Container内部会根据
width
、height
来生成一个constraints
。color
和decoration
是互斥的,如果同时设置它们则会报错!实际上,当指定
color
时,Container
内会自动创建一个decoration
。
decoration 常用设置
背景圆角
1 | BoxDecoration( |
Clip(剪裁)
Flutter中提供了一些剪裁函数,用于对组件进行剪裁。
剪裁Widget | 作用 |
---|---|
ClipOval | 子组件为正方形时剪裁为内贴圆形,为矩形时,剪裁为内贴椭圆 |
ClipRRect | 将子组件剪裁为圆角矩形 |
ClipRect | 剪裁子组件到实际占用的矩形大小(溢出部分剪裁) |
示例
1 | ClipOval(child: avatar),//剪裁为圆形 |
Scaffold、TabBar、底部导航
一个完整的路由页可能会包含导航栏、抽屉菜单(Drawer)以及底部Tab导航菜单等。如果每个路由页面都需要开发者自己手动去实现这些,这会是一件非常麻烦且无聊的事。幸运的是,Flutter Material组件库提供了一些现成的组件来减少我们的开发任务。Scaffold
是一个路由页的骨架,我们使用它可以很容易地拼装出一个完整的页面。
我们实现一个页面,它包含:
- 一个导航栏
- 导航栏右边有一个分享按钮
- 有一个抽屉菜单
- 有一个底部导航
- 右下角有一个悬浮的动作按钮
代码如下:
1 | class ScaffoldRoute extends StatefulWidget { |
上面代码中我们用到了如下组件:
组件名称 | 解释 |
---|---|
AppBar | 一个导航栏骨架 |
MyDrawer | 抽屉菜单 |
BottomNavigationBar | 底部导航栏 |
FloatingActionButton | 漂浮按钮 |
AppBar
AppBar
是一个Material风格的导航栏,通过它可以设置导航栏标题、导航栏菜单、导航栏底部的Tab标题等。下面我们看看AppBar的定义:
1 | AppBar({ |
如果给Scaffold
添加了抽屉菜单,默认情况下Scaffold
会自动将AppBar
的leading
设置为菜单按钮(如上面截图所示),点击它便可打开抽屉菜单。如果我们想自定义菜单图标,可以手动来设置leading
,如:
1 | Scaffold( |
TabBar
下面我们通过“bottom”属性来添加一个导航栏底部Tab按钮组
Material组件库中提供了一个TabBar
组件,它可以快速生成Tab
菜单,下面是上图对应的源码:
1 | class _ScaffoldRouteState extends State<ScaffoldRoute> |
Tab
组件有三个可选参数,除了可以指定文字外,还可以指定Tab菜单图标,或者直接自定义组件样式。Tab
组件定义如下:
1 | Tab({ |
Drawer(抽屉菜单)
Scaffold
的drawer
和endDrawer
属性可以分别接受一个Widget来作为页面的左、右抽屉菜单。如果开发者提供了抽屉菜单,那么当用户手指从屏幕左(或右)侧向里滑动时便可打开抽屉菜单。本节开始部分的示例中实现了一个左抽屉菜单MyDrawer
,它的源码如下:
1 | class MyDrawer extends StatelessWidget { |
抽屉菜单通常将Drawer
组件作为根节点,它实现了Material风格的菜单面板,MediaQuery.removePadding
可以移除Drawer默认的一些留白(比如Drawer默认顶部会留和手机状态栏等高的留白),读者可以尝试传递不同的参数来看看实际效果。抽屉菜单页由顶部和底部组成,顶部由用户头像和昵称组成,底部是一个菜单列表,用ListView实现
FloatingActionButton
FloatingActionButton
是Material设计规范中的一种特殊Button,通常悬浮在页面的某一个位置作为某种常用动作的快捷入口
SafeArea
使用 SafeArea 可以让 child widget 在顶部和底部腾出足够的空间方便处理 iPhoneX 这类的手机。