Flutter入门指南(七)之滑动部件

前面的小节基本上讲完了常用的部件和容器部件,也可以完成很多的界面,但是又一个问题,假如我们要显示一段文字,比如将 一段又臭又长的文字 在界面上显示 1000 次,不难完成吧

1
2
// ..省略一些无关代码
body: Text('一段又臭又长的文字' * 1000, softWrap: true)

很简单,运行到手机…「诶诶诶,**,怎么只显示了一部分,剩下的怎么画不下去」

日常开发中,会遇到很多这种情况,许多界面不是一页就能够显示完的。那么这里提下可滑动的容器部件

SingleChildScrollView

这个部件非常简单,不贴源码了。最简单的使用方式只需要提供一个 child 即可。现在给前面写的 Text 包裹上一层 SingleChildScrollView 然后再运行,文字全部都展示出来了。

如果需要实现一个垂直的滚动列表,可以直接通过 SingleChildScrollView 包裹 Column 来实现,列表内容全部塞到 Column 即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class SingleChildScrollDemoPage extends StatelessWidget {
@override
Widget build(BuildContext context) {
/// letters 自由发挥吧...一定要大量,大量,大量
List<String> letters = [......];

return Scaffold(
appBar: AppBar(
title: Text('Single Child Demo'),
),
body: SingleChildScrollView(
child: Center(
child: Column(
children: List.generate(
letters.length,
(index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0),
child: Text(letters[index], style: TextStyle(fontSize: 18.0)),
)),
),
)),
);
}
}

运行结果会根据你的 letters 不同而不同,这边就不贴效果图了,反正你可以看到一串列表…

那么如果需要实现横向滚动列表呢,稍稍做下修改就行了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

body: SingleChildScrollView(
// 设置滚动方向
scrollDirection: Axis.horizontal,
child: Center(
// 修改为 `Row` 即可
child: Row(
children: List.generate(
letters.length,
// 如果你的 letters 数量比较少,推荐加个 `Container` 把宽度指定大点
(index) => Container(
child: Padding(
padding: const EdgeInsets.symmetric(vertical: 4.0, horizontal: 6.0),
child: Text(letters[index], style: TextStyle(fontSize: 18.0)),
),
width: 30.0)),
),
))

效果图也不贴了,都比较简单。

该部分代码查看 single_child_scroll_main.dart 文件*

ListView

平时开发 Android 的时候,如果有相同格式的列表要实现,一般会使用 ListView 或者 RecyclerView 来实现,Flutter 也提供了类似的部件 ListView

实现 ListView 的方法主要有

  • 通过 ListView 设置 children 属性实现

  • 通过 ListView.custom 实现

  • 通过 ListView.builder 实现

  • 通过 ListView.separated 实现带分割线列表

ListView children

第一种方法实现列表,和通过 SingleChildScrollView + Column / Row 的方法比较类似,不过可以直接通过指定 ListView 的 scrollDirection 就可以了。

1
2
3
4
5
6
7
8
9
10
11
body: ListView(
// 通过修改滑动方向设置水平或者垂直方向滚动
scrollDirection: Axis.vertical,
// 通过 iterable.map().toList 和 List.generate 方法效果是一样的
children: letters
.map((s) =>
Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Center(
child: Text(s))))
.toList()),

ListView.custom

1
2
3
4
5
6
7
8
9
10
11

body: ListView.custom(
// 指定 item 的高度,可以加快渲染的速度
itemExtent: 40.0,
// item 代理
childrenDelegate: SliverChildBuilderDelegate(
// IndexedWidgetBuilder,根据 index 设置 item 中需要变化的数据
(_, index) => Center(child: Text(letters[index], style: TextStyle(color: Colors.red))),
// 指定 item 的数量
childCount: letters.length,
)),

如果每个 item 的高度可以确定,那么推荐通过 itemExtent 来设置 item 的高度/宽度,能够加快 ListView 的渲染速度。如果不指定高度/宽度,ListView 需要根据每个 item 来计算 ListView 的高度,这个计算过程是需要消耗时间和资源的

ListView.builder

该方法同 custom 类似,custom 需要通过一个 Delegate 生成 item,该方法直接通过 builder 生成,同时也可以直接指定 item 的高度

1
2
3
4
5
body: ListView.builder(
itemBuilder: (_, index) => Center(child: Text(letters[index], style: TextStyle(color: Colors.green))),
itemExtent: 40.0,
itemCount: letters.length),
)),

相对比较简单,代码也比较少…就冲这点,我也愿意用这个方法

ListView.separated

如果需要在每个 item 之间添加分割线,那么通过以上的方式实现就比较困难了,所以 Flutter 提供了 separated 方法用来快速构建带有分割线的 ListView

加入我们的 item 之间的分割线需要如下样式:奇数位和偶数位之间用黑色分割线,偶数位和奇数位之间用红色分割线

1
2
3
4
5
6
7
8
9
// 需要分割线的时候才使用,不能指定 item 的高度
body: ListView.separated(
itemBuilder: (_, index) => Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0),
child: Center(child: Text(letters[index], style: TextStyle(color: Colors.blue))),
),
// 这里用来定义分割线
separatorBuilder: (_, index) => Divider(height: 1.0, color: index % 2 == 0 ? Colors.black : Colors.red),
itemCount: letters.length),

最终的效果如下:

以上代码查看 listview_main.dart 文件

总结下:如果 item 的高度能够准确获取,一定要指定 itemExtent 的值,这样会更加高效,至于要通过哪种方式来生成,完全看个人喜好吧。

ExpansionTile

既然讲到了 ListView,在日常开发中,折叠列表也是一个比较常用的,所以这边要提下 ExpansionTile 这个部件,因为相对比较简单,所以直接上代码了

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
class ExpansionTilesDemoPage extends StatelessWidget {

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ExpansionTile Demo'),
),
body: ExpansionTile(
// 最前面的 widget
leading: Icon(Icons.phone_android),
// 替换默认箭头
// trailing: Icon(Icons.phone_iphone),
title: Text('Parent'),
// 默认是否展开
initiallyExpanded: true,
// 展开时候的背景色
backgroundColor: Colors.yellow[100],
// 展开或者收缩的回调,true 表示展开
onExpansionChanged: (expanded) => print('ExpansionTile is ${expanded ? 'expanded' : 'collapsed'}'),
children: List.generate(
10,
(position) =>
Container(
padding: const EdgeInsets.only(left: 80.0),
child: Text('Children ${position + 1}'),
height: 50.0,
alignment: Alignment.centerLeft,
)),
),
);
}
}

这样就完成了一个折叠部件,看下最后的效果

那么实现折叠列表也就是通过 ListView 创建一个 ExpansionTile 列表即可,先准备下模拟的数据

1
2
3
4
5
6
7
8
9
final _keys = ['ParentA', 'ParentB', 'ParentC', 'ParentD', 'ParentE', 'ParentF'];
final Map<String, List<String>> _data = {
'ParentA': ['Child A0', 'Child A1', 'Child A2', 'Child A3', 'Child A4', 'Child A5'],
'ParentB': ['Child B0', 'Child B1', 'Child B2', 'Child B3', 'Child B4', 'Child B5'],
'ParentC': ['Child C0', 'Child C1', 'Child C2', 'Child C3', 'Child C4', 'Child C5'],
'ParentD': ['Child D0', 'Child D1', 'Child D2', 'Child D3', 'Child D4', 'Child D5'],
'ParentE': ['Child E0', 'Child E1', 'Child E2', 'Child E3', 'Child E4', 'Child E5'],
'ParentF': ['Child F0', 'Child F1', 'Child F2', 'Child F3', 'Child F4', 'Child F5']
};

在平时开发过程中,后台返回的数据应该是列表嵌套列表的形式比较多,我这边主要就是为了偷懒就随便弄了,接着修改下 body 的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
body: ListView(
children: _keys
.map((key) => ExpansionTile(
title: Text(key),
children: _data[key]
.map((value) => InkWell(
child: Container(
child: Text(value),
padding: const EdgeInsets.only(left: 80.0),
height: 50.0,
alignment: Alignment.centerLeft,
),
onTap: () {}))
.toList(),
))
.toList()),

最终的效果就是个折叠列表了

该部分代码查看 expansion_tile_main.dart 文件

当然了,只要数据到位,别说两层折叠,三层,四层甚至更多层都能够实现,源码中有实现四层的 demo,这边就不贴代码了,有需要的小伙伴可以查看源码

GridView

生成列表可以通过 ListView 来实现,那么同样,实现网格列表 Flutter 也提供了 GridView 来实现,实现 GridView 的方法也很多…我数了下,大概有 10 种..对你没看错,就是那么多,(诶诶诶,别走啊…虽然方法有点多,但是,大同小异)

GridView

GridView 需要一个 gridDelegate,gridDelegate 目前有两种

SliverGridDelegateWithFixedCrossAxisCount 看命名就知道,值固定数量的,这个数量是只单排的数量

SliverGridDelegateWithMaxCrossAxisExtent 这个是设置最大宽度/高度,在这个值范围内取最大值,比如一排能给你排下 6 个,但是远不到设置的最大值,它绝不给你排 6 个

那么接下来的使用就比较简单了

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
class GridViewDemoPage extends StatelessWidget {
// 自行设置
final List<String> letters = [ ..... ];

// 用于区分网格单元
final List<Color> colors = [Colors.red, Colors.green, Colors.blue, Colors.pink];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('GridView Demo'),
),
body: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5, // 单行的个数
mainAxisSpacing: 10.0, // 同 scrollDirection 挂钩,item 之间在主轴方向的间隔
crossAxisSpacing: 10.0, // item 之间在副轴方法的间隔
childAspectRatio: 1.0 // item 的宽高比
),
// 需要根据 index 设置不同背景色,所以使用 List.generate,如果不设置背景色,也可用 iterable.map().toList
children: List.generate(
letters.length,
(index) => Container(
alignment: Alignment.center,
child: Text(letters[index]),
color: colors[index % 4],
)),
),
);
}
}

关键地方已经添加了注释,跑下运行效果

接下来换一种 delegate 试试效果,当然这个最大值可以根据个人喜好来设置

1
2
3
4
5
6
7
8
9
10
11
12
body: GridView(
// 通过设置 `maxCrossAxisExtent` 来指定最大的宽度,在这个值范围内,会选取相对较大的值
gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent(
maxCrossAxisExtent: 60.0, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0),
children: List.generate(
letters.length,
(index) => Container(
alignment: Alignment.center,
child: Text(letters[index]),
color: colors[index % 4],
)),
)

最后效果:

为了方便写法呢,Flutter 对以上的两种方式进行了封装,省略了 delegate

GridView.count/GridView.extent

直接看下如何修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这种情况简化了 `GridView` 使用 `SliverGridDelegateWithFixedCrossAxisCount` 代理的方法
body: GridView.count(
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
childAspectRatio: 1.0,
crossAxisCount: 5,
childAspectRatio: 2.0,
children: List.generate(
letters.length,
(index) => Container(
alignment: Alignment.center,
color: colors[index % 4],
child: Text(letters[index]),
))),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 这种情况简化了 `GridView` 使用 `SliverGridDelegateWithMaxCrossAxisExtent` 代理的方法
body: GridView.extent(
crossAxisSpacing: 10.0,
mainAxisSpacing: 10.0,
childAspectRatio: 1.0,
maxCrossAxisExtent: 60.0,
children: List.generate(
letters.length,
(index) =>
Container(
alignment: Alignment.center,
color: colors[index % 4],
child: Text(letters[index]),
))),

运行的效果入和前面的相同

GridView.custom

这种生成方式,比 GridView 多了一个 childrenDelegate,childrenDelegate 主要分为两种,一种是通过 IndexedWidgetBuilder 来构建 item 的 SliverChildBuilderDelegate,还有一种是通过 List 来构建 item 的 SliverChildListDelegate,所以…这边直接有 4 中生成方式,当然,我们只需要了解 childrenDelegate 如何使用即可

1
2
3
4
5
6
7
8
9
10
11
12
13

body: GridView.custom(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5, mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0, childAspectRatio: 1.0),
// item 通过 delegate 来生成,内部实现还是 `IndexedWidgetBuilder`
childrenDelegate: SliverChildBuilderDelegate(
(_, index) => Container(
alignment: Alignment.center,
color: colors[index % 4],
child: Text(letters[index]),
),
childCount: letters.length)),
1
2
3
4
5
6
7
8
9
10
11
12
13
14
body: GridView.custom(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5, mainAxisSpacing: 10.0,
crossAxisSpacing: 10.0, childAspectRatio: 1.0),
// 内部通过返回控件列表实现
childrenDelegate: SliverChildListDelegate(
List.generate(
letters.length,
(index) => Container(
child: Text(letters[index]),
alignment: Alignment.center,
color: colors[index % 4],
)),
)),

运行效果也同上面。

GridView.builder

前面介绍的方法中,生成 item 的方式基本上是通过 List 进行转换的,在 custom 提到了 IndexWidgetBuilder 的生成方式,当然,在 ListView 的时候也用到了这种生成方式,当然 GridView 也有啊,要「雨露均沾」你说是吧

1
2
3
4
5
6
7
8
// 通过 `IndexedWidgetBuilder` 来构建 item,别的参数同上
body: GridView.builder(
// 这里又需要分两种 `gridDelegate`
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5, crossAxisSpacing: 10.0, mainAxisSpacing: 10.0, childAspectRatio: 1.0),
itemCount: letters.length,
itemBuilder: (_, index) =>
Container(color: colors[index % 4], child: Text(letters[index]), alignment: Alignment.center)),

到这 10 种方式就说完了。终于可以歇一口气了。

该部分代码查看 gridview_main.dart 文件

CustomScrollView

在平时的开发中,应该会遇到这么种情况,头部是一个 GridView 接下来拼接一些别的部件,然后再拼接一个列表,例如下图

因为 GridView 和 ListView 亮着都是可滑动的部件,直接拼接肯定会有「滑动冲突」,所以 Flutter 就提供了一个粘合剂,CustomScrollView,那么 Flutter 如何实现呢,因为会涉及到 Sliver 系列部件,所以这边先看下大概的代码,下节会补充 Sliver 系列部件的内容

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
class CustomScrollDemoPage extends StatelessWidget {
// 这边用的 A-Z 字母
final List<String> letters = [ ..... ];

final List<Color> colors = [Colors.red, Colors.green, Colors.blue, Colors.pink, Colors.yellow, Colors.deepPurple];

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('CustomScrollDemo'),
),
body: CustomScrollView(
// 这里需要传入 `Sliver` 部件,下节课填坑
slivers: <Widget>[
// SliverGrid 实现同 GridView 实现方式一样
// 同样 SliverGrid 有提供 `count`, `entent` 方法便于快速生成 SliverGrid
SliverGrid(
delegate: SliverChildBuilderDelegate(
(_, index) => InkWell(
child: Image.asset('images/ali.jpg'),
onTap: () {},
),
childCount: 8),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4, mainAxisSpacing: 10.0, crossAxisSpacing: 10.0)),
// 这里下节讲
SliverToBoxAdapter(
child: Container(
color: Colors.black12,
margin: const EdgeInsets.symmetric(vertical: 10.0),
child: Column(children: <Widget>[
Divider(height: 2.0, color: Colors.black54),
Stack(
alignment: Alignment.center,
children: <Widget>[
Image.asset('images/app_bar_hor.jpg', fit: BoxFit.cover),
Text('我是一些别的东西..例如广告', textScaleFactor: 1.5, style: TextStyle(color: Colors.red))
],
),
Divider(height: 2.0, color: Colors.black54),
], mainAxisAlignment: MainAxisAlignment.spaceBetween),
alignment: Alignment.center)),
// SliverFixedExtentList 实现同 List.custom 实现类似
SliverFixedExtentList(
delegate: SliverChildBuilderDelegate(
(_, index) => InkWell(
child: Container(
child: Text(letters[index] * 10,
style: TextStyle(color: colors[index % colors.length], letterSpacing: 2.0),
textScaleFactor: 1.5),
alignment: Alignment.center,
),
onTap: () {},
),
childCount: letters.length),
itemExtent: 60.0)
],
),
);
}
}

该部分代码查看 custom_scroll_main.dart 文件

滑动部件其实还有好几个,但是以上介绍的在平时开发过程中够用了,如果后期发现还需要别的部件,我会继续补上。在结束前,我们再说下如何通过 ScrollController 来控制 Scrollable 的滚动位置。例如我们需要实现,当滚动的距离大于一定距离的时候显示一个回到顶部的按钮,有了 ScrollController 就能够非常方便的实现

ScrollController

因为需要根据滑动的距离显示回到顶部按钮,那么就需要通过一个状态位来控制按钮显隐

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
class ScrollControllerDemoPage extends StatefulWidget {
@override
_ScrollControllerDemoPageState createState() => _ScrollControllerDemoPageState();
}

class _ScrollControllerDemoPageState extends State<ScrollControllerDemoPage> {
var _scrollController = ScrollController();
var _showBackTop = false;

@override
void initState() {
super.initState();

// 对 scrollController 进行监听
_scrollController.addListener(() {
// _scrollController.position.pixels 获取当前滚动部件滚动的距离
// window.physicalSize.height 获取屏幕高度
// 当滚动距离大于 800 后,显示回到顶部按钮
setState(() => _showBackTop = _scrollController.position.pixels >= 800);
});
}

@override
void dispose() {
// 记得销毁对象
_scrollController.dispose();
super.dispose();
}

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('ScrollController Demo'),
),
body: ListView(
controller: _scrollController,
children: List.generate(
20, (index) => Container(height: 50.0, alignment: Alignment.center, child: Text('Item ${index + 1}'))),
),
floatingActionButton: _showBackTop // 当需要显示的时候展示按钮,不需要的时候隐藏,设置 null
? FloatingActionButton(
onPressed: () {
// scrollController 通过 animateTo 方法滚动到某个具体高度
// duration 表示动画的时长,curve 表示动画的运行方式,flutter 在 Curves 提供了许多方式
_scrollController.animateTo(0.0, duration: Duration(milliseconds: 500), curve: Curves.decelerate);
},
child: Icon(Icons.vertical_align_top),
)
: null,
);
}
}

最后的效果图

坚持原创技术分享,您的支持将鼓励我继续创作!