Flutter第一部分(UI)第三篇:简单几步带你构建一个漂亮的UI实例
前言:Flutter系列的文章我应该会持续更新至少一个月左右,从User Interface(UI)到数据相关(文件、数据库、网络)再到Flutter进阶(平台特定代码编写、测试、插件开发等),欢迎感兴趣的读者持续关注(可以扫描左边栏二维码或者微信搜索”IT工匠“关注微信公众号哦,会同步推送)。
摘要
本文通过一个简单的例子来逐步为大家介绍如何在Flutter
中构建漂亮的布局,通过本文你将会了解到以下几点:
-
Flutter
的布局机制是如何工作的 - 如何在垂直方向和水平方向布局
Widget
- 如何在
Flutter
中进行Widget
的布局
本文档主要介绍如何在Flutter
中进行布局,你将最终会构建一个下图这样的页面:
本文将一步一步带你构建一个像上图那样的页面。
第0步:创建一个Flutter
项目
-
创建一个
Flutter
项目 -
将该项目的app bar的标题和app的标题设置为下面这样:
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'), ), )); }
设置完成后运行起来效果如下图所示:
第1步:分析页面的布局逻辑
第一步是将整个页面布局分解为基本元素,主要从以下几点入手
- 辨识行(
Row
)和列(Column
) - 布局中是否存在网格(
grid
)? - 是否存在
Widget
之间的覆盖? - 整个UI布局是否需要
tab
栏? - 关注需要对其(
alignment
)、填充(padding
)、边框(border
)的区域
那么我们先来看一下整个页面的主要组成部分:
![Column elements] (http://picture-pool.oss-cn-beijing.aliyuncs.com/2019-06-10-051413.png)
可以看到,整个页面的主要组成部分就是由红色框标注出来的4部分,这4部分位于同一个列(Column
),分别是:1个Image
、2个Row
、1个Text
。
再深入地分析一下每一个行(Row
):
-
第1行,也就是标题栏(Title section),它有3列:1个由2行text组成的列(Column)、1个星星的Icon、1个数字:
可以看到,由于第1个列(
Column
)占据了整个行的大部分空间,所以其应该被Expanded Widget
包裹。 -
第二行,我们也就是按钮列(
button section
),同样也包含了3个子元素,每一个子元素都是1个行,行里面的内容是1个icon
和1个text
: -
第三行:第三行没有很复杂的构成,就是单纯的
text
块。
经过以上的分析,我们将一个复杂的页面分解为了多个简单的组成部分,这样可以简化整个页面的实现,为了避免布局代码的混乱,我们应该利用变量和函数来构建布局的子部分,这一点我会在下面的代码中进行演示。
第2步:实现第一行(title Row
)
直接给出代码,具体的解释在代码的注释中,大家注意看注释:
Widget titleSection = Container(
//为整个Widget(即这一行)的所有所有方向设置32px的填充
padding: const EdgeInsets.all(32),
child: Row(
children: [
Expanded(
/**
将Column放置在Expanded中,由于Expanded会默认占据所有当前Row的空闲可用空间,所以这个Column也会自然被拉伸的占据完所有当前Row可用的空闲空间。
*/
child: Column(
/**将Column的crossAxisAlignment属性设置为CrossAxisAlignment.start以保证这个列中的元素(即children属性中的Widget)在水平方向上排列在当前Column的起始位置
*/
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/**
将这个Text放在Container中的目的是通过Container来添加填充(padding)
*/
Container(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Oeschinen Lake Campground',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
),
Text(
'Kandersteg, Switzerland',
style: TextStyle(
color: Colors.grey[500],
),
),
],
),
),
/**
最后的2个元素分别是1个Icon和1个Text,分别用来显示星星和数字
*/
Icon(
Icons.star,
color: Colors.red[500],
),
Text('41'),
],
),
);
直接将上面的变量titleSection
放在在app body
中即可:
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter layout demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: Column(
children: <Widget>[titleSection],
)));
}
注意,这里通过使用变量titleSection来达到简化布局代码的目的,这样就不会导致MaterialApp(..)
中的代码太长、太混乱。
运行起来的效果如下图所示:
第3步:实现按钮行(button row
)
按钮行(button section
)有3个列,每个列都是同样的布局组成:1个Icon
、1个Text
。这一行中的列之间的间距都是均匀的,由于构建每一列的代码几乎相同,因此创建一个名为buildbuttoncolumn()
的私有方法,该方法接收一个颜色(Color
)参数、一个图标(Icon
)参数和1个文本(Text
)参数,并返回一个具有以给定颜色绘制的Widget
的列(Column
),代码如下:
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// ···
}
Column _buildButtonColumn(Color color, IconData icon, String label) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: color),
Container(
margin: const EdgeInsets.only(top: 8),
child: Text(
label,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w400,
color: color,
),
),
),
],
);
}
}
_buildButtonColumn()
方法将Icon
直接添加到Column
中,而Text
是先用Container
进行包裹,然后再将Container
添加到Column
中,这样做的目的是借助Container
为Text
设置顶部填充(padding
),这样就不至于Text
和Icon
距离过近。
完成了_buildButtonColumn()
函数之后,我们只需要在需要构建列(Column
)的时候调用该函数并传入对应的3个参数即可构建出我们需要的列:
Color color = Theme.of(context).primaryColor;
Widget buttonSection = Container(
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildButtonColumn(color, Icons.call, 'CALL'),
_buildButtonColumn(color, Icons.near_me, 'ROUTE'),
_buildButtonColumn(color, Icons.share, 'SHARE'),
],
),
);
注意这里设置Row
的mainAxisAlignment
属性值为MainAxisAlignment.spaceEvenly
,目的是让Row
中的列均匀地占满整个行的可用空间。
然后将变量buttonSection
放在app body
中:
return MaterialApp(
title: 'Flutter layout demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: Column(
children: <Widget>[titleSection, buttonSection],
)));
运行效果图如下图所示:
第4步:实现Text
部分
将Text
部分定义为一个变量,然后将Text
放置在一个Container
中,并为Container
设置padding
属性:
Widget textSection = Container(
padding: const EdgeInsets.all(32),
child: Text(
'Lake Oeschinen lies at the foot of the Blüemlisalp in the Bernese '
'Alps. Situated 1,578 meters above sea level, it is one of the '
'larger Alpine Lakes. A gondola ride from Kandersteg, followed by a '
'half-hour walk through pastures and pine forest, leads you to the '
'lake, which warms to 20 degrees Celsius in the summer. Activities '
'enjoyed here include rowing, and riding the summer toboggan run.',
softWrap: true,
),
);
注意这里设置softwrap
属性值为true
,这样可以保证文字可以在单词分界的地方换行而不是在单词中间换行。
然后将上面的textSection
变量放在app body
中:
return MaterialApp(
title: 'Flutter layout demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: Column(
children: <Widget>[titleSection, buttonSection, textSection],
)));
运行效果图如下图所示:
第5步:实现Image
部分
到现在为止我们已经完成了4行中的3行,只剩下图像那行还没实现,这一步我们来实现图像的显示:
-
在项目的*目录下创建一个
images
文件夹 -
将下图放在刚才创建的
images
文件夹下,命名为lake.jpg
-
更新
pubspec.yaml
文件,添加assets
标签,配置图片路径,这样就可以在代码中访问到你存放的图片:flutter: uses-material-design: true assets: - images/lake.jpg
然后我们就可以在代码中引用该图了:
return MaterialApp(
title: 'Flutter layout demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: Column(
children: <Widget>[
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
),
titleSection,
buttonSection,
textSection
],
)));
运行起来效果如下:
第6步:另一种尝试
我们来看前几步完成之后的最终代码:
return MaterialApp(
title: 'Flutter layout demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: Column(
children: <Widget>[
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
),
titleSection,
buttonSection,
textSection
],
)));
注意,我们将body
的属性值设置为了Column
,然后在Column
中放置了我们实现的几个子Widget
,现在我们换种方式,使用ListView
来取代这个Column
:
return MaterialApp(
title: 'Flutter layout demo',
debugShowCheckedModeBanner: false,
home: Scaffold(
appBar: AppBar(
title: Text('Flutter layout demo'),
),
body: ListView(
children: [
Image.asset(
'images/lake.jpg',
width: 600,
height: 240,
fit: BoxFit.cover,
),
titleSection,
buttonSection,
textSection
],
)));
效果如下:
可以看到,使用Column
和使用ListView
的静态视觉效果是一样的,那我们到底应该使用Column
还是ListView
呢?二者的区别在哪里呢?答案就在"动态",二者在动态下的效果是不一样的,再具体点,使用Column
构建的Widget
是不支持滚动的,即不支持进行上下或者左右的滚动事件,而使用ListView
会支持滚动事件,就像下面这样:
以上就是本文的所有内容,希望大家通过本文可以对Flutter
的布局有更深入的了解和认知,我们下篇文章再见~
上一篇: Compiz 0.9.0发布
下一篇: 鼠标前置