概述
- ListView
- GridView
- sliver
- 滚动的监听
一、ListView
移动端数据量比较大时,我们都是通过列表来进行展示的,比如商品数据、聊天列表、通信录、朋友圈等。
在Android
中,我们可以使用ListView
或RecyclerView
来实现,在iOS
中,我们可以通过UITableView
来实现。
在Flutter
中,我们也有对应的列表Widget,就是ListView
。
- 1.1、ListView 基本创建ListView可以沿一个方向(垂直或水平方向,默认是垂直方向)来排列其所有子Widget。一种最简单的使用方式是直接将所有需要排列的子Widget放在ListView的children属性中即可。我们来看一下直接使用ListView的代码演练:
- 1>、为了让文字之间有一些间距,我使用了Padding Widget
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: <Widget>[ Padding( padding: const EdgeInsets.all(20.0), child: Text("人的一切痛苦,本质上都是对自己无能的愤怒。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), Padding( padding: const EdgeInsets.all(20.0), child: Text("人活在世界上,不可以有偏差;而且多少要费点劲儿,才能把自己保持到理性的轨道上。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), Padding( padding: const EdgeInsets.all(20.0), child: Text("我活在世上,无非想要明白些道理,遇见些有趣的事。", style: TextStyle(fontSize: 22.0, backgroundColor: Colors.brown),), ), ], ); } }
提示:我们可以通过
List.generate
创建子 Widget
- List.generate(100, (index):第一个参数是加载多少个Widget, 第二个是第几个
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: List.generate(100, (index) { return Text("Hello World $index"); }) ); } }
- 2>、ListTile的使用
在开发中,我们经常见到一种列表,有一个图标或图片(Icon),有一个标题(Title),有一个子标题(Subtitle),还有尾部一个图标(Icon)。
这个时候,我们可以使用ListTile来实现:
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( children: <Widget>[ ListTile( leading: Icon(Icons.people, size: 20,), title: Text("联系人"), subtitle: Text("联系人信息"), trailing: Icon(Icons.arrow_right), ), ListTile( leading: Icon(Icons.people, size: 20,), title: Text("邮箱"), subtitle: Text("邮箱地址信息"), trailing: Icon(Icons.arrow_right), ), ], ); } }
- 3>、垂直方向滚动,默认是垂直方向
我们可以通过设置scrollDirection
参数来控制视图的滚动方向
。
我们通过下面的代码实现一个水平滚动的内容:
这里需要注意,我们需要给Container设置width,否则它是没有宽度的,就不能正常显示。或者我们也可以给ListView设置一个itemExtent
,该属性会设置滚动方向上每个item所占据的宽度
。
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView( scrollDirection: Axis.horizontal, itemExtent: 200, children: <Widget>[ Container(color: Colors.red, width: 200), Container(color: Colors.green, width: 200), Container(color: Colors.blue, width: 200), Container(color: Colors.purple, width: 200), Container(color: Colors.orange, width: 200), ], ); } }
- 1.2、ListView.build 创建
通过构造函数中的children传入所有的子Widget有一个问题:默认会创建出所有的子Widget。
但是对于用户来说,一次性构建出所有的Widget并不会有什么差异,但是对于我们的程序来说会产生性能问题,而且会增加首屏的渲染时间。
我们可以ListView.build来构建子Widget,提供性能。
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.builder( // 创建多少个 row itemCount: 50, // 滚动方向的 row 宽度 itemExtent: 100, // 生成 Widget itemBuilder: (BuildContext ctx, int index) { return ListTile(title: Text("标题$index"), subtitle: Text("详情内容$index")); } ); } }
- 1.3、ListView.separated 创建(带分割线)
ListView.separated
可以生成列表项之间的分割器
,它除了比ListView.builder多了一个separatorBuilder参数,该参数是一个分割器生成器。
下面我们看一个例子:奇数行添加一条蓝色下划线,偶数行添加一条红色下划线:
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return ListView.separated( itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), trailing: Icon(Icons.arrow_right), title: Text("联系人${index+1}"), subtitle: Text("联系人电话${index+1}"), ); }, itemCount: 10, separatorBuilder: (BuildContext context, int index) { return Divider( // 每个Widget 之间的距离 height: 30, // 距离左边的距离 indent: 16, // 距离右边的距离 endIndent: 16, // 每条分割线的高度 thickness: 10, color: index % 2 == 0 ? Colors.red : Colors.green, ); }, ); } }
二、GridView 组件
GridView用于展示多列的展示,在开发中也非常常见,比如直播App中的主播列表、电商中的商品列表等等。
在Flutter中我们可以使用GridView来实现,使用方式和ListView也比较相似。
- 2.1、GridView构造函数使用GridView的方式就是使用构造函数来创建,和ListView对比有一个特殊的参数:
gridDelegate
gridDelegate用于控制交叉轴的item数量或者宽度,需要传入的类型是SliverGridDelegate,但是它是一个抽象类,所以我们需要传入它的子类:
SliverGridDelegateWithFixedCrossAxisCount
SliverGridDelegateWithFixedCrossAxisCount({ @requireddouble crossAxisCount, // 交叉轴的item个数 double mainAxisSpacing = 0.0, // 主轴的间距 double crossAxisSpacing = 0.0, // 交叉轴的间距 double childAspectRatio = 1.0, // 子Widget的宽高比 })
- 如下代码
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return GridView( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, crossAxisSpacing: 20, mainAxisSpacing: 20, // 宽 / 高 childAspectRatio: 2 ), children: List.generate(100, (index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); } ), ); } }
SliverGridDelegateWithMaxCrossAxisExtent
SliverGridDelegateWithMaxCrossAxisExtent({ double maxCrossAxisExtent, // 交叉轴的item宽度 double mainAxisSpacing = 0.0, // 主轴的间距 double crossAxisSpacing = 0.0, // 交叉轴的间距 double childAspectRatio = 1.0, // 子Widget的宽高比 })
- 如下代码
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return GridView( gridDelegate: SliverGridDelegateWithMaxCrossAxisExtent ( maxCrossAxisExtent: 100, mainAxisSpacing: 20, crossAxisSpacing: 20, childAspectRatio: 2 ), children: List.generate(100, (index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); } ), ); } }
提示:
前面两种方式也可以不设置delegate,可以分别使用:GridView.count构造函数和GridView.extent构造函数实现相同的效果
- 2.2. GridView.build
和ListView一样,使用构造函数会一次性创建所有的子Widget,会带来性能问题,所以我们可以使用GridView.build来交给GridView自己管理需要创建的子Widget。
我们直接使用之前的数据来进行代码演练:
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.all(5.0), child: GridView.builder( shrinkWrap: true, physics: ClampingScrollPhysics(), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount ( crossAxisCount: 2, mainAxisSpacing: 10, crossAxisSpacing: 10, childAspectRatio: 1.2 ), itemCount: 10, itemBuilder: (BuildContext context, int index) { return Container( child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Image.network('http://image.xcar.com.cn/attachments/a/day_200323/2020032314_59939b0716c40f9be872JrmcP75B4KfO.jpg-app'), SizedBox(height: 5), Text('王三', style: TextStyle(fontSize: 6),), ], ), ); } ), ); } }
三、Sliver
- 3.1、Sliver 的简单介绍
我们考虑一个这样的布局:一个滑动的视图中包括一个标题视图(HeaderView),一个列表视图(ListView),一个网格视图(GridView)。
我们怎么可以让它们做到统一的滑动效果呢?使用前面的滚动是很难做到的。
Flutter中有一个可以完成这样滚动效果的Widget:CustomScrollView,可以统一管理多个滚动视图。
在CustomScrollView中,每一个独立的,可滚动的Widget被称之为Sliver。
补充:Sliver可以翻译成裂片、薄片,你可以将每一个独立的滚动视图当做一个小裂片。 - 3.2、Slivers 的基本使用因为我们需要把很多的Sliver放在一个CustomScrollView中,所以CustomScrollView有一个slivers属性,里面让我们放对应的一些Sliver:不可以放弃他的
- SliverList:类似于我们之前使用过的ListView;
- SliverFixedExtentList:类似于SliverList只是可以设置滚动的高度;
- SliverGrid:类似于我们之前使用过的GridView;
- SliverPadding:设置Sliver的内边距,因为可能要单独给Sliver设置内边距;
- SliverAppBar:添加一个AppBar,通常用来作为CustomScrollView的HeaderView;
- SliverSafeArea:设置内容显示在安全区域(比如不让齐刘海挡住我们的内容),也就是可以
滚动过安全区域
class MyHomeBody1 extends StatelessWidget { @override Widget build(BuildContext context) { return CustomScrollView( slivers: <Widget>[ SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, childAspectRatio: 2, mainAxisSpacing: 16 ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); }, childCount: 10 ) ), ], ); } }
- 3.3、Slivers的组合使用:SliverAppBar、SliverGrid、SliverList 的设置
class MyHomeBody extends StatelessWidget { @override Widget build(BuildContext context) { return CustomScrollView( slivers: <Widget>[ SliverAppBar( // true: bar不动 // false: bar动 pinned: true, // bar 的高度 expandedHeight: 200, flexibleSpace: FlexibleSpaceBar( title: Text("Hello World!"), background: Image.asset("assets/images/iron.png", fit: BoxFit.cover,), ), ), SliverSafeArea( sliver: SliverPadding( padding: EdgeInsets.all(16), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, crossAxisSpacing: 16, childAspectRatio: 2, mainAxisSpacing: 16 ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return Container( color: Color.fromARGB(255, Random().nextInt(256), Random().nextInt(256), Random().nextInt(256)), ); }, childCount: 6 ) ), ), ), SliverList( delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), title: Text("联系人"), ); }, childCount: 20 ), ) ], ); } }
四、滚动的监听
对于滚动的视图,我们经常需要监听它的一些滚动事件,在监听到的时候去做对应的一些事情。
比如视图滚动到底部时,我们可能希望做上拉加载更多;
比如滚动到一定位置时显示一个回到顶部的按钮,点击回到顶部的按钮,回到顶部;
比如监听滚动什么时候开始,什么时候结束;
在 Flutter 中监听滚动相关的内容由两部分组成:ScrollController
和ScrollNotification
。
- 4.1、ScrollController 监听,可以预先设置offset,也可以监听滚动的位置,
缺点
是:无法检测股东开始和结束在Flutter中,Widget并不是最终渲染到屏幕上的元素(真正渲染的是RenderObject),因此通常这种监听事件以及相关的信息并不能直接从Widget中获取,而是必须通过对应的Widget的Controller来实现。ListView、GridView的组件控制器是ScrollController,我们可以通过它来获取视图的滚动信息,并且可以调用里面的方法来更新视图的滚动位置。另外,通常情况下,我们会根据滚动的位置来改变一些Widget的状态信息,所以ScrollController通常会和StatefulWidget一起来使用,并且会在其中控制它的初始化、监听、销毁等事件。我们来做一个案例,当滚动到500位置的时候,显示一个回到顶部的按钮:
jumpTo(double offset)、animateTo(double offset,...):
这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。- ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件。
- 代码如下
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { // 设置变量 _controller 并设置偏移量 ScrollController _controller = ScrollController(initialScrollOffset: 200); /* 默认设置为 false */ bool _isFloatingActionButton = false; @override void initState() { // TODO: implement initState super.initState(); _controller.addListener(() { print("监听到滚动"); setState(() { _isFloatingActionButton = _controller.offset > 500 ? true : false; }); }); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表滚动测试"), ), body: ListView.builder( controller: _controller, itemCount: 20, itemBuilder: (BuildContext context, int index) { return ListTile( leading: Icon(Icons.people), title: Text("测试 $index"), ); } ), floatingActionButton: _isFloatingActionButton ? FloatingActionButton( child: Icon(Icons.arrow_upward), onPressed: () { // 返回到顶部 _controller.animateTo(0, duration: Duration(milliseconds: 200), curve: Curves.easeIn); }, ) : null, ); } }
- 4.2、ScrollNotification如果我们希望监听什么时候开始滚动,什么时候结束滚动,这个时候我们可以通过NotificationListener。
- NotificationListener是一个Widget,模板参数T是想监听的通知类型,如果省略,则所有类型通知都会被监听,如果指定特定类型,则只有该类型的通知会被监听。
- NotificationListener需要一个onNotification回调函数,用于实现监听处理逻辑。
- 该回调可以返回一个布尔值,代表是
false
阻止该事件继续向上冒泡,如果为true
时,则冒泡终止,事件停止向上传播,如果不返回或者返回值为false 时,则冒泡继续。
案例: 列表滚动, 并且在中间显示滚动进度
class HomePage extends StatefulWidget { @override _HomePageState createState() => _HomePageState(); } class _HomePageState extends State<HomePage> { int _progress = 0; @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text("列表滚动测试"), ), body: NotificationListener( onNotification: (ScrollNotification notification ) { if (notification is ScrollStartNotification) { print("----开始滚动----"); } else if (notification is ScrollUpdateNotification) { // 当前滚动的位置和总长度 final currentPixel = notification.metrics.pixels; final totalPixel = notification.metrics.maxScrollExtent; double progress = currentPixel / totalPixel; setState(() { _progress = (progress * 100).toInt(); }); print("正在滚动:${notification.metrics.pixels} - ${notification.metrics.maxScrollExtent}"); } else if (notification is ScrollEndNotification) { print("----结束滚动----"); } return true; }, child: Stack( alignment: Alignment(0.9, 0.9), children: <Widget>[ ListView.builder( itemCount: 100, itemExtent: 60, itemBuilder: (BuildContext context, int index) { return ListTile(title: Text("item$index")); } ), CircleAvatar( radius: 30, child: Text("$_progress%"), backgroundColor: Colors.black54, ) ], ), ), ); } }