Flutter 学习之旅06 高级组件


1 可滚动组件

对于列表和长布局的显示溢出问题,可以使用Flutter提供的可滚动组件来处理。

1.1 Scrollable组件

在Flutter中,一个可滚动的组件直接或间接包含一个Scrollable组件,它是可滚动组件的基础组件。

1
2
3
4
5
6
Scrollable({
this.axisDirection = AxisDirection.down,//滚动方向
this.controller,//用于接收一个ScrollController对象,控制滚动位置和监听滚动事件
this.physics,//用于接收一个ScrollPhysics对象,可以决定滚动组件响应用户操作的方式
@required this.viewportBuilder
})
1.2 Scrollbar组件

Scrollbar是一个Material风格的滚动指示器组件,如果要给可滚动组件添加滚动条,只需将Scrollbar组件作为可滚动组件的父组件使用即可。

如果一个可滚动组件支持Sliver模型,那么该滚动可以将子组件分成多个部分,只有当子组件出现在视口中时才会去构建它。

目前,可滚动组件中的大部分组件都支持基于Sliver的延迟构建模型,如ListView、GridView。

1.3 SingleChildScrollView组件

是一个只能包含单一子组件的可滚动组件,其作用类似于iOS的UIScrollView组件或Android的ScrollView组件。

只能应用于内容不会超过屏幕尺寸太多的情况,因为SingleChildScrollView组件目前还不支持基于Sliver的延迟加载,如果视图内容超出屏幕尺寸太多会导致性能问题。

所谓基于Sliver的延迟加载,是Flutter中提出的薄片(Sliver)概念。如果一个可滚动组件支持Sliver,那么该可滚动组件可以将子组件分成多个Sliver,只有当Sliver出现在视图窗口时才会去构建它,从而提高渲染的性能。

SingleChildScrollView组件的构造函数:

1
2
3
4
5
6
7
8
9
10
11
const SingleChildScrollView({
Key key,
this.scrollDirection = Axis.vertical,//滚动的方向,默认在垂直方向滚动
this.reverse = false,//控制从头还是从尾开始滚动,默认false,即从头开始滚动
this.padding,//插入子组件时的内边距
bool primary,//是否是与父级关联的主滚动视图
this.physics,//设置滚动效果
this.controller,//控制滚动位置,当primary为true时,controller必须为null
this.child,//列表项内容
this.dragStrartBehavior = DragStrartBehavior.down,//处理拖拽开始行为的方式
})

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 'package:flutter/material.dart';

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

class ScollWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--可滚动组件',
home: Scaffold(
appBar: AppBar(title: Text('可滚动组件--SingleChildScrollView')),
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: <Widget>[Text('Hello Flutter ' * 100)]
),
)
)
);
}
}

示例效果:

img

1.4 CustomScrollView组件

可以使用Sliver模型实现自定义滚动组件,可以包含多个子组件,而且可以将这些子组件包裹起来实现一致的滚动效果。

CustomScrollView作为容器组件时,子组件不能是ListView、GridView等可滚动组件,会造成滚动冲突。在实际使用过程中,Flutter提供了SliverList、SliverGrid等可滚动组件的Sliver版本。

ListView、GridView自带滚动模型,SliverList、SliverGrid不包含滚动模型,不会造成滚动冲突。

CustomScrollView组件的构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class CustomScrollView extends ScrollView {
const CustomScrollView({
Key key,
Axis scrollDirection = Axis.vertical,//滚动的方向,默认在垂直方向滚动
bool reverse = false,//控制从头还是从尾开始滚动,默认false,即从头开始滚动
ScrollController controller,//控制滚动位置,当primary为true时,controller必须为null
bool primary,//是否是与父级关联的主滚动视图
ScrollPhysics physics,//设置滚动效果
bool shrinkWrap = false,//子组件是否只满足自身大小
Key center,//子组件的key值
double anchor = 0.0,//开始滚动的偏移量,默认从坐标原点开始排列
double cacheExtent,//缓存不可见的列表项,即使这部分区域不可见,也会被加载处理
this.slivers = const <Widget>[],//列表子元素
int semanticChildCount,//子项数量
DragStartBehavior dragStartBehavior = DragStartBehavior.down,//开始处理拖拽行为的方式,默认为检测到拖拽手势时开始处理
})
}

CustomScrollView组件通常被用于实现复杂的滚动效果,并且可以用来实现复杂的动画效果。

示例代码:

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
import 'package:flutter/material.dart';

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

class ScollWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--可滚动组件',
home: Scaffold(
appBar: AppBar(title: Text('可滚动组件--CustomScrollView')),
body: CustomScrollView(
slivers: <Widget>[
// 头部
SliverAppBar(
pinned: true,
expandedHeight: 160.0,
flexibleSpace: FlexibleSpaceBar(
title: Text('CustomScrollView'),
background: Image.asset('images/test1.png'),
),
),
// 中间
SliverGrid(
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 200.0,
mainAxisSpacing: 10.0,
childAspectRatio: 3.0,
),
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Card(
child: Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.all(10),
child: Text('grid $index'),
),
);
},
childCount: 11,
),
),
// 底部
SliverFixedExtentList(
itemExtent: 60.0,
delegate: SliverChildListDelegate(
List.generate(20, (int index) {
return GestureDetector(
onTap: () => print('单击$index'),
child: Card(
child: Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.all(15),
child: Text('list $index'),
)
)
);
})
)
)
],
)
)
);
}
}

示例效果:

img

1.5 ScrollController组件

如果需要监听可滚动组件的滚动过程,可以使用ScrollController组件来进行监听。

ScrollController组件的构造函数:

1
2
3
4
5
ScrollController({
double initialScrollOffset = 0.0,//初始化滚动位置
this.keepScrollOffset = true,//是否保持滚动位置
this.debugLabel,
})

当keepScrollOffset的属性值为true时,可滚动组件的滚动位置会被存储到PageStorage中,当可滚动组件重新创建时可以使用PageStorage恢复存储的位置。

ScrollController组件还有如下属性和方法:

offset:可滚动组件当前的滚动位置;

jumpTo():用于跳转到指定的位置;

animateTo():跳转到指定位置,跳转时会执行设置的动画。

示例代码:

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(ScrollControllerPage());

class ScrollControllerPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return ScrollControllerPageState();
}
}

class ScrollControllerPageState extends State<ScrollControllerPage> {
ScrollController controller = new ScrollController();
bool showTopBtn = false;//是否显示按钮
@override
void initState() {
super.initState();
controller.addListener(() {
if(controller.offset < 500 && showTopBtn) {
setState(() {
showTopBtn = false;
});
} else if (controller.offset >= 500 && !showTopBtn) {
setState(() {
showTopBtn = true;
});
}
});
}
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--可滚动组件',
home: Scaffold(
appBar: AppBar(title: Text('可滚动组件--ScrollController')),
body: ListView.builder(
itemCount: 100,
itemExtent: 50.0,
controller: controller,
itemBuilder: (context, index) {
return ListTile(title: Text('列表Item $index'));
},
),
floatingActionButton: !showTopBtn ? null :
FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
controller.jumpTo(0);
}
)
)
);
}
}

示例效果:

img

在有多个组件嵌套的组件树中,组件树的子组件可以通过发送通知来与父组件进行通信,父组件则可以通过NotificationListener组件来监听自己关注的通知,这种跨组件的通信方式通常被称为事件冒泡。

接收滚动事件的参数类型为ScrollNotification,它提供了一个metrics属性,该属性包含了当前可视窗口和滚动位置等信息。

NotificationListener组件支持的属性如下:

pixels:当前滚动位置;

maxScrollExtent:最大可滚动长度;

extentBefore:距离滚出视图窗口顶部的长度;

extentInside:视图窗口内部长度,大小等于屏幕显示的列表长度;

extentAfter:列表中未滑入视图窗口部分的长度;

atEdge:是否滚动到了可滚动组件的边界。 示例代码:

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
import 'package:flutter/material.dart';

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

class ScrollNotificationPage extends StatefulWidget {
@override
State<StatefulWidget> createState() {
return ScrollNotificationPageState();
}
}

class ScrollNotificationPageState extends State<ScrollNotificationPage> {
String _progress = '0%';
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--可滚动组件',
home: Scaffold(
appBar: AppBar(title: Text('可滚动组件--NotificationListener')),
body: Scrollbar(
child: NotificationListener<ScrollNotification>(
onNotification: (ScrollNotification notification) {
double progress = notification.metrics.pixels / notification.metrics.maxScrollExtent;
setState(() {
_progress = '${(progress * 100).toInt()}%';
});
return null;
},
child: Stack(
alignment: Alignment.center,
children: <Widget>[
ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (context, index) {
return ListTile(title: Text('标题 $index'));
},
),
CircleAvatar(
radius: 30.0,
child: Text(_progress),
backgroundColor: Colors.black54,
)
]
)
)
)
)
);
}
}

示例效果:

img

NotificationListener组件和ScrollController组件都可以实现列表滚动的监听。NotificationListener组件可以监听可滚动组件的整个组件树,并且监听到的信息更多,ScrollController则只能监听关联的可滚动组件的相关信息。

2 列表组件

2.1 ListView

ListView,即列表组件,作用类似于Android的RecyclerView或ListView。ListView可以沿一个线性方向排布相同或相似的子组件元素,并支持基于Sliver的延迟。

ListView的默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class ListView extends BoxScrollView {
ListView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
EdgeInsetsGeometry padding,
bool shrinkWrap = false,//是否根据列表项的总长度来设置ListView的长度,默认为false
this.itemExtent,//列表项的大小。如果滚动方向是垂直方向,则表示子组件的高度;如果滚动方向为水平方向,则表示子组件的长度。
bool addAutomaticKeepAlives = true,//是否将列表项包裹在AutomaticKeepAlive组件中,默认值为true,表示列表项滑出视图窗口时不会被垃圾回收,会保存之前的状态。
bool addRepaintBoundaries = true,//是否将列表项包裹在RepaintBoundary组件中,默认值为true,可以避免列表项的重绘,提高渲染的性能。
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
DragStartBehavior dragStartBehavior = DragStartBehavior .down,
})
}

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'package:flutter/material.dart';

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

class ListViewWidget extends StatelessWidget {
final _items = List<Widget>.generate(10,
(index) => Container(
padding: EdgeInsets.all(16.0),
child: Text('Item $index')
)
);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--列表组件',
home: Scaffold(
appBar: AppBar(title: Text('可滚动组件--列表组件')),
body: ListView(children: _items)
)
);
}
}

示例效果:

img

默认的构造函数适合只含有少量子组件的情况,因为它不支持基于Sliver的延迟加载,当列表的元素较多时,容易出现卡顿现象。

2.2 ListView.builder

使用ListView.builder创建的列表是基于Sliver的延迟加载创建的,渲染性能比较高,适合用于列表元素比较多的情况。

ListView.builder特有的属性:

1)itemBuilder:用于构建列表项的可见子组件构建器,只有索引>= 0且< itemCount时才会被调用;

2)itemCount:列表项的数量,如果为null,则列表为无限列表。

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import 'package:flutter/material.dart';

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

class ListViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--列表组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--列表组件')),
body: ListView.builder(
itemCount: 100,
itemExtent: 50.0,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
}
)
)
);
}
}

示例效果:

img

2.3 ListView.separated

和ListView.builder相比,ListView.separated多了一个separatorBuilder属性,该属性可以在生成的列表项之间添加一条分割线。

示例代码:

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
import 'package:flutter/material.dart';

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

class ListViewWidget extends StatelessWidget {
Widget divider = Divider(color: Colors.grey);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--列表组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--列表组件')),
body: ListView.separated(
itemCount: 100,
itemBuilder: (BuildContext context, int index) {
return ListTile(title: Text('Item $index'));
},
separatorBuilder: (BuildContext context, int index) {
return divider;
},
)
)
);
}
}

示例效果:

img

2.4 ListView.custom

ListView.custom适用于自定义列表的场景。其中,childrenDelegate是它的必传参数,需要传入一个实现了SliverChildDelegate抽象类的组件,用来给ListView组件添加列表项。

SliverChildDelegate是一个抽象类,它的实现类有SliverChildListDelegate和SliverChildBuilderDelegate,并且SliverChildDelegate的build()可以对单个子组件进行自定义样式处理。

示例代码:

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

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

class ListViewWidget extends StatelessWidget {
final _items = List<Widget>.generate(100,
(i) => Container(
padding: EdgeInsets.all(16.0),
child: Text('Item $i')
)
);
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--列表组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--列表组件')),
body: ListView.custom(
childrenDelegate: SliverChildListDelegate(_items),
)
)
);
}
}

示例效果:

img

如果滚动视图中出现列表嵌套的场景,为了不造成滚动时的冲突,需要对子组件添加禁止滚动属性。

1
2
3
4
5
ListView.builder(
...
physics: NeverScrollableScrollPhysics(),//禁止滚动
...
)

3 网格组件

3.1 GridView基础

GridView是一个可以构建二维网格的列表组件,作用类似于原生Android中的GridView/RecyclerView或者iOS的UICollectionView。

GridView的默认构造函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
GridView({
Key key,
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required this.gridDelegate,//类型是SliverGridDelegate,控制GridView子组件的排列方式
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
bool addSemanticIndexes = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
int semanticChildCount,
})

SliverGridDelegate是一个抽象类,是一个控制子元素排列方式的接口,有两个实现类:

1)SliverGridDelegateWithFixedCrossAxisCount:用于列数固定的场景

1
2
3
4
5
6
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount,//列数
double mainAxisSpacing = 0.0,//主轴方向上子组件的间距
double crossAxisSpacing = 0.0,//横轴方向上子组件的间距
double childAspectRatio = 1.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
28
29
import 'package:flutter/material.dart';

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

class GridWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--网格组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--网格组件')),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
childAspectRatio: 1.5
),
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
)
)
);
}
}

示例效果:

img

SliverGridDelegateWithFixedCrossAxisCount还可以使用GridView.count进行代替:

1
2
3
4
5
6
7
8
9
10
...
body: GridView.count(
crossAxisCount: 3,
childAspectRatio: 1.5,
children: <Widget>[
Icon(Icons.ac_unit),
...
],
)
...

2)SliverGridDelegateWithMaxCrossAxisExtent:用于子元素有最大宽度限制的场景

1
2
3
4
5
6
SliverGridDelegateWithMaxCrossAxisExtent({
@required this.maxCrossAxisExtent,//子元素在横轴上的最大长度
this.mainAxisSpacing = 0.0,//主轴方向上子组件的间距
this.crossAxisSpacing = 0.0,//横轴方向上子组件的间距
this.childAspectRatio = 1.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
28
29
30
import 'package:flutter/material.dart';

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

class GridWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--网格组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--网格组件')),
body: GridView(
padding: EdgeInsets.zero,
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0
),
children: <Widget>[
Icon(Icons.ac_unit),
Icon(Icons.airport_shuttle),
Icon(Icons.all_inclusive),
Icon(Icons.beach_access),
Icon(Icons.cake),
Icon(Icons.free_breakfast),
],
)
)
);
}
}

示例效果:

img

SliverGridDelegateWithFixedCrossAxisCount还可以使用GridView.extent()代替:

1
2
3
4
5
6
7
8
9
10
11
...
body: GridView.extent(
padding: EdgeInsets.zero,
maxCrossAxisExtent: 120.0,
childAspectRatio: 2.0,
children: <Widget>[
Icon(Icons.ac_unit),
...
],
)
...
3.2 GridView构造函数

GridView的构造函数一共有5个:

1)GridView():默认构造函数,适用于元素个数有限的场景,会一次性全部渲染children属性中的子元素组件;

2)GridView.builder():适用于构建大量或无限长的列表,它只会构建那些可见的组件,对于不可见的会动态销毁,减少内存销毁,渲染更高效;必须要传入gridDelegate和itemBuilder属性;

3)GridView.count():SliverGridDelegateWithFixedCrossAxisCount实现类的简写,用于创建横轴数量固定的网格视图;

4)GridView.extent():SliverGridDelegateWithFixedCrossAxisCount实现类的简写,用于创建横轴子元素宽度固定的网格视图;

5)GridView.custom():自定义的网格视图,需要同时传入gridDelegate和childrenDelegate。 示例代码:

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
import 'package:flutter/material.dart';

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

class ItemViewModel {
final String icon;
final String title;
const ItemViewModel({this.icon, this.title});
}

class GridItem extends StatelessWidget {
final ItemViewModel data;
GridItem({Key key, this.data}): super(key: key);
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(bottom: 5),
child: Column(
children: <Widget>[
Image.asset(this.data.icon, width: 55, fit: BoxFit.fitWidth),
Text(this.data.title)
],
),
);
}
}

const List<ItemViewModel> list = [
ItemViewModel(title: '微信', icon: 'images/wx.png'),
ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
ItemViewModel(title: '微信', icon: 'images/wx.png'),
ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
ItemViewModel(title: '微信', icon: 'images/wx.png'),
ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
ItemViewModel(title: '微信', icon: 'images/wx.png'),
ItemViewModel(title: 'QQ', icon: 'images/qq.png'),
];

class GridViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--网格组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--网格组件')),
body: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4),
itemCount: list.length,
padding: EdgeInsets.symmetric(vertical: 10),
itemBuilder: (context, index) {
return GridItem(data: list[index]);
}
)
)
);
}
}

示例效果:

img

4 滑动切换组件

PageView是一个滑动视图列表组件,它继承自CustomScrollView,作用类似于Android的ViewPager,可以用它实现视图的左右滑动切换功能。

PageView的构造函数:

1)PageView():默认构造函数,创建一个可滚动列表,适合子组件比较少的场景;

1
2
3
4
5
6
7
8
9
10
11
12
PageView({
Key key,
this.scrollDirection = Axis.horizontal,
this.reverse = false,
PageController controller,
this.physics,
this.pageSnapping = true,
this.onPageChanged,//页面滑动切换时调用
this.semanticChildCount,//列表项的数量
List<Widget> children = const <Widget>[],//PageView的列表项
this.dragStartBehavior = DragStartBehavior.down,//处理拖拽开始行为的方式,默认为检测到拖拽手势时开始执行滚动拖拽行为
})

2)PageView.builder():创建一个滚动列表,适合子组件比较多的场景,需要指定子组件的数量;

3)PageView.custom():创建一个可滚动的列表,需要自定义子项。

示例代码:

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
import 'package:flutter/material.dart';

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

const List<String> items = [
'images/test1.png',
'images/test2.png',
];

class PageViewWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--滑块切换组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--滑块切换组件')),
body: PageView.builder(
onPageChanged: (index) {
print('current page $index');
},
itemCount: items.length,
itemBuilder: (context, index) {
return Image.asset(items[index]);
}
)
)
);
}
}

示例效果:

img

5 自定义组件

5.1 组合组件

按照从上到下、从左到右的方式去拆解布局结构即可。

5.2 自绘组件

在Flutter中创建自绘组件需要用到CustomPaint和CustomPainter两个类:CustomPaint在绘制阶段提供一个Canvas,即画布;CustomPainter在绘制阶段提供画笔,可配置画笔的颜色、样式和粗细等属性。

示例代码:

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
import 'dart:math';
import 'package:flutter/material.dart';

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

class PiePageWidget extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: '高级组件--自绘组件',
home: Scaffold(
appBar: AppBar(title: Text('高级组件--自绘组件')),
body: Center(
child: CustomPaint(
size: Size(300, 300),
painter: PiePainter()
)
)
)
);
}
}

class PiePainter extends CustomPainter {
Paint getPaint(Color color) {
Paint paint = Paint();
paint.color = color;
return paint;
}
@override
void paint(Canvas canvas, Size size) {
double wheelSize = min(size.width, size.height) / 2;
double nbElem = 6;
double radius = (2 * pi) / nbElem;
Rect boundingRect = Rect.fromCircle(center: Offset(wheelSize, wheelSize), radius: wheelSize);
canvas.drawArc(boundingRect, 0, radius, true, getPaint(Colors.red));
canvas.drawArc(boundingRect, radius, radius, true, getPaint(Colors.black38));
canvas.drawArc(boundingRect, radius * 2, radius, true, getPaint(Colors.green));
canvas.drawArc(boundingRect, radius * 3, radius, true, getPaint(Colors.amber));
canvas.drawArc(boundingRect, radius * 4, radius, true, getPaint(Colors.blue));
canvas.drawArc(boundingRect, radius * 5, radius, true, getPaint(Colors.purple));
}
@override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;//是否需要执行重绘
}
}

示例效果:

img

创建Flutter自绘组件时,可以做以下两点性能优化:

1)尽可能利用好shouldRepaint()的返回值

如果绘制的内容不需要依赖外部状态,返回false即可;如果绘制过程需要依赖外部状态,可以在shouldRepaint()中判断依赖的状态是否改变,如果已改变,则返回true并执行重绘操作,反之则返回false不执行重绘;

2)绘制应尽可能多地进行分层

因为复杂的自绘组件都是由很多功能构成的,如果都写在一个方法中,不利于阅读,而且全部重绘带来的性能开销也很大。分层渲染可以降低视图渲染带来的性能开销。

无论是创建组合组件还是创建自绘组件,首先需要考虑如何将复杂的布局简化,把大问题拆分成若干小问题。