前言
在 Android、和 iOS 开发中,列表分别使用的是 Android 的 ListView 或 RecyclerView,iOS 的 UITableView 实现的。而在 Flutter 中实现这种需求使用的则是 ListView。
(一)ListView 的基本使用
在 Flutter 中,ListView 可以沿一个方向(垂直或水平)来排列其所有子 Widget,比如通讯录、优化卷、商品列表等。
ListView 提供了一个默认构造函数 ListView,我们可以通过设置它的 children 参数,很方便地将所有的子 Widget 包含到 ListView 中。
不过,这种创建方式要求提前将所有子 Widget 一次性创建好,而不是等到它们真正在屏幕上需要显示时才创建。所有性能会很差。因此,这种方式仅适用于列表中含有少量元素的场景。
代码如下所示:
body: ListView(
children: <Widget>[
ListTile(
leading: Icon(Icons.map),
title: Text('Map'),
subtitle: Text('Map'),
),
ListTile(
leading: Icon(Icons.mail),
title: Text('Mail'),
subtitle: Text('Mail'),
),
ListTile(
leading: Icon(Icons.message),
title: Text('Message'),
subtitle: Text('Message'),
),
...
],
)
其中,ListTile 是 Flutter 提供的用于快速构建列表项元素的一个组件元素,参数包含 leading(主导)、title(文本)、subtitle(副文本)等。
具体 ListTile 的使用细节,可以参考官方文档
效果图如下所示:

除了默认的垂直方向布局外,ListView 还可以通过设置 scrollDirection 参数设置水平布局。
代码如下所示:
body: ListView(
scrollDirection: Axis.horizontal,
itemExtent: 140, // 宽度 140
children: <Widget>[
Container(
color: Colors.pink,
),
Container(
color: Colors.black,
),
Container(
color: Colors.blue,
),
Container(
color: Colors.red,
),
Container(
color: Colors.green,
),
Container(
color: Colors.orange,
),
],
)
效果图如下所示:

(二)ListView.builder 的使用
考虑到创建子 Widget 产生的性能问题,更好的方式是抽象出创建子 Widget 的方法,交由 ListView 统一管理,在真正需要展示该子 Widget 时再去创建。
ListView 的另一个构造函数 ListView.builder,则适用于子 Widget 比较多的场景。这个构造函数有两个关键参数:
- itemBuilder,是列表项的创建方法。当列表滚动到相应位置时,ListView 会调用该方法创建对应的子 Widget。
- itemCount,表示列表项的数量,如果为空,则表示 ListView 为无限列表。
代码如下所示:
body: ListView.builder(
itemCount: 20,
itemExtent: 50.0,
itemBuilder: (BuildContext context, int index) => ListTile(
title: Text("这是第$index个条目"),
))
效果图如下所示:

(三)ListView 分割线
在 ListView 中,有两种方式支持分割线:
- 一种是,在 itemBuilder 中,根据 index 的值动态创建分割线,也就是将分割线视为列表项的一部分;
- 另一种是,使用 ListView 的另一个构造方法 ListView.separated,单独设置分割线的样式。
与 ListView.builder 抽离出了子 Widget 的构造方法类似,ListView.separated 抽离出了分割线的创建方法 separatorBuilder,以便根据 index 设置不同样式的分割线。
代码如下所示:
body: ListView.separated(
separatorBuilder: (BuildContext context, int index) =>
index % 2 == 0
? Divider(
height: 1,
color: Colors.orange,
)
: Divider(
height: 1,
color: Colors.blue,
),
itemCount: 20,
itemBuilder: (BuildContext context, int index) => ListTile(
title: Text("这是第$index个条目"),
))
效果图如下所示:

(四)CustomScrollView
在使用 ListView 时,对于某些特殊交互场景,比如多个效果联动、嵌套滚动、精细滑动、视图跟随手势操作等,还需要嵌套多个 ListView 来实现。这时,各自视图的滚动和布局模型就是相互独立、分离的,就很难保证整个页面统一一致的滑动效果。
在 Flutter 中有一个专门的控件 CustomScrollView,用来处理多个需要自定义滚动效果的 Widget。在 CustomScrollView 中,这些彼此独立的、可滚动的 Widget 被统称为 Sliver。
比如,ListView 的 Sliver 实现为 SliverList,AppBar 的 Sliver 实现为 SliverAppBar。这些 Sliver 不再维护各自的滚动状态,而是交由 CustomScrollView 统一管理,最终实现滑动效果的一致性。
下面以滚动视差为例,演示 CustomScrollView 的使用方法。
视差滚动是指让多层背景以不同的速度移动,在形成立体滚动效果的同时,还能保证良好的视觉体验。作为移动应用交互设计的热点趋势,越来越多的移动应用使用了这项技术。
以一个有着封面头图的列表为例,我们希望封面头图和列表这两层视图的滚动联动起来,当用户滚动列表时,头图会根据用户的滚动手势,进行缩小和展开。
经分析得出,要实现这样的需求,我们需要两个 Sliver:作为头图的 SliverAppBar,与作为列表的 SliverList。具体的实现思路是:
- 在创建 SliverAppBar 时,把 flexibleSpace 参数设置为悬浮头图背景。flexibleSpace 可以让背景图显示在 AppBar 下方,高度和 SliverAppBar 一样;
- 而在创建 SliverList 时,通过 SliverChildBuilderDelegate 参数实现列表项元素的创建;
- 最后,将它们一并交由 CustomScrollView 的 slivers 参数统一管理。
代码如下所示:
body: CustomScrollView(
slivers: <Widget>[
SliverAppBar(//SliverAppBar 作为头图控件
title: Text('CustomScrollView Demo'),// 标题
floating: true,// 设置悬浮样式
flexibleSpace: Image.network('https://upload.jianshu.io/users/upload_avatars/7534136/e21b56cd-6ac5-4ec9-ab00-4de058f63ae2.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/300/h/300/format/webp',fit:BoxFit.cover),// 设置悬浮头图背景
expandedHeight: 300,// 头图控件高度
),
SliverList(//SliverList 作为列表控件
delegate: SliverChildBuilderDelegate(
(context, index) => ListTile(title: Text('Item #$index')),// 列表项创建方法
childCount: 100,// 列表元素个数
),
),
])
效果图如下所示:

(五)ScrollController 与 ScrollNotfication
在某些情况下,我们希望获取视图的滚动信息,并进行相应的控制,比如,列表是否已经滑到底(顶)部?如果快速回到列表顶部?列表滚动是否已经开始,是否已经停止?
对于前两个问题,可以使用ScrollController 进行滚动信息的监听,以及相应的滚动控制;最后一个问题,需要接受 ScrollNotfication 通知进行滚动事件的获取。
(1)ScrollController
在 Flutter 中,因为 Widget 并不是渲染到屏幕的最终视觉元素(RenderObject 才是),所以我们无法像原生的 Android 或 iOS 系统那样,向持有的 Widget 对象获取或设置最终渲染相关的视觉信息,而必须通过对应的组件控制器才能实现。
ListView 的组件控制器则是 ScrollControler,我们可以通过它来获取视图的滚动信息,更新视图的滚动位置。
一般而言,获取视图的滚动信息往往是为了进行界面的状态控制,因此 ScrollController 的初始化、监听及销毁需要与 StatefulWidget 的状态保持同步。
代码如下所示:
class _MyHomePageState extends State<MyHomePage> {
ScrollController _controller; // listView 控制器
bool isToTop = false; // 标示目前是否需要启动 Top 按钮
@override
void initState() {
print('调用了 + initState');
_controller = ScrollController();
_controller.addListener(() { // 为控制器注册滚动监听方法
if (_controller.offset > 1000) {
// 如果 ListView 已经向下滚动了 1000,则开启 Top 按钮
setState(() {
isToTop = true;
});
} else if (_controller.offset < 300) {
// 如果 ListView 向下滚动距离不足 300,则禁用
setState(() {
isToTop = false;
});
}
});
super.initState();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
// 顶部 Top 按钮,根据 isToTop 变量判断是否需要注册滚动到顶部的方法
body: Column(
children: <Widget>[
Container(
child: RaisedButton(
onPressed: (isToTop
? () {
if (isToTop) {
_controller.animateTo(.0,
duration: Duration(milliseconds: 500),
curve: Curves.ease); // 做一个滚动到顶部的动画
}
}
: null),
child: Text('Top'),
),
),
Expanded(
child: ListView.builder(
controller: _controller, // 初始化传入控制器
itemCount: 100, // 列表总数
itemBuilder: (context, index) => ListTile(
title: Text('Index:$index'),
)))
],
));
}
@override
void dispose() {
super.dispose();
_controller.dispose();
print('调用了 + dispose');
}
}
效果图如下所示:

- 首先,我们在 State 的初始化方法里,创建了 ScrollController,并通过 _controller.addListener 注册了滚动监听方法回调,根据当前视图的滚动位置,判断当前是否需要展示“Top”按钮。
- 随后,在视图构建方法 build 中,我们将 ScrollController 对象与 ListView 进行了关联,并且在 RaisedButton 中注册了对应的回调方法,可以在点击按钮时通过 _controller.animateTo 方法返回列表顶部。
- 最后,在 State 的销毁方法中,我们对 ScrollController 进行了资源释放。
(2)ScrollNotfication
Flutter 中为了感知 ListView 的各类滚动事件,需要获取 ScrollNotification 通知。
ScrollNotification 通知的获取是通过 NotificationListener 来实现的。与 ScrollController 不同的是,NotificationListener 是一个 Widget,为了监听滚动类型的事件,需要将 NotificationListener 添加为 ListView 的父容器,从而捕获 ListView 中的通知。而这些通知,需要通过 onNotification 回调函数实现监听逻辑:
代码如下所示:
Widget build(BuildContext context) {
return MaterialApp(
title: 'ScrollController Demo',
home: Scaffold(
appBar: AppBar(title: Text('ScrollController Demo')),
body: NotificationListener<ScrollNotification>(
// 添加 NotificationListener 作为父容器
onNotification: (scrollNotification) {
// 注册通知回调
if (scrollNotification is ScrollStartNotification) {
// 滚动开始
print('Scroll Start');
} else if (scrollNotification is ScrollUpdateNotification) {
// 滚动位置更新
print('Scroll Update');
} else if (scrollNotification is ScrollEndNotification) {
// 滚动结束
print('Scroll End');
}
},
child: ListView.builder(
itemCount: 30, // 列表元素个数
itemBuilder: (context, index) =>
ListTile(title: Text("Index : $index")), // 列表项创建方法
),
)));
}
打印结果如下所示:

相比于 ScrollController 只能和具体的 ListView 关联后才可以监听到滚动信息;通过 NotificationListener 则可以监听其子 Widget 中的任意 ListView,不仅可以得到这些 ListView 的当前滚动位置信息,还可以获取当前的滚动事件信息 。
总结
在处理用于展示一组连续、可滚动的视图元素的场景,Flutter 提供了比原生系统更加强大的列表组件 ListView 和 CustomScrollView,不仅可以支持单一视图下可滚动 Widget 的交互模型及 UI 控制模型,对于某些特殊交互,需要嵌套多重可滚动 Widget 的场景,也提供了统一管理机制,最终实现体验一致的滑动效果。这些强大的组件,不仅可以开发出样式丰富的界面,更可以实现复杂的交互。
网友评论