前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。
首先明确几点概念:
Widget
是构建UI
的类Widget
在构建UI
元素和构建布局时都会用到- 将简单的
Widget
组合起来可以构建复杂的Widget
Flutter
布局机制的核心是Widget
。在Flutter
中,几乎所有东西都是一个Widget
,即使布局模型也不例外。在Flutter
应用程序中看到的图像(image
)、图标(icon
)和文本(text
)都是Widget
。那些你看不到的东西也是Widget
,比如用来排列(arrange
)、约束(constrain
)和对齐(align
)可见Widget
的行(Row
)、列(Column
)和网格(grid
),都是Widget
。
你可以通过组合多个Widget
来创建布局,这样可以构建出更复杂的Widget
。例如,下图显示了3个图标(icon
),每个图标下都有一个标签(label
):
我们透过现象看本质:
上图显示了真正的视觉布局,它有一行三列,其中每列包含一个图标(icon
)和一个标签(label
),就是这种简单Widget
的组合最终构成了我们看到的复杂Widget
。
注意:在本文档中的大多数插图都是在
debugPaintSizeEnabled=true
的前提下获取的,这个选项可以在屏幕上绘制出视觉布局(如上图,绘制出了每一个可见、不可见Widget
的分隔线,即实际的布局)。
下图画出了上图布局的Widget
树:
这其中的大部分看起来和我们预想的差不多,但是你可能会对Container
(粉红色的圈)感到疑惑,Container
是一个Widget
类,允许我们自定义其子Widget
。如果要添加填充(padding
)、页边距(margin
)、边框(border
)或背景色(background color
),请使用Container
来设置对应功能。
在本例中,每个Text Widget
都放置在一个Container
中以添加页边距(margin
)。整个行(Row
)也放置在一个Container
中,以便在该行的周围添加填充(padding
)。
本例中的其余UI元素均由属性控制。使用Icon
的color
属性设置图标的颜色。使用Text.Style
属性设置字体、颜色、粗细等。Row
和Column
的属性允许您指定其子级垂直或水平对齐的方式,以及子级应占用的空间。
注意:可能有的读者对
margin
和padding
的区别不是很清楚,margin
指的是当前Widget
距离其他Widget
的距离,padding
指的是当前Widget
中的内容距离当前Widget
边界的距离(比如Text
中的文字显示出来距离Text Widget
外边缘的距离)。
在Flutter
中你应该如何对一个简单的Widget
进行布局?这节将向你介绍如何创建并展示一个简单的Widget
,同时会向你介绍Hello World
示例app
的全部代码。
在Flutter
中,只需要很少的步骤就可以将Text
、Icon
或者Image
放置在屏幕上:
根据您希望如何对齐或约束可见的Widget
从各种布局Widget
中进行选择,因为这些布局Widget
的特性通常会传递给他们所包含的Widget
。
此示例使用Center
这个Widget
,该Widget
的特点是将其包裹的内容水平和垂直居中。
比如创建一个Text Widget
:
Text('Hello World'),
创建一个Image Widget
:
Image.asset(
'images/lake.jpg',
fit: BoxFit.cover,
),
创建一个Icon Widget
:
Icon(
Icons.star,
color: Colors.red[500],
),
所有的布局Widget
都具有以下特性:
Widget
则具有一个child
属性,比如Center
或者Container
Widget
则具有一个children
属性,比如Row
、Column
、ListView
、Stack
。将Text Widget
添加到Center Widget
中:
Center(
child: Text('Hello World'),
),
一个Flutter App
本身就是一个Widget
,而大多数Widget
都具有一个build()
方法,所以我们可以在app
的build()
方法中实例化并返回一个Widget
,这样被返回的Widget就可以被显示出来。
对于Material
风格的app
,你可以使用一个Scaffold Widget
,这个Widget
提供了一个默认的横栏、默认的背景色,而且还有一些像添加drawer
、添加snack bar
、添加bottom sheet
这样的API
。你可以直接将Center Widget
作为Scaffold
的body
属性进而添加到页面中:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter layout demo',
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: Center(
child: Text('Hello World'),
),
),
);
}
}
注意:
Material
库(地址:https://api.flutter.dev/flutter/material/material-library.html
)中实现的Widget
都是遵循Material
设计准则的,当你设计你的UI
的时候,你可以仅仅只使用标准的Widget
库(地址:https://api.flutter.dev/flutter/widgets/widgets-library.html
),你也可以使用Material
库中的Widget
。当然,你也可以混合使用这两种Widget
库中的Widget
,你可以在现有Widget
的基础上自定义Widget
,也可以不基于现有的Widget
构建完全属于自己的Widget
。
对于非Material
风格的app
,你可以直接在你app
的build()
方法中返回Center Widget
:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
decoration: BoxDecoration(color: Colors.white),
child: Center(
child: Text(
'Hello World',
textDirection: TextDirection.ltr,
style: TextStyle(
fontSize: 32,
color: Colors.black87,
),
),
),
);
}
}
默认的非Material
风格的App
是没有AppBar
、没有标题、没有背景色的,如果你想在非Material
风格的app
中显示上述元素,就只有自己手动去构建了。在这个示例中手动将背景色设置成为了白色、将Text
中的文字颜色设置成了黑色以模仿Material
风格的app
,运行起来是这样的:
最常见的布局模式是垂直或水平排列多个Widget
。可以使用Row Widget
水平排列Widget
,使用Column Widget
垂直排列Widget
。
首先明确几点概念:
Row
和Column
是最常用的2种布局模式Row
和Column
这两个Widget
都可以包裹多个Widget
Row
和Column
的子Widget
可以是Row
或Column
,或者其他任何复杂的Widget
- 你可以指定
Row
和Column
如何垂直或者水平排列其子Widget
- 你可以拉伸(
stretch
)或者约束(constrain
)指定的子Widget
- 你可以指定子
Widget
如何使用Row
或者Column
的剩余可用空间
为了在Flutter
中创建一个行或者列,你应该将子Widget
组成的List
放进Row Widget
或者Column Widget
中。每个子元素本身可以是Row
或Column
,下面的示例展示了如何在Row
或Column
中嵌套Row
或Column
:
此布局整体上为一行,这一行包含了两个子元素:左侧的列和右侧的图像,而左侧的列中的Widget
树又是由多个行和列组成的:
注意:
Row
和Column
是水平和垂直布局的基本Widget
,这些基础的Widget
允许最大程度的自定义。Flutter
还提供了功能更专一的,更高级别的Widget
,足以满足你的需要。例如,你有时可能更喜欢ListTile
而不是Row
,它是一个易于使用的Widget
,具有用于前导(leading
)和尾随图标(trailing icon
)的属性,并且最多有3行文本。再比如,相比于Column
,您有时可能更喜欢ListView
,它是一种类似于Column
的布局,如果其包裹的内容太长导致可用空间无法完全容纳,它会进行自动滚动。
将多个Widget放在同一行或同一列之后,我们有时还需要对这些同一行或者同一列的Widget进行对齐方式的设置,比如:
上图中黑色框代表Row
(行),1、2、3分别是3个子Widget
,可以看到a
、b
、c
展示出了3中不不同的效果,这是由于a
、b
、c
中对子Widget
的对其方式的设置策略不同导致的,所以,在Flutter
中为了在Row
和Column
中对子Widget
进行更加明确的排列设置,我们可以通过mainAxisAlignment
和crossAxisAlignment
属性来控制Row
和Column
如何排列子Widget
。对于Row
,mainAxis
指的是水平方向,crossAxis
指的是垂直方向。对于Column
,mainAxis
指的是垂直方向,crossAxis
指的是水平方向。比如像上图中a
、b
、c
三种效果,分别对应给Row
的crossAxisAlignment
属性设置了top
、center
、bottom
。
MainAxisAlignment
和CrossAxisAlignment
这两个类提供了各种用于控制对齐的常量,一般设置的时候我们会这样设置:
new Column(
mainAxisAlignment: MainAxisAlignment.center,
...
);
注意:当你将一张本地图片添加到你的项目中以后,你应该更新你项目的
pubspec
文件以让你的代码可以访问到你添加的图片,如果你引用的是网络图片的话就不用更新pubspec
文件了。
在下面的例子中,每一张图片都是100px
宽,而其容器(在这里是整个屏幕)的宽度是大于300px
的,将mainAxis
设置为spaceEvently
后可以保证这3张图片中的每一张在之前、之间、之后将水平方向的剩余空间自由划分掉:
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),
Image.asset('images/pic2.jpg'),
Image.asset('images/pic3.jpg'),
],
);
效果图:
Column
和Row
具有相同的工作逻辑,下面的这3张图片,每一张的高度是100px
,而其容器(在这里是整个屏幕)的高度大于300px
,将mainAxis
设置为spaceEvenly
之后可以保证这3张图片中的每一张在之上、之间、之下将垂直方向的剩余空间自由划分掉:
Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Image.asset('images/pic1.jpg'),
Image.asset('images/pic2.jpg'),
Image.asset('images/pic3.jpg'),
],
);
效果图:
当一个layout
的尺寸太大以至于超过了设备屏幕的尺寸,超出的屏幕边缘会出现黄色和黑色条纹图案,就像下面这样:
在Row
或者Column
中可以使用Expanded
这个Widget
来设置子Widget
的尺寸,为了修复上图中图像尺寸比其容器尺寸还大的现象,我们可以使用Expanded
来包裹每一个Image Widget
,这样就可以保证这些图片平均分割可用空间而不至于尺寸太大导致出现上图尺寸越界的问题:
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Image.asset('images/pic1.jpg'),
),
Expanded(
child: Image.asset('images/pic2.jpg'),
),
Expanded(
child: Image.asset('images/pic3.jpg'),
),
],
);
效果如下:
有时你也许想让一个Widget
占用的空间是其相邻Widget
所占空间的两倍,这时,你可以使用Expanded Widget
的flex
属性,该属性的类型是一个整数类型,默认为1,表示当前Expanded Widget
所占的比例。以下代码将中间图像的弹性系数设置为2:
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Expanded(
child: Image.asset('images/pic1.jpg'),
),
Expanded(
flex: 2,
child: Image.asset('images/pic2.jpg'),
),
Expanded(
child: Image.asset('images/pic3.jpg'),
),
],
);
由于其他两个Expanded
默认别的flex
为1,所以最终的效果是中间图像的尺寸是两边图像的2倍:
默认情况下Row
或Column
会尽可能在主轴方向占据尽可能多的可用空间,这样就会导致如果可用空间很大但是Row
或Column
中的子Widget
很少时出现子Widget
之间距离很大的情况(分布很分散),如果你希望让Row
或Column
中的子Widget
们尽可能近地出现,可以将mainAxisSize
属性设置为MainAxisSize.min
,下面的示例通过此属性的设置将星形图标紧密地放置在了一起:
Row(
mainAxisSize: MainAxisSize.min,
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),
],
)
运行效果:
Flutter
的布局框架允许你根据你的需要不限量地在Row/Column
中嵌套Row/Column
,让我们来看看下图中红色框圈出来的部分:
红色框中的内容是由2个Row
构成的,上面的Row Widget
包含了5个星星和一个数字Reviews
,对应的Widget
树如下图所示:
对应的代码实现如下:
var stars = Row(
mainAxisSize: MainAxisSize.min,
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),
],
);
final ratings = Container(
padding: EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
stars,
Text(
'170 Reviews',
style: TextStyle(
color: Colors.black,
fontWeight: FontWeight.w800,
fontFamily: 'Roboto',
letterSpacing: 0.5,
fontSize: 20,
),
),
],
),
);
可以看到,主要的组成部分是5个Icon
(用于展示星星)和一个Text
(用于展示数字+Reviews
)。
提示:为了避免由于布局的大量嵌套导致的布局代码混乱的问题,我们可以使用变量或者函数定义一些
Widget
,比如上面代码的ratings
。
我们再来看下面的一行,该Row Widget
包含了3个Column
,每个Column
都包含有一个icon
和2行Text
,对应的Widget
树如下:
对应的实现代码如下:
final descTextStyle = TextStyle(
color: Colors.black,
fontWeight: FontWeight.w800,
fontFamily: 'Roboto',
letterSpacing: 0.5,
fontSize: 18,
height: 2,
);
// 借助DefaultTextStyle.merge()来创建一个Text的主题样式
final iconList = DefaultTextStyle.merge(
style: descTextStyle,
child: Container(
padding: EdgeInsets.all(20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
Icon(Icons.kitchen, color: Colors.green[500]),
Text('PREP:'),
Text('25 min'),
],
),
Column(
children: [
Icon(Icons.timer, color: Colors.green[500]),
Text('COOK:'),
Text('1 hr'),
],
),
Column(
children: [
Icon(Icons.restaurant, color: Colors.green[500]),
Text('FEEDS:'),
Text('4-6'),
],
),
],
),
),
);
再看这张图:
整体是一个Row
,左边是一个Column
,该Column
由一个标题文字、一个摘要文字、以及红色框中的2个Row
组成,所以左边这个Column
的整体代码如下:
final leftColumn = Container(
padding: EdgeInsets.fromLTRB(20, 30, 20, 20),
child: Column(
children: [
titleText,//标题(Strawberry Paova)
subTitle,//摘要文字(Pavlova is a....)
ratings,//红色框中的第一个Row
iconList,//红色框中的第二个Row
],
),
);
然后就可以把左边的这个leftColumn放置在整个布局中了:
body: Center(
child: Container(
margin: EdgeInsets.fromLTRB(0, 40, 0, 30),
height: 600,
child: Card(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 440,
child: leftColumn,//左边的Column
),
mainImage,//右边的那个图像
],
),
),
),
),
可以看到左边的Column
被整个放置在了一个Container Widget
中,这样做的目的是通过Container
来设置该Column
的宽度。
Flutter
有丰富的布局Widget
库,下面是一些最常用的布局Widget
。为了避免你被一整张布局Widget
的清单吓到,这里只列举出了常见的布局Widget
,目的是让你快速上手,如果你希望了解更多Flutter
中的布局Widget
,你可以阅读官方的Widget
文档(地址:https://flutter.dev/docs/development/ui/widgets
)。
以下小部件分为两类:来自Widget
库的标准Widget
和来自Material
库的专用Widget
。任何应用程序都可以使用标准的Widget
库,但只有Material
应用程序可以使用Material
库中的Widget
。
Container
:为一个Widget
添加向小部件添加填充(padding
)、页边距(margin
)、边框(border
)、背景色(background color
)等GridView
:在一个可滚动的网格中放置子Widget
ListView
:在一个可滚动的列表中放置子Widget
Stack
:将一个Widget重叠在另一个Widget
之上Card
:将相关内容布局到具有圆角和阴影的框中。ListTile
:最多将3行文本(text
)以及可选的前导(leading
)和尾随图标(trailing icon
)布局到一行。许多布局都充分利用Container
来的padding
属性来添加填充,或者其border
属性添加边框或利用margin
属性添加页边距。你可以通过将整个布局放置到Container
中并更改其背景色或背景图像来更改屏幕的背景。
padding
)、页边距(margin
)、边框(border
)Widget
或者像Row
、Column
这样复杂的Widget
,甚至包裹Widget
树的根Widget
下面的这个布局包含了一个Column
,这个Column
中包含了2个Row
,每一个Row包含了
2个Image
,使用Container
达到了将Column
的背景颜色改为亮灰色的目的:
Widget _buildImageColumn() => Container(
decoration: BoxDecoration(
color: Colors.black26,
),
child: Column(
children: [
_buildImageRow(1),
_buildImageRow(3),
],
),
);
效果图如下:
同样也可以借助Container
来实现为每个Image
添加圆形边框和页边距:
Widget _buildDecoratedImage(int imageIndex) => Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(width: 10, color: Colors.black38),
borderRadius: const BorderRadius.all(const Radius.circular(8)),
),
margin: const EdgeInsets.all(4),
child: Image.asset('images/pic$imageIndex.jpg'),
),
);
Widget _buildImageRow(int imageIndex) => Row(
children: [
_buildDecoratedImage(imageIndex),
_buildDecoratedImage(imageIndex + 1),
],
);
使用GridView
可以构建一个二维的列表,GridView
默认提供两个列,当然你也可以构建自己的自定义网格。当GridView
检测到它的内容太长时,会自动进行滚动。
Widget
布局在网格中GridView.count
允许你设置网格有多少个列GridView.extent
允许你设置tile的最大像素使用GridView.extent
创建一个tile
最大为150px
的网格:
Widget _buildGrid() => GridView.extent(
maxCrossAxisExtent: 150,
padding: const EdgeInsets.all(4),
mainAxisSpacing: 4,
crossAxisSpacing: 4,
children: _buildGridTileList(30));
// The images are saved with names pic0.jpg, pic1.jpg...pic29.jpg.
// The List.generate() constructor allows an easy way to create
// a list when objects have a predictable naming pattern.
List _buildGridTileList(int count) => List.generate(
count, (i) => Container(child: Image.asset('images/pic$i.jpg')));
效果图:
ListView
是一个用于显示多行的Widget
,当内容多到其容器不能一次性显示完全的时候会自动提供滚动机制。
Widget
Column
配置更加简单,而且使用滚动机制会更方便使用ListView
显示一个企业列表:
Widget _buildList() => ListView(
children: [
_tile('CineArts at the Empire', '85 W Portal Ave', Icons.theaters),
_tile('The Castro Theater', '429 Castro St', Icons.theaters),
_tile('Alamo Drafthouse Cinema', '2550 Mission St', Icons.theaters),
_tile('Roxie Theater', '3117 16th St', Icons.theaters),
_tile('United Artists Stonestown Twin', '501 Buckingham Way',
Icons.theaters),
_tile('AMC Metreon 16', '135 4th St #3000', Icons.theaters),
Divider(),
_tile('Kescaped_code#39;s Kitchen', '757 Monterey Blvd', Icons.restaurant),
_tile('Emmyescaped_code#39;s Restaurant', '1923 Ocean Ave', Icons.restaurant),
_tile(
'Chaiya Thai Restaurant', '272 Claremont Blvd', Icons.restaurant),
_tile('La Ciccia', '291 30th St', Icons.restaurant),
],
);
ListTile _tile(String title, String subtitle, IconData icon) => ListTile(
title: Text(title,
style: TextStyle(
fontWeight: FontWeight.w500,
fontSize: 20,
)),
subtitle: Text(subtitle),
leading: Icon(
icon,
color: Colors.blue[500],
),
);
对应效果图:
使用stack
在基Widget
(通常是Image
)上排列Widget
。Widget
可以完全或部分地与基Widget
重叠。
Widget
需要位于另一个Widget
的上层时使用Widget
是基Widget
,之后的子Widget
覆盖在该基Widget
的顶部。Widget
借助Stack
实现将一个Container
(在半透明黑色背景上显示text
)覆盖在CircleAvatar
的顶部:
Widget _buildStack() => Stack(
alignment: const Alignment(0.6, 0.6),
children: [
CircleAvatar(
backgroundImage: AssetImage('images/pic.jpg'),
radius: 100,
),
Container(
decoration: BoxDecoration(
color: Colors.black45,
),
child: Text(
'Mia B',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
],
);
效果图:
来自Material
库的Card Widget
用来包裹相关的信息块,被包裹的信息块可以由几乎任何Widget
组成,但通常与ListTile
一起使用。Card
只有一个child
属性,即被包裹的Widget
,但它的这个child
属性可以接收支持包含多个子Widget
的Row
、Column
、ListView
、GridView
的Widget
。默认情况下,如果不设置Card
的child
属性,即不被设置Card
包裹的Widget
,Card
的大小将缩小到0 x 0像素,但可以使用sizedbox
限制Card
的大小。
在Flutter
中,一个Card
默认有轻微的圆角和阴影,这样使其具有3D
效果。更改Card
的Elevation
属性可以控制阴影的效果。例如,将“elevation”
设置为24,可以从视觉上将Card
提升到离表面更远的位置,并使阴影变得更加分散。
Material Card
的实现类Row
、Column
或其他包含子项列表的Widget
Card
中的内容无法滚动Material
库使用一个SizedBox包裹的包含3个列表块的Card,Card中使用一个Divider将第1个和后2个列表块分割开:
Widget _buildCard() => SizedBox(
height: 210,
child: Card(
child: Column(
children: [
ListTile(
title: Text('1625 Main Street',
style: TextStyle(fontWeight: FontWeight.w500)),
subtitle: Text('My City, CA 99984'),
leading: Icon(
Icons.restaurant_menu,
color: Colors.blue[500],
),
),
Divider(),
ListTile(
title: Text('(408) 555-1212',
style: TextStyle(fontWeight: FontWeight.w500)),
leading: Icon(
Icons.contact_phone,
color: Colors.blue[500],
),
),
ListTile(
title: Text('[email protected]'),
leading: Icon(
Icons.contact_mail,
color: Colors.blue[500],
),
),
],
),
),
);
效果图:
ListTile
是一个Material
库中的特殊行(Row
),该Widget
的特性是可以很容易地创建最多包括3行Text
和可选引导(leading
)、可选尾标(trailing icon
)的widget
。ListTile
大多数情况下被用在Card
或者ListView
中。
Row
,可以包含最多3行Text
以及引导、尾标Row
更易配置,使用起来更简单Material
库中的Widget
上面Card中的实例就是赤裸裸的ListTile
的使用: