Flutter对UI控件的组织形式
在Dart中强调万物皆对象,而Flutter则强调万物皆Widget,Flutter的重点是如何简洁快速地构建UI界面并能同时应用到多种终端设备上,因此Flutter把Widget看得格外重要,以至于Flutter的程序从整体上看来就是一层一层的Widget嵌套堆叠起来的。 Flutter项目自动生成了一个Hello World项目,基本功能是右下角有一个悬浮按钮,点击时中间的数字会显示点击了多少次。从代码层面看,对于Android来说,程序的入口还是一个Activity,这是看到AndroidManifest.xml中定义的就是MainActivity,MainActivity的onCreate方法中调用了GeneratedPluginRegistrant#registerWith方法,这里就会进到Flutter的环境,运行main.dart文件中的main函数。main函数的实现直接调用了runApp函数,传入的参数为一个MyApp对象。MyApp继承自StatelessWidget,从这里开始就全部是Widget了。 Flutter有两种Widget,分别是StatelessWidget和StatefulWidget,表示的是无状态控件和有状态控件。之所以要有这两种Widget,是因为Flutter希望将控件的状态管理与UI管理完全隔离开,State更像是Android中的ViewModel,把控件的状态字段声明在State中,而真正的布局创建则是在State对象的build函数中,StatefulWidget要求实现类实现createState方法返回一个State对象,而State的实现类需要继承自State类,并将StatefulWidget的实现类作为其范型参数,State的widget变量就表示State中持有的StatefulWidget对象。State实现类需要实现build方法,这跟StatelessWidget一样,需要返回一个Widget对象,也就是这个Widget要实现的UI布局。 和其他UI组件库一样,Flutter对每个Widget也提供了手势监听器,例如Demo项目中就为悬浮按钮添加了点击监听器,监听函数是_incrementCounter,在这个方法中调用了State的setState函数,传入了一个闭包,在闭包中将_counter自加1。这个setState有点像react中的setState函数,都是更改状态字段后会自动更新UI。
颜色的使用
Flutter有四种颜色的定义方式
Color c = const Color(0xFF42A5F5);//16进制的ARGB
Color c = const Color.fromARGB(0xFF, 0x42, 0xA5, 0xF5);
Color c = const Color.fromARGB(255, 66, 165, 245);
Color c = const Color.fromRGBO(66, 165, 245, 1.0);//opacity:不透明度
或者可以直接使用Flutter预定义的颜色,Colors类中定义了若干静态常量Color对象,可以在代码中使用,就像使用Android中的Color.BLUDE那样。
Text组件的使用
Flutter的Text跟Android原生组件TextView类型,都是为了呈现简单文本的控件。Text继承自StatelessWidget,构造器参数列表为this.data, { Key key, this.style, this.strutStyle, this.textAlign, this.textDirection, this.locale, this.softWrap, this.overflow, this.textScaleFactor, this.maxLines, this.semanticsLabel, this.textWidthBasis, } 其中style为TextStyle类型,可为文字定义前景色和背景色,textAlign表示文字对齐方式,而textDirection表示文字方向,只有两个取值rtl和ltr,分别表示从左到右排列和从右到左排列。maxLines表示最大行数。
Alignment
Flutter用Alignment来表示对齐方式,还有继承自Alignment的FractionalOffset和与Alignment一样继承自AlignmentGeometry的AlignmentDirectional。其中Alignment构造器有x和y两个double参数,分别表示以组件中心为基准,x和y轴两个方向上的对齐,x负左正右,y负上正下,取值都是[-1,1];FractionalOffset则是以组件左上角为基准,这和Android中的标准坐标系一样,而参数则只有正数;AlignmentDirectional和Alignment比较相似,都是以组件的中心为基准,但和textDirection有关,默认textDirection为ltr,则负左正右,当textDirection为rtl时,则是负右正左了,但都是负上正下。
decoration相关
作为decoration属性的value,UnderlineTabIndicator见名知意,一般用于Tab下面的指示线,比如说当哪个Tab为active时,则Tab的下方会有一条横线表示它被选中了,同时,这个decoration也可以被用于任何Container。这个在实际应用中,跟Text的下划线看起来差不多。
decoration还可以选用BoxDecoration,表示四个边的边框,属性分别表示
color:颜色
image:图片
border:边框
borderRadius:圆角半径
boxShadow:阴影
gradient:渐变
backgroundBlendMode:背景融合模式
shape:形状
单容器控件
Flutter的容器分两种,一种是多子Widget容器控件,类似于Android原生中的ViewGroup,其children属性是个数组。而另一类则是单子Widget容器控件,主要是为了方便子Widget管理样式,子Widget在其child属性中声明。
最常用的单容器控件是Container,就是简单的容器,有属性
alignment:对齐方式
padding:内边距
width:宽度
height:高度
margin:外边距
color:背景颜色
decoration:背景样式,各种边框样式
foregroundDecoration:前景样式
constraints:宽高约束
transform:旋转平移等3D操作
child:子Widget
demo见simpleContainerDemo
Padding容器也比较简单,只能设置padding属性来确定内边距
Center被用于快速确定子View位于父View的居中,并且还能用widthFactor和heightFactor来设置Center的高度和宽度,Center的width = 子控件的width* widthFactor,Center的 height = 子控件的 height* heightFactor
Align是用于快速确定子View位于父View的对齐位置的,其中alignment可使用Alignment的值,并且Align也支持widthFactor和heightFactor属性来定制Align的size
FittedBox用于确定填充类型
AspectRatio用于确定固定宽高比
Flutter中也有跟Android一样的约束布局的容器ConstrainedBox,可以设置最大或最小size,例如设置最小高度为50,就算height设置为30,最终高度也会是50
多容器控件
既然有单容器控件就会有多容器控件,这类控件就跟Android原生中的ViewGroup一样,可以有多个child,其children是个数组。以排列方向为例,可分为Row和Column,分别是水平排列和竖直排列,也可以叫作行和列。Row和Column都有四个布局属性
mainAxisAligment:主轴对齐方式
crossAxisAlignment:次轴对齐方式
textDirection:排列方向,主要是指按序列是从左到右还是从右到左
verticalDirection:底部对齐还是顶部对齐
其中的主轴和次轴是相对而言的,Row的主轴是水平方向,次轴是竖直方向,而Column的主轴则是竖直方向,次轴是水平方向。mainAxisAligment和crossAxisAlignment属性的值都是MainAxisAlignment类型的,其中MainAxisAlignment有6种取值,分别是start、center、end、spaceBetween、spaceAround、spaceEvenly,而最后三种的区别是spaceBetween表示剩余区域平均分在各种子View中间,spaceAround表示剩余区域平均分给每个子View主轴两边,spaceEvenly表示每两个子View之间的间隔相等,包括最边上的子View与边的间距
GridView是个表格控件,分为主轴和次轴,主轴是scrollDirection属性指定的方向,子控件由children属性给出,是个Widget的集合,GridView还可以设置次轴的插件个数,这样children会先铺满次轴,也就是在次轴方向上一行一行或者一列一列地铺。
ListView跟Android原生中的ListView表现一致,但构造方式不一样,ListView也支持children,但如果想支持懒加载,则需要用到builder函数,通过index来构造子Widget。其中builder函数的itemExtent表示每一个item的高,如果不指定则高度由item自身决定,itemCount表示item的个数,index的范围是[0,itemCount),itemBuilder是一个函数,入参是BuildContext和index,返回值是一个Widget,当需要加载下一个item时,就会调用itemBuilder的函数。
Expanded是一种空白容器,主要作用是撑开剩余空间,只能在Row、Column和Flex中使用。如果子控件的宽度或高度之和达不到父控件的,那Expanded就会占据所有的剩余空间。
ExpansionTile是个分组容器,就像QQ的好友列表一样,ExpansionTile主要分为标题部分和展开内容部分,标题部分又分为title标题文字和leading标题图标以及trailing标题的展开箭头。而内容区则是children属性,是个Widget的集合。backgroundColor控制背景色,onExpansionChanged监听打开关闭事件,initiallyExpanded表示初始时是展开还是折叠状态,默认是折叠状态。
图片
Flutter的图片使用由Image类提供,Image提供了常用的图片加载static函数。Image.network用于加载网络图片,Image.asset用于加载asset的图片资源,Image.file用于加载sd card的图片,由于Flutter默认是不支持加载sd card文件的,所以需要使用path_provider库,而读写文件需要权限,Flutter又默认不支持权限请求,因此也要使用permission_handler。
Flutter在pubspec.yaml文件中配置工程使用的一些属性,例如assets资源和dart依赖库。在flutter的assets下配置asset资源,可以直接配置资源所在的目录,例如在工程根目录下创建imgs目录用于存放asset图片资源,则可在assets下配置- imgs/,这样就可加载上imgs目录下的所有图片。而依赖库配置则是在dependencies下配置,默认已经配置好了flutter sdk,我们只需要再添加上我们所需要的第三方库即可。
关于使用
Widget imgDemo() {
_getLocalFile();
return Column(children: [
Row(children: <Widget>[Text("Image.network", style: demoTextStyle,), Image.network("http://pic1.win4000.com/pic/c/cf/cdc983699c.jpg", height: 200,)],),
Row(children: <Widget>[Text("Image.asset", style: demoTextStyle,), Image.asset("imgs/girl.jpg", height: 200,)],),
Row(children: <Widget>[Text("Image.file", style: demoTextStyle,), Image.file(File("$_storageDir/news_article/test.jpg"), height: 200,)],)
]);
}
加载sd card的图片资源较为麻烦,是异步加载的,因此需要使用dart的异步编程,
Future<bool> _requestPermissions() async {
Map<PermissionGroup, PermissionStatus> permissions =
await PermissionHandler().requestPermissions([
PermissionGroup.storage,
]);
List<bool> results = permissions.values.toList().map((status) {
return status == PermissionStatus.granted;
}).toList();
return !results.contains(false);
}
String _storageDir = '';
_getLocalFile() async {
if (!await _requestPermissions()) {
return "";
}
String appDir = (await getApplicationDocumentsDirectory()).path;
String tempDir = (await getTemporaryDirectory()).path;
String storageDir = (await getExternalStorageDirectory()).path;
setState(() {
_storageDir = storageDir;
});
return storageDir;
}
使用_storageDir来保存sd card根目录的值,在异步函数中获取到其值时,调用setState来同步给UI让界面刷新。这里还使用到了permission_handler来作动态权限请求。 除了普通图片,还有Icon我一般也认为是图片类,Icon的底层实现可能有多种,但说到底Icon还是一种图形,也应该被划到图片类中。Flutter的Icon由Icons提供,已经内置了非常多的Icon图标。更多Icon的查找可以前往material.io/tools/icons…
单选复选和开关控件
Flutter的单选框是Radio,其有四个属性 value、groupValue,控制是否选中,当value=groupValue时为选中 onChanged选中状态变化时的回调 activeColor选中状态下的颜色 materialTapTargetSize表示点击区域 Radio不能给title,因此就有了RadioListTile,可以在Radio的右边添加上title
Widget radioDemo() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Radio(value: "red",
groupValue: _radioValue,
activeColor: Colors.red,
onChanged: (value) {
setState(() {
_radioValue = value;
});
}
),
Radio(value: "green",
groupValue: _radioValue,
activeColor: Colors.green,
onChanged: (value) {
setState(() {
_radioValue = value;
});
}
),
Radio(value: "blue",
groupValue: _radioValue,
activeColor: Colors.blue,
onChanged: (value) {
setState(() {
_radioValue = value;
});
}
),
],),
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
RadioListTile<String>(value: "red",
groupValue: _radioValue,
title: Text("red", style: demoTextStyle),
onChanged: (value) {
setState(() {
_radioValue = value;
});
}
),
RadioListTile<String>(value: "green",
groupValue: _radioValue,
title: Text("green", style: demoTextStyle),
onChanged: (value) {
setState(() {
_radioValue = value;
});
}
),
RadioListTile<String>(value: "blue",
groupValue: _radioValue,
title: Text("blue", style: demoTextStyle),
onChanged: (value) {
setState(() {
_radioValue = value;
});
}
)
],
)
],);
}
CheckBox和Radio的用法类似,但没有Radio的group,这是因为Radio是有分组的概念的,而CheckBox只能是单个控件。同样也提供了CheckboxListTile来为Checkbox增加title、subtitle和secondary
var _checkBoxValue = true;
var _checkBoxListTileValue = true;
Widget checkBoxDemo() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text("$_checkBoxValue, $_checkBoxListTileValue", style: demoTextStyle,),
Checkbox(value: _checkBoxValue, onChanged: (value) {
setState(() {
_checkBoxValue = value;
});
}),
CheckboxListTile(value: _checkBoxListTileValue, title: Text("CheckboxListTile", style: demoTextStyle), activeColor: Colors.blue, selected: true, onChanged: (value) {
setState(() {
_checkBoxListTileValue = value;
});
})
],
);
}
开关控件是Switch,总体分为track和thumb两部分,track表示Switch的底层,而thumb则表示开关的点击头部,Switch的诸多属性则是为了控制点击和track和thumb的样式。 value表示开关状态,只有true和false onChanged表示状态变化的回调 activeColor打开状态下的颜色,也即是打开状态下thumb的颜色 activeTrackColor打开状态下的track的颜色 inactivetrackColor关闭状态下tract的颜色 inactiveThumbColor关闭状态下的thumb的颜色 activeThumbImage打开状态下thumb的图片 inactiveThumbImage关闭状态下thumb的图片
var _switchValue = true;
Widget switchDemo() {
return Switch(value: _switchValue,
activeColor: Colors.blue,
activeTrackColor: Colors.green,
inactiveTrackColor: Colors.grey,
inactiveThumbColor: Colors.red,
inactiveThumbImage: AssetImage("imgs/girl.jpg"),
onChanged: (value) {
setState(() {
_switchValue = value;
});
}
);
}
滑动条和进度条
滑动条是Slider而进度条是ProgressIndicator,Slider是在进度条的基础上添加了一个滑块,因此我们可以与滑动条交互,也就是拖动滑动条。Slider有如下一些属性
value表示滑动条当前的滑动块位置
onChanged表示值变化时的回调
onChangeStart开始滑动时回调一次
onChangeEnd滑动结束时回调一次
min表示最小值,max表示最大值
divisions表示整个滑动条分为几块,如果是5,则表示滑动条只能滑到5个位置
label表示一个文字提示,当滑动结果时,会在滑动条上显示一个气泡
activeColor表示滑动过的区域的颜色
inactiveColor表示未滑到的区域的颜色
进度条主要有CircularProgressIndicator和LinearProgressIndicator两种,用法基本上一样,属性如下
backgroundColor表示未到达的区域颜色
valueColor表示到达的区域颜色
value表示进度值,为0-1的double值
以下代码是用Slider来同时控制两种进度条的值的demo
var _sliderValue = 0.0;
Widget sliderProgressDemo() {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Slider(value: _sliderValue,
activeColor: Colors.green,
inactiveColor: Colors.grey,
min: 0.0,
max: 1.0,
divisions: 10,
label: "$_sliderValue",
onChanged: (value) {
setState(() {
_sliderValue = value;
});
}
),
Container(height: 100,
width: 100,
child: CircularProgressIndicator(backgroundColor: Colors.yellow,
value: _sliderValue,
valueColor: AlwaysStoppedAnimation(Colors.green),
semanticsLabel: "$_sliderValue",
),
),
LinearProgressIndicator(backgroundColor: Colors.yellow,
value: _sliderValue,
valueColor: AlwaysStoppedAnimation(Colors.green),
semanticsLabel: "$_sliderValue",)
],
);
}
关于如何自定义这些控件的大小,由于这些控件都是match_parent的,因此可以自定义其父控件的大小,如果没有,则加一层父控件即可,例如上面的demo就给CircularProgressIndicator包了一层Container并设置了其宽高为100,因此CircularProgressIndicator的宽高就变成了100,这样就能自定义这些控件的大小尺寸了。
Dialog相关
Flutter的Dialog都是基于showDialog函数来弹出的,showDialog函数有两个参数,分别是context和builder,context就是Widget的build函数的形参BuildContext,而builder参数是一个函数类型,形参是context,也是BuildContext类型的,返回值是Widget类型。Flutter SDK已经为我们实现了非常多的Dialog类型,都是继承自StatelessWidget,比如Android中常用的AlertDialog,其包括一个Text类型的title,一个Text类型的content,一个Widget数组类型的actions。AlertDialog是Android系统的Material风格的,对于iOS系统,常用的Dialog是CupertinoDialog,和AlertDialog的使用方式几乎一样,只不过是使用showCupertinoDialog。除了系统特定风格的Dialog,还可以直接使用Dialog来自定义布局,使用方式也跟AlertDialog一样,使用showDialog来弹出。
除了使用showDialog函数来弹出,还有一种sheet类型的屏幕底部弹出框,使用showBottomSheet函数来弹出,使用方式跟showDialog类似,但要求其是在Scaffold的父Widget中使用,并且要求Scaffold的body是个Builder类型的
Widget dialogDemo(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
FlatButton(onPressed: () {
showDialog(context: context, builder: (context) {
return AlertDialog(
title: Text("test", style: demoTextStyle),
content: Text("test Content", style: demoTextStyle),
actions: <Widget>[
FlatButton(onPressed: () {
Navigator.of(context).pop(true);
}, child: Text("取消", style: demoTextStyle)),
FlatButton(onPressed: () {
}, child: Text("确认", style: demoTextStyle)),
],
);
});
}, child: Text("Material Dialog", style: demoTextStyle,)),
FlatButton(onPressed: () {
showCupertinoDialog(context: context, builder: (context) {
return CupertinoAlertDialog(
title: Text("test", style: demoTextStyle),
content: Text("test Content", style: demoTextStyle),
actions: <Widget>[
FlatButton(onPressed: () {
Navigator.of(context).pop(true);
}, child: Text("取消", style: demoTextStyle)),
FlatButton(onPressed: () {
}, child: Text("确认", style: demoTextStyle)),
],
);
});
}, child: Text("Cupertino Dialog", style: demoTextStyle,)),
FlatButton(onPressed: () {
showDialog(context: context, builder: (context) {
return Dialog(child: Row(
children: <Widget>[
Icon(Icons.warning),
RaisedButton(onPressed: () {
Navigator.of(context).pop(true);
}, child: Text("cancel", style: demoTextStyle))
],
));
});
}, child: Text("custom Dialog", style: demoTextStyle,)),
FlatButton(onPressed: () {
showBottomSheet(context: context, builder: (context) {
return Container(
height: 200,
width: double.infinity,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
FlatButton(onPressed: () {
}, child: Text("1", style: demoTextStyle)),
FlatButton(onPressed: () {
}, child: Text("2", style: demoTextStyle)),
FlatButton(onPressed: () {
}, child: Text("3", style: demoTextStyle))
],
),);
});
}, child: Text("bottom sheet", style: demoTextStyle,))
],);
}
@override
Widget build(BuildContext context) {
// return colorDemo;
return MaterialApp(
title: "study",
home: Scaffold(
body: Builder(builder: (context) {
return Center(
child: Container(
color: Colors.white,
child: dialogDemo(context)
,),
);
})
)
);
}