用Flutter构建漂亮的UI界面 - 基础组件篇 · Issue #12 · SmallStoneSK/Blog · GitHub
source link: https://github.com/SmallStoneSK/Blog/issues/12?
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
Flutter
作为时下最流行的技术之一,凭借其出色的性能以及抹平多端的差异优势,早已引起大批技术爱好者的关注,甚至一些闲鱼
,美团
,腾讯
等大公司均已开始使用。虽然目前其生态还没有完全成熟,但身靠背后的Google
加持,其发展速度已经足够惊人,可以预见将来对Flutter
开发人员的需求也会随之增长。
无论是为了现在的技术尝鲜还是将来的潮流趋势,都9102年了,作为一个前端开发者,似乎没有理由不去尝试它。正是带着这样的心理,笔者也开始学习Flutter
,同时建了一个用于练习的仓库,后续所有代码都会托管在上面,欢迎star,一起学习。
今天分享的是Flutter中最常用到的一些基础组件,它们是构成UI界面的基础元素:容器
,行
,列
,绝对定位布局
,文本
,图片
和图标
等。
2. 基础组件
2.1 Container(容器组件)
Container
组件是最常用的布局组件之一,可以认为它是web开发中的div
,rn开发中的View
。其往往可以用来控制大小、背景颜色、边框、阴影、内外边距和内容排列方式等。我们先来看下其构造函数:
Container({ Key key, double width, double height, this.margin, this.padding, Color color, this.alignment, BoxConstraints constraints, Decoration decoration, this.foregroundDecoration, this.transform, this.child, })
2.1.1 width
,height
,margin
,padding
这些属性的含义和我们已经熟知的并没有区别。唯一需要注意的是,margin
和padding
的赋值不是一个简单的数字,因为其有left
, top
, right
, bottom
四个方向的值需要设置。Flutter
提供了EdgeInsets
这个类,帮助我们方便地生成四个方向的值。通常情况下,我们可能会用到EdgeInsets
的4种构造方法:
EdgeInsets.all(value)
: 用于设置4个方向一样的值;EdgeInsets.only(left: val1, top: val2, right: val3, bottom: val4)
: 可以单独设置某个方向的值;EdgeInsets.symmetric(horizontal: val1, vertical: val2)
: 用于设置水平/垂直方向上的值;EdgeInsets.fromLTRB(left, top, right, bottom)
: 按照左上右下的顺序设置4个方向的值。
2.1.2 color
该属性的含义是背景颜色,等同于web/rn中的backgroundColor。需要注意的是Flutter
中有一个专门表示颜色的Color
类,而非我们常用的字符串。不过我们可以非常轻松地进行转换,举个栗子:
在web/rn中我们会用'#FF0000'
或'red'
来表示红色,而在Flutter中,我们可以用Color(0xFFFF0000)
或Colors.red
来表示。
2.1.3 alignment
该属性是用来决定Container
组件的子组件将以何种方式进行排列(PS:再也不用为怎么居中操心了)。其可选值通常会用到:
Alignment.topLeft
: 左上Alignment.topCenter
: 上中Alignment.topRight
: 右上Alignment.centerLeft
: 左中Alignment.center
: 居中Alignment.centerRight
: 右中Alignment.bottomLeft
: 左下Alignment.bottomCenter
: 下中Alignment.bottomRight
: 右下
2.1.4 constraints
在web/rn中我们通常会用minWidth
/maxWidth
/minHeight
/maxHeight
等属性来限制容器的宽高。在Flutter
中,你需要使用BoxConstraints
(盒约束)来实现该功能。
// 容器的大小将被限制在[100*100 ~ 200*200]内 BoxConstraints( minWidth: 100, maxWidth: 200, minHeight: 100, maxHeight: 200, )
2.1.5 decoration
该属性非常强大,字面意思是装饰,因为通过它你可以设置边框
,阴影
,渐变
,圆角
等常用属性。BoxDecoration
继承自Decoration
类,因此我们通常会生成一个BoxDecoration
实例来设置这些属性。
1) 边框
可以用Border.all
构造函数直接生成4条边框,也可以用Border
构造函数单独设置不同方向上的边框。不过令人惊讶的是官方提供的边框竟然不支持虚线
(issue在这里)。
// 同时设置4条边框:1px粗细的黑色实线边框 BoxDecoration( border: Border.all(color: Colors.black, width: 1, style: BorderStyle.solid) ) // 设置单边框:上边框为1px粗细的黑色实线边框,右边框为1px粗细的红色实线边框 BoxDecoration( border: Border( top: BorderSide(color: Colors.black, width: 1, style: BorderStyle.solid), right: BorderSide(color: Colors.red, width: 1, style: BorderStyle.solid), ), )
2) 阴影
阴影属性和web中的boxShadow
几乎没有区别,可以指定x
,y
,blur
,spread
,color
等属性。
BoxDecoration( boxShadow: [ BoxShadow( offset: Offset(0, 0), blurRadius: 6, spreadRadius: 10, color: Color.fromARGB(20, 0, 0, 0), ), ], )
3) 渐变
如果你不想容器的背景颜色是单调的,可以尝试用gradient
属性。Flutter
同时支持线性渐变
和径向渐变
:
// 从左到右,红色到蓝色的线性渐变 BoxDecoration( gradient: LinearGradient( begin: Alignment.centerLeft, end: Alignment.centerRight, colors: [Colors.red, Colors.blue], ), ) // 从中心向四周扩散,红色到蓝色的径向渐变 BoxDecoration( gradient: RadialGradient( center: Alignment.center, colors: [Colors.red, Colors.blue], ), )
4) 圆角
通常情况下,你可能会用到BorderRadius.circular
构造函数来同时设置4个角的圆角,或是BorderRadius.only
构造函数来单独设置某几个角的圆角:
// 同时设置4个角的圆角为5 BoxDecoration( borderRadius: BorderRadius.circular(5), ) // 设置单圆角:左上角的圆角为5,右上角的圆角为10 BoxDecoration( borderRadius: BorderRadius.only( topLeft: Radius.circular(5), topRight: Radius.circular(10), ), )
2.1.6 transform
transform
属性和我们在web/rn中经常用到的基本也没有差别,主要包括:平移
,缩放
、旋转
和倾斜
。在Flutter中,封装了矩阵变换类Matrix4
帮助我们进行变换:
translationValues(x, y, z)
: 平移x, y, z;rotationX(radians)
: x轴旋转radians弧度;rotationY(radians)
: y轴旋转radians弧度;rotationZ(radians)
: z轴旋转radians弧度;skew(alpha, beta)
: x轴倾斜alpha度,y轴倾斜beta度;skewX(alpha)
: x轴倾斜alpha度;skewY(beta)
: y轴倾斜beta度;
2.1.7 小结
Container
组件的属性很丰富,虽然有些用法上和web/rn有些许差异,但基本上大同小异,所以过渡起来也不会有什么障碍。另外,由于Container
组件是单子节点组件,也就是只允许子节点有一个。所以在布局上,很多时候我们会用Row
和Column
组件进行行
/列
布局。
2.2 Row/Column(行/列组件)
Row
和Column
组件其实和web/rn中的Flex布局
(弹性盒子)特别相似,或者我们可以就这么理解。使用Flex布局
的同学对主轴
和次轴
的概念肯定都已经十分熟悉,Row
组件的主轴就是横向,Column
组件的主轴就是纵向。且它们的构造函数十分相似(已省略不常用属性):
Row({ Key key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, MainAxisSize mainAxisSize = MainAxisSize.max, List<Widget> children = const <Widget>[], }) Column({ Key key, MainAxisAlignment mainAxisAlignment = MainAxisAlignment.start, CrossAxisAlignment crossAxisAlignment = CrossAxisAlignment.center, MainAxisSize mainAxisSize = MainAxisSize.max, List<Widget> children = const <Widget>[], })
2.2.1 mainAxisAlignment
该属性的含义是主轴排列方式,根据上述构造函数可以知道Row
和Column
组件在主轴方向上默认都是从start开始,也就是说Row
组件默认从左到右开始排列子组件,Column
组件默认从上到下开始排列子组件。
当然,你还可以使用其他的可选值:
- MainAxisAlignment.start
- MainAxisAlignment.end
- MainAxisAlignment.center
- MainAxisAlignment.spaceBetween
- MainAxisAlignment.spaceAround
- MainAxisAlignment.spaceEvenly
2.2.2 crossAxisAlignment
该属性的含义是次轴排列方式,根据上述构造函数可以知道Row
和Column
组件在次轴方向上默认都是居中。
这里有一点需要特别注意:由于Column
组件次轴方向上(即水平)默认是居中对齐,所以水平方向上不会撑满其父容器,此时需要指定CrossAxisAlignment.stretch
才可以。
另外,crossAxisAlignment其他的可选值有:
- crossAxisAlignment.start
- crossAxisAlignment.end
- crossAxisAlignment.center
- crossAxisAlignment.stretch
- crossAxisAlignment.baseline
2.2.3 mainAxisSize
字面意思上来说,该属性指的是在主轴上的尺寸。其实就是指在主轴方向上,是包裹其内容,还是撑满其父容器。它的可选值有MainAxisSize.min
和MainAxisSize.max
。由于其默认值都是MainAxisSize.max
,所以主轴方向上默认大小都是尽可能撑满父容器的。
2.2.4 小结
由于Row
/Column
组件和我们熟悉的Flex布局
非常相似,所以上手起来非常容易,几乎零学习成本。
2.3 Stack/Positoned(绝对定位布局组件)
绝对定位布局在web/rn开发中也是使用频率较高的一种布局方式,Flutter
也提供了相应的组件实现,需要将Stack
和Positioned
组件搭配在一起使用。比如下方的这个例子就是创建了一个黄色的盒子,并且在其四个角落放置了4个红色的小正方形。Stack
组件就是绝对定位的容器,Positioned
组件通过left
,top
,right
,bottom
四个方向上的属性值来决定其在父容器中的位置。
Container( height: 100, color: Colors.yellow, child: Stack( children: <Widget>[ Positioned( left: 10, top: 10, child: Container(width: 10, height: 10, color: Colors.red), ), Positioned( right: 10, top: 10, child: Container(width: 10, height: 10, color: Colors.red), ), Positioned( left: 10, bottom: 10, child: Container(width: 10, height: 10, color: Colors.red), ), Positioned( right: 10, bottom: 10, child: Container(width: 10, height: 10, color: Colors.red), ), ], ), )
2.4 Text(文本组件)
Text
组件也是日常开发中最常用的基础组件之一,我们通常用它来展示文本信息。来看下其构造函数(已省略不常用属性):
const Text( this.data, { Key key, this.style, this.textAlign, this.softWrap, this.overflow, this.maxLines, })
data
: 显示的文本信息;style
: 文本样式,Flutter
提供了一个TextStyle
类,最常用的fontSize
,fontWeight
,color
,backgroundColor
和shadows
等属性都是通过它设置的;textAlign
: 文字对齐方式,常用可选值有TextAlign
的left
,right
,center
和justify
;softWrap
: 文字是否换行;overflow
: 当文字溢出的时候,以何种方式处理(默认直接截断)。可选值有TextOverflow
的clip
,fade
,ellipsis
和visible
;maxLines
: 当文字超过最大行数还没显示完的时候,就会根据overflow
属性决定如何截断处理。
Flutter
的Text
组件足够灵活,提供了各种属性让我们定制,不过一般情况下,我们更多地只需下方几行代码就足够了:
Text( '这是测试文本', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, color: Color(0xFF999999), ), )
除了上述的应用场景外,有时我们还会遇到富文本
的需求(即一段文本中,可能需要不同的字体样式)。比如在一些UI设计中经常会遇到表示价格的时候,¥
符号比金额
的字号小点。对于此类需求,我们可以用Flutter
提供的Text.rich
构造函数来创建相应的文本组件:
Text.rich(TextSpan( children: [ TextSpan( '¥', style: TextStyle( fontSize: 12, color: Color(0xFFFF7528), ), ), TextSpan( '258', style: TextStyle( fontSize: 15, color: Color(0xFFFF7528), ), ), ] ))
2.5 Image(图片组件)
Image
图片组件作为丰富内容的基础组件之一,日常开发中的使用频率也非常高。看下其构造函数(已省略不常用属性):
Image({ Key key, @required this.image, this.width, this.height, this.color, this.fit, this.repeat = ImageRepeat.noRepeat, })
image
: 图片源,最常用到主要有两种(AssetImage
和NetworkImage
)。使用AssetImage
之前,需要在pubspec.yaml
文件中声明好图片资源,然后才能使用;而NextworkImage
指定图片的网络地址即可,主要是在加载一些网络图片时会用到;width
: 图片宽度;height
: 图片高度;color
: 图片的背景颜色,当网络图片未加载完毕之前,会显示该背景颜色;fit
: 当我们希望图片根据容器大小进行适配而不是指定固定的宽高值时,可以通过该属性来实现。其可选值有BoxFit
的fill
,contain
,cover
,fitWidth
,fitHeight
,none
和scaleDown
;repeat
: 决定当图片实际大小不足指定大小时是否使用重复效果。
另外,Flutter
还提供了Image.network
和Image.asset
构造函数,其实是语法糖。比如下方的两段代码结果是完全一样的:
Image( image: NetworkImage('https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg'), width: 100, height: 100, ) Image.network( 'https://ss1.bdstatic.com/70cFuXSh_Q1YnxGkpoWK1HF6hhy/it/u=1402367109,4157195964&fm=27&gp=0.jpg', width: 100, height: 100, )
2.6 Icon
(图标组件)
Icon
图标组件相比于图片有着放大不会失真的优势,在日常开发中也是经常会被用到。Flutter
更是直接内置了一套Material
风格的图标(你可以在这里预览所有的图标类型)。看下构造函数:
const Icon( this.icon, { Key key, this.size, this.color, })
icon
: 图标类型;size
: 图标大小;color
: 图标颜色。
3. 布局实战
通过上一节的介绍,我们对Container
,Row
,Column
,Stack
,Positioned
,Text
,Image
和Icon
组件有了初步的认识。接下来,就让我们通过一个实际的例子来加深理解和记忆。
3.1 准备工作 - 数据类型
根据上述卡片中的内容,我们可以定义一些字段。为了规范开发流程,我们先给卡片定义一个数据类型的类,这样在后续的开发过程中也能更好地对数据进行Mock和管理:
class PetCardViewModel { /// 封面地址 final String coverUrl; /// 用户头像地址 final String userImgUrl; /// 用户名 final String userName; /// 用户描述 final String description; /// 话题 final String topic; /// 发布时间 final String publishTime; /// 发布内容 final String publishContent; /// 回复数量 final int replies; /// 喜欢数量 final int likes; /// 分享数量 final int shares; const PetCardViewModel({ this.coverUrl, this.userImgUrl, this.userName, this.description, this.topic, this.publishTime, this.publishContent, this.replies, this.likes, this.shares, }); }
3.2 搭建骨架,布局拆分
根据给的视觉图,我们可以将整体进行拆分,一共划分成4个部分:Cover
,UserInfo
,PublishContent
和InteractionArea
。为此,我们可以搭起代码的基本骨架:
class PetCard extends StatelessWidget { final PetCardViewModel data; const PetCard({ Key key, this.data, }) : super(key: key); Widget renderCover() { } Widget renderUserInfo() { } Widget renderPublishContent() { } Widget renderInteractionArea() { } @override Widget build(BuildContext context) { return Container( margin: EdgeInsets.all(16), decoration: BoxDecoration( color: Colors.white, borderRadius: BorderRadius.circular(8), boxShadow: [ BoxShadow( blurRadius: 6, spreadRadius: 4, color: Color.fromARGB(20, 0, 0, 0), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: <Widget>[ this.renderCover(), this.renderUserInfo(), this.renderPublishContent(), this.renderInteractionArea(), ], ), ); } }
3.3 封面区域
为了更好的凸现图片的效果,这里加了一个蒙层,所以此处刚好可以用得上Stack
/Positioned
布局和LinearGradient
渐变,Dom结构如下:
Widget renderCover() { return Stack( fit: StackFit.passthrough, children: <Widget>[ ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), ), child: Image.network( data.coverUrl, height: 200, fit: BoxFit.fitWidth, ), ), Positioned( left: 0, top: 100, right: 0, bottom: 0, child: Container( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color.fromARGB(0, 0, 0, 0), Color.fromARGB(80, 0, 0, 0), ], ), ), ), ), ], ); }
3.4 用户信息区域
用户信息区域就非常适合使用Row
和Column
组件来进行布局,Dom结构如下:
Widget renderUserInfo() { return Container( margin: EdgeInsets.only(top: 16), padding: EdgeInsets.symmetric(horizontal: 16), child: Row( crossAxisAlignment: CrossAxisAlignment.start, mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Row( children: <Widget>[ CircleAvatar( radius: 20, backgroundColor: Color(0xFFCCCCCC), backgroundImage: NetworkImage(data.userImgUrl), ), Padding(padding: EdgeInsets.only(left: 8)), Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Text( data.userName, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xFF333333), ), ), Padding(padding: EdgeInsets.only(top: 2)), Text( data.description, style: TextStyle( fontSize: 12, color: Color(0xFF999999), ), ), ], ), ], ), Text( data.publishTime, style: TextStyle( fontSize: 13, color: Color(0xFF999999), ), ), ], ), ); }
3.5 发布内容区域
通过这块区域的UI练习,我们可以实践Container
组件设置不同的borderRadius
,以及Text
组件文本内容超出时的截断处理,Dom结构如下:
Widget renderPublishContent() { return Container( margin: EdgeInsets.only(top: 16), padding: EdgeInsets.symmetric(horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( margin: EdgeInsets.only(bottom: 14), padding: EdgeInsets.symmetric(horizontal: 8, vertical: 2), decoration: BoxDecoration( color: Color(0xFFFFC600), borderRadius: BorderRadius.only( topRight: Radius.circular(8), bottomLeft: Radius.circular(8), bottomRight: Radius.circular(8), ), ), child: Text( '# ${data.topic}', style: TextStyle( fontSize: 12, color: Colors.white, ), ), ), Text( data.publishContent, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: 15, fontWeight: FontWeight.bold, color: Color(0xFF333333), ), ), ], ), ); }
3.6 互动区域
在这个模块,我们会用到Icon
图标组件,可以控制其大小和颜色等属性,Dom结构如下:
Widget renderInteractionArea() { return Container( margin: EdgeInsets.symmetric(vertical: 16), padding: EdgeInsets.symmetric(horizontal: 16), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ Row( children: <Widget>[ Icon( Icons.message, size: 16, color: Color(0xFF999999), ), Padding(padding: EdgeInsets.only(left: 6)), Text( data.replies.toString(), style: TextStyle( fontSize: 15, color: Color(0xFF999999), ), ), ], ), Row( children: <Widget>[ Icon( Icons.favorite, size: 16, color: Color(0xFFFFC600), ), Padding(padding: EdgeInsets.only(left: 6)), Text( data.likes.toString(), style: TextStyle( fontSize: 15, color: Color(0xFF999999), ), ), ], ), Row( children: <Widget>[ Icon( Icons.share, size: 16, color: Color(0xFF999999), ), Padding(padding: EdgeInsets.only(left: 6)), Text( data.shares.toString(), style: TextStyle( fontSize: 15, color: Color(0xFF999999), ), ), ], ), ], ), ); }
3.7 小结
通过上面的一个例子,我们成功地把一个看起来复杂的UI界面一步步拆解,将之前提到的组件都用了个遍,并且最终得到了不错的效果。其实,日常开发中90%以上的需求都离不开上述提到的基础组件。因此,只要稍加练习,熟悉了Flutter
中的基础组件用法,就已经算是迈出了一大步哦~
这里还有银行卡和朋友圈的UI练习例子,由于篇幅原因就不贴代码了,可以去github仓库看。
本文首先介绍了Flutter
中构建UI界面最常用的基础组件(容器
,行
,列
,绝对定位布局
,文本
,图片
和图标
)用法。接着,介绍了一个较复杂的UI实战例子。通过对Dom结构的层层拆解,前文提到过的组件得到一个综合运用,也算是巩固了前面所学的概念知识。
不过最后不得不吐槽一句:Flutter
的嵌套真的很难受。。。如果不对UI布局进行模块拆分,那绝对是噩梦般的体验。而且不像web/rn开发样式可以单独抽离,Flutter
这种将样式当做属性的处理方式,一眼看去真的很难理清dom结构,对于新接手代码的开发人员而言,需要费点时间理解。。。
Recommend
-
33
Comments Owner
-
8
在我们使用Vue+Element开发前端的时候,往往涉及到很多界面组件的使用,其中很多直接采用Element官方的案例即可,有些则是在这个基础上封装更好利用、更少代码的组件;另外有些则是直接采用第三方开发好的组件,目的就是实现所需功能外,尽量简化界面使用代码。...
-
6
循序渐进VUE+Element 前端应用开发(25)--- 各种界面组件的使用(1) 在我们使用Vue+Element开发前端的时候...
-
4
Flutter工程笔记 | 最基础的视频组件,一点也不基础……厦门量潮科技有限公司 执行董事最近在写我们自己的课程平台“量潮课堂”,前端使用Flutter写Web PC/Mobile、PC Desktop(Window...
-
2
一个更漂亮的Swagger界面——RapiDoc 今天看了一下有人推荐了一个swagger的UI
-
7
对于 ios 老手来说,漂亮的界面是用 storyboard 做吗?还是直接 coding? V2EX = way to explore V2EX 是一个关于分享和探索的地方 iOS 开发实用技术导...
-
6
如何做出漂亮实用的后台管理UI界面? 6月 8, 2021 发表于: 视觉设计. 评论...
-
5
基于Vue的前端框架有很多,Element算一个,而BootstrapVue也可以非常不错的一个,毕竟Bootstrap也是CSS中的大佬级别的,它和Vue的整合,使得开发起来更加方便了。BootstrapVue 是基于 Bootstrap v4 + Vue.js 的前端 UI 框架。它是流行的
-
5
今天推荐一个界面简洁、美观的、支持国际化开源音频播放器。 项目简介 这是一个基于C# + WPF开发的,界面外观简洁大方,操作体验良好的音频播放器。 支持各种音频格式,包括:MP4、WMA、OGG、FLAC、M4A、AAC...
-
7
14.4K Star,一款漂亮、快速、现代化的开源UI组件库
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK