#####简单样式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import 'package:flutter/material.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
title: 'flutter demo',
home: _home(),
);
}
}

class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}

class _homeState extends State<_home> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body: new Column(
//垂直布局
children: <Widget>[
//控件组
Text(
"Hello world",
textAlign: TextAlign.center,
//可以选择左对齐、右对齐还是居中。注意,对齐的参考系是Text widget本身。本例中虽然是指定了居中对齐,
// 但因为Text文本内容宽度不足一行,Text的宽度和文本内容长度相等,那么这时指定对齐方式是没有意义的,
// 只有Text宽度大于文本内容长度时指定此属性才有意义。
),
Text(
"Hello world! I'm Jack. " * 4,//文字重复4次
maxLines: 1,//最大行数
overflow: TextOverflow.ellipsis,//超出显示省略号
),
Text(
"Hello world",
textScaleFactor: 1.5,//代表文本相对于当前字体大小的缩放因子,相对于去设置文本的样式style属性的fontSize,它是调整字体大小的一个快捷方式。
// 该属性的默认值可以通过MediaQueryData.textScaleFactor获得,如果没有MediaQuery,那么会默认值将为1.0。
),
],
));
}
}

效果图
#####TextStyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
new Text(
'center',
style: TextStyle(
color: Colors.white,//字体颜色
fontSize: 18.0,
//fontSize:该属性和Text的textScaleFactor都用于控制字体大小。但是有两给主要区别:
//fontSize可以精确指定字体大小,而textScaleFactor只能通过缩放比例来控制。
//textScaleFactor主要是用于系统字体大小设置改变时对Flutter应用字体进行全局调整,而fontSize通常用于单个文本。
height: 1.2,
//指定行高,但它并不是一个绝对值,而是一个因子,具体的行高等于fontSize*height
fontFamily: "Courier",
//由于不同平台默认支持的字体集不同,所以在手动指定字体时一定要先在不同平台测试一下
background: new Paint()..color = Colors.lightBlue,
decoration: TextDecoration.underline,//下划线
decorationStyle: TextDecorationStyle.dashed),//下划线样式
// decorationColor: Colors.white,下划线颜色
),

style效果图
#####softWrap
是否自动换行,若为false,文字将不考虑容器大小,单行显示,超出屏幕部分将默认截断处理
#####overflow
当文字超出屏幕的时候,如何处理:

  • TextOverflow.clip(裁剪)
  • TextOverflow.fade(渐隐)
  • TextOverflow.ellipsis(省略号)

Card 摘要:

  • 实现了一个 Material Design card
  • 接受单个孩子,但该孩子可以是Row,Column或其他包含子级列表的widget
  • 显示圆角和阴影
  • Card内容不能滚动
  • Material Components 库的一个widget
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    class _home extends StatefulWidget {
    @override
    State<StatefulWidget> createState() {
    return _homeState();
    }
    }
    class _homeState extends State<_home> {
    @override
    Widget build(BuildContext context) {
    // TODO: implement build
    return new Scaffold(
    appBar: new AppBar(
    title: Text("title"),
    centerTitle: true,
    ),
    body: new Card(
    elevation: 4.0,//阴影
    color: Colors.grey,//背景色
    child: new Container(
    color: Colors.lightBlue,
    width: 200.0,
    height: 200.0,

    ),
    )
    );
    }
    }

#####固定的垂直布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}
class _homeState extends State<_home> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body: new ListView(
children: <Widget>[
new Image.network(
"https://cdn2.jianshu.io/assets/web/banner-s-3-7123fd94750759acf7eca05b871e9d17.png"),
new Image.network(
"https://cdn2.jianshu.io/assets/web/banner-s-4-b70da70d679593510ac93a172dfbaeaa.png"),
new Image.network(
"https://cdn2.jianshu.io/assets/web/banner-s-7-1a0222c91694a1f38e610be4bf9669be.png"),
],
),
);
}

固定布局
#####横向滑动的listview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}
class _homeState extends State<_home> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body: new Container(
height: 200.0,
child: new ListView(
scrollDirection: Axis.horizontal,//ListView滑动方向horizontal水平vertical垂直
children: <Widget>[
new Container(
width: 180.0,
color: Colors.greenAccent,
),
new Container(
width: 180.0,
color: Colors.lightBlue,
),
new Container(
width: 180.0,
color: Colors.red,
),
new Container(
width: 180.0,
color: Colors.black12,
),
new Container(
width: 180.0,
color: Colors.amberAccent,
)
],
),
)
);
}

横向滑动

#####简单demo无限滚动的listview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
//pubspec.yaml 中添加包 english_words: ^3.1.0
import 'package:flutter/material.dart';
import 'package:english_words/english_words.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new MaterialApp(
title: 'flutter demo',
home: _home(),
);
}
}

class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}
class _homeState extends State<_home> {
final data = <WordPair>[];//一个随机英文单词的数据源
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body: new ListView.builder(
padding: EdgeInsets.all(8.0),//内边距
itemBuilder: (conText, i) {
if(i.isOdd){//isOdd判断是否是奇数
return new Divider();//返回下划线
}
final index=i~/2;
if(data.length<=i){
data.addAll(generateWordPairs().take(10));//数据源添加10条随机英文单词
}
return _row(data[index]);
}));
}
Widget _row (WordPair pair){
return new ListTile(
title: new Text(pair.asPascalCase,style: TextStyle(fontSize: 15.0),),
);
}
}

无限滚动的listview

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}
class _homeState extends State<_home> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body: new GridView.count(
// padding: EdgeInsets.fromLTRB(8.0,0,8.0,0),//上下左右内边距
mainAxisSpacing: 4.0,//x轴间距
childAspectRatio: 1.5,//宽高比
crossAxisSpacing: 4.0,//y轴间距
crossAxisCount: 3,//行个数
children: <Widget>[
new Container(
color: Colors.deepPurpleAccent,
),
new Container(
color: Colors.yellow,
),
new Container(
color: Colors.lightBlue,
),
new Container(
color: Colors.grey,
),
new Container(
color: Colors.red,
),
new Container(
color: Colors.blueGrey,
),
],
)
);
}
}

效果图
######另一种写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}

class _homeState extends State<_home> {
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body: new GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
mainAxisSpacing: 4.0,
//x轴间距
childAspectRatio: 1.5,
//宽高比
crossAxisSpacing: 4.0,
//y轴间距
crossAxisCount: 3,
//行个数
),
children: <Widget>[
new Container(
color: Colors.deepPurpleAccent,
),
new Container(
color: Colors.yellow,
),
new Container(
color: Colors.lightBlue,
),
new Container(
color: Colors.grey,
),
new Container(
color: Colors.red,
),
new Container(
color: Colors.blueGrey,
),
],
)
);
}

######效果一样
效果图

垂直布局时:y轴为主轴,x轴为副轴!
水平不就是:x轴为主轴,y轴为副轴!

Material widgets库中提供了Material风格的单选开关Switch和复选框Checkbox,它们都是继承自StatelessWidget,所以它们本身不会保存当前选择状态,所以一般都是在父widget中管理选中状态。当用户点击Switch或Checkbox时,它们会触发onChanged回调,我们可以在此回调中处理选中状态改变逻辑。我们看一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
class _home extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return _homeState();
}
}
class _homeState extends State<_home> {
bool _switchSelected=true; //维护单选开关状态
bool _checkboxSelected=true;//维护复选框状态
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: Text("title"),
centerTitle: true,
),
body:new Column(
children: <Widget>[
Switch(//传入value和onChanged,传入value按钮初始化状态,onChanged状态改变回调
value: _switchSelected,
activeColor: Colors.red,
onChanged: (value){
setState(() {//setState动态更新
_switchSelected=value;
});
},
),
Checkbox(
value: _checkboxSelected,
activeColor: Colors.greenAccent,
onChanged: (value){
setState(() {
_checkboxSelected=value;
});
},
)
],
)
);
}
}

Checkbox没有设置setState因此点击没有动态更新

上面代码中,由于要维护Switch和Checkbox状态,所以SwitchAndCheckBoxTestRoute继承自StatefulWidget 。在其build方法中分别构建了一个Switch和Checkbox,初始状态都为选中状态,当用户点击时,会将状态置反,然后回调用setState()通知framework重新构建UI。
它们都有一个activeColor属性,用于设置激活态的颜色。至于大小,到目前为止,Checkbox的大小是固定的,无法自定义,而Switch只能定义宽度,高度也是固定的。值得一提的是Checkbox有一个属性tristate ,表示是否为三态,其默认值为false ,这时Checkbox有两种状态即“选中”和“不选中”,对应的value值为true和false ;如果其值为true时,value的值会增加一个状态null,读者可以自行了解。

#####图片
Flutter中,我们可以通过Image来加载并显示图片,Image的数据源可以是asset、文件、内存以及网络。
#####ImageProvider
ImageProvider 是一个抽象类,主要定义了图片数据获取的接口load(),从不同的数据源获取图片需要实现不同的ImageProvider ,如AssetImage是实现了从Asset中加载图片的ImageProvider,而NetworkImage实现了从网络加载图片的ImageProvider。
#####Image
Image widget有一个必选的image参数,它对应一个ImageProvider。下面我们分别演示一下如何从asset和网络加载图片。
#####从asset中加载图片

  • 在工程根目录下创建一个assets目录再在assets中创建images目录,并将图片拷贝到该目录。
    images目录

  • 在pubspec.yml中的flutter部分添加如下内容:

    1
    2
    assets:
    - assets/images/dubai.jpg
  • 加载该图片

    1
    Image.asset("assets/images/dubai.jpg",width: 300,),//width宽度 可以不设置

#####从网络加载图片

1
Image.network("https://www.baidu.com/img/bd_logo1.png"),

本地及网络加载图片
//参数

1
2
3
4
5
6
7
8
9
10
Image.asset(
"assets/images/dubai.jpg",//图片路径
width: 200.0,////图片的宽
height: 200.0,//图片高度
color: Colors.amber,//图片的混合色值
alignment: Alignment.center,//对齐方式
repeat: ImageRepeat.noRepeat,//重复方式
fit: BoxFit.fill,//fit缩放模式
colorBlendMode: BlendMode.difference,//colorBlendMode混合模式
),

混合色效果图

width、height:用于设置图片的宽、高,当不指定宽高时,图片会根据当前父容器的限制,尽可能的显示其原始大小,如果只设置width、height的其中一个,那么另一个属性默认会按比例缩放,但可以通过下面介绍的fit属性来指定适应规则。
fit:该属性用于在图片的显示空间和图片本身大小不同时指定图片的适应模式。适应模式是在BoxFit中定义,它是一个枚举类型,有如下值:
  • fill:会拉伸填充满显示空间,图片本身长宽比会发生变化,图片会变形。
  • cover:会按图片的长宽比放大后居中填满显示空间,图片不会变形,超出显示空间部分会被剪裁。
  • contain:这是图片的默认适应规则,图片会在保证图片本身长宽比不变的情况下缩放以适应当前显示空间,图片不会变形。
  • fitWidth:图片的宽度会缩放到显示空间的宽度,高度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
  • fitHeight:图片的高度会缩放到显示空间的高度,宽度会按比例缩放,然后居中显示,图片不会变形,超出显示空间部分会被剪裁。
  • none:图片没有适应策略,会在显示空间内显示图片,如果图片比显示空间大,则显示空间只会显示图片中间部分。

#####层叠布局

层叠布局和Web中的绝对定位、Android中的Frame布局是相似的,子widget可以根据到父容器四个角的位置来确定本身的位置。绝对定位允许子widget堆叠(按照代码中声明的顺序)。Flutter中使用Stack和Positioned来实现绝对定位,Stack允许子widget堆叠,而Positioned可以给子widget定位(根据Stack的四个角)。
#####Stack

1
2
3
4
5
6
7
Stack({
this.alignment = AlignmentDirectional.topStart,
this.textDirection,
this.fit = StackFit.loose,
this.overflow = Overflow.clip,
List<Widget> children = const <Widget>[],
})
  • alignment:此参数决定如何去对齐没有定位(没有使用Positioned)或部分定位的子widget。所谓部分定位,在这里特指没有在某一个轴上定位:left、right为横轴,top、bottom为纵轴,只要包含某个轴上的一个定位属性就算在该轴上有定位。
  • textDirection:和Row、Wrap的textDirection功能一样,都用于决定alignment对齐的参考系即:textDirection的值为TextDirection.ltr,则alignment的start代表左,end代表右;textDirection的值为TextDirection.rtl,则alignment的start代表右,end代表左。
  • fit:此参数用于决定没有定位的子widget如何去适应Stack的大小。StackFit.loose表示使用子widget的大小,StackFit.expand表示扩伸到Stack的大小。
  • overflow:此属性决定如何显示超出Stack显示空间的子widget,值为Overflow.clip时,超出部分会被剪裁(隐藏),值为Overflow.visible 时则不会。

#####Positioned

1
2
3
4
5
6
7
8
9
10
const Positioned({
Key key,
this.left,
this.top,
this.right,
this.bottom,
this.width,
this.height,
@required Widget child,
})

left、top 、right、 bottom分别代表离Stack左、上、右、底四边的距离。width和height用于指定定位元素的宽度和高度,注意,此处的width、height 和其它地方的意义稍微有点区别,此处用于配合left、top 、right、 bottom来定位widget,举个例子,在水平方向时,你只能指定left、right、width三个属性中的两个,如指定left和width后,right会自动算出(left+width),如果同时指定三个属性则会报错,垂直方向同理。
#####示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class _home extends StatefulWidget{
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _homeState();
}
}
class _homeState extends State<_home>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: new Text("title"),
centerTitle: true,
),
body: //通过ConstrainedBox来确保Stack占满屏幕
ConstrainedBox(
constraints: BoxConstraints.expand(),
child: Stack(
alignment:Alignment.center , //指定未定位或部分定位widget的对齐方式
children: <Widget>[
Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
left: 18.0,
child: Text("I am Jack"),
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
)
);
}
}


由于第一个子widget Text(“Hello world”)没有指定定位,并且alignment值为Alignment.center,所以,它会居中显示。第二个子widget Text(“I am Jack”)只指定了水平方向的定位(left),所以属于部分定位,即垂直方向上没有定位,那么它在垂直方向对齐方式则会按照alignment指定的对齐方式对齐,即垂直方向居中。对于第三个子widget Text(“Your friend”),和第二个Text原理一样,只不过是水平方向没有定位,则水平方向居中。
我们给上例中的Stack指定一个fit属性,然后将三个子widget的顺序调整一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class _home extends StatefulWidget{
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _homeState();
}
}
class _homeState extends State<_home>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: new Text("title"),
centerTitle: true,
),
body: //通过ConstrainedBox来确保Stack占满屏幕
Stack(
alignment:Alignment.center ,
fit: StackFit.expand, //未定位widget占满Stack整个空间
children: <Widget>[
Positioned(
left: 18.0,
child: Text("I am Jack"),
),
Container(child: Text("Hello world",style: TextStyle(color: Colors.white)),
color: Colors.red,
),
Positioned(
top: 18.0,
child: Text("Your friend"),
)
],
),
);
}
}

#####导航返回拦截WillPopScope

为了避免用户误触返回按钮而导致APP退出,在很多APP中都拦截了用户点击返回键的按钮,当用户在某一个时间段内点击两次时,才会认为用户是要退出(而非误触)。Flutter中可以通过WillPopScope来实现返回按钮拦截,我们看看WillPopScope的默认构造函数:

1
2
3
4
5
const WillPopScope({
...
@required WillPopCallback onWillPop,
@required Widget child
})

onWillPop是一个回调函数,当用户点击返回按钮时调用(包括导航返回按钮及Android物理返回按钮),该回调需要返回一个Future对象,如果返回的Future最终值为false时,则当前路由不出栈(不会返回),最终值为true时,当前路由出栈退出。我们需要提供这个回调来决定是否退出。

#####示例
为了防止用户误触返回键退出,我们拦截返回事件,当用户在1秒内点击两次返回按钮时,则退出,如果间隔超过1秒则不退出,并重新记时。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import 'package:flutter/material.dart';

class WillPopScopeTestRoute extends StatefulWidget {
@override
WillPopScopeTestRouteState createState() {
return new WillPopScopeTestRouteState();
}
}

class WillPopScopeTestRouteState extends State<WillPopScopeTestRoute> {
DateTime _lastPressedAt; //上次点击时间

@override
Widget build(BuildContext context) {
return new WillPopScope(
onWillPop: () async {
if (_lastPressedAt == null ||
DateTime.now().difference(_lastPressedAt) > Duration(seconds: 1)) {
//两次点击间隔超过1秒则重新计时
_lastPressedAt = DateTime.now();
return false;
}
return true;
},
child: Container(
alignment: Alignment.center,
child: Text("1秒内连续按两次返回键退出"),
)
);
}
}

#####弹性布局

弹性布局允许子widget按照一定比例来分配父容器空间,弹性布局的概念在其UI系统中也都存在,如H5中的弹性盒子布局,Android中的FlexboxLayout。Flutter中的弹性布局主要通过Flex和Expanded来配合实现。

#####Flex
Flex可以沿着水平或垂直方向排列子widget,如果你知道主轴方向,使用Row或Column会方便一些,因为Row和Column都继承自Flex,参数基本相同,所以能使用Flex的地方一定可以使用Row或Column。Flex本身功能是很强大的,它也可以和Expanded配合实现弹性布局,接下来我们只讨论Flex和弹性布局相关的属性(其它属性已经在介绍Row和Column时介绍过了)。

1
2
3
4
5
Flex({
...
@required this.direction, //弹性布局的方向, Row默认为水平方向,Column默认为垂直方向
List<Widget> children = const <Widget>[],
})

Flex继承自MultiChildRenderObjectWidget,对应的RenderObject为RenderFlex,RenderFlex中实现了其布局算法。

#####Expanded
可以按比例“扩伸”Row、Column和Flex子widget所占用的空间。

1
2
3
4
const Expanded({
int flex = 1,
@required Widget child,
})

flex为弹性系数,如果为0或null,则child是没有弹性的,即不会被扩伸占用的空间。如果大于0,所有的Expanded按照其flex的比例来分割主轴的全部空闲空间。下面我们看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
class _home extends StatefulWidget{
@override
State<StatefulWidget> createState() {
// TODO: implement createState
return _homeState();
}
}
class _homeState extends State<_home>{
@override
Widget build(BuildContext context) {
// TODO: implement build
return new Scaffold(
appBar: new AppBar(
title: new Text("title"),
centerTitle: true,
),
body: _body(),
);
}
}

Widget _body(){
return Column(
children: <Widget>[
//Flex的两个子widget按1:2来占据水平空间
Flex(
direction: Axis.horizontal,
children: <Widget>[
Expanded(
flex: 1,
child: Container(
height: 80.0,
color: Colors.red,
),
),
Expanded(
flex: 2,
child: Container(
height: 80.0,
color: Colors.green,
),
),
],
),
Padding(
padding: const EdgeInsets.only(top: 20.0),
child: SizedBox(
height: 100.0,
//Flex的三个子widget,在垂直方向按2:1:1来占用100像素的空间
child: Flex(
direction: Axis.vertical,
children: <Widget>[
Expanded(
flex: 2,
child: Container(
color: Colors.red,
),
),
Spacer(//Spacer的功能是占用指定比例的空间,实际上它只是Expanded的一个包装
flex: 1,
),
Expanded(
flex: 1,
child: Container(
color: Colors.green,
),
),
],
),
),
),
],
);
}