一文让你玩转Flutter中的列表
在flutter 中ListView 与GridView 都是继承自BoxScrollView,而 BoxScrollView 继承自ScrollView,
基础
1、ScrollView
const ScrollView({
Key key,
this.scrollDirection = Axis.vertical,
this.reverse = false,
this.controller,
bool primary,
ScrollPhysics physics,
this.shrinkWrap = false,
this.center,
this.anchor = 0.0,
this.cacheExtent,
this.semanticChildCount,
this.dragStartBehavior = DragStartBehavior.start,
this.keyboardDismissBehavior = ScrollViewKeyboardDismissBehavior.manual,
this.restorationId,
this.clipBehavior = Clip.hardEdge,
})
-
axisDirection滚动方向。
-
reverse参数表示反转滚动方向,并不是垂直转为水平,而是垂直方向滚动时,默认向下滚动,reverse设置false,滚动方向改为向上,同理水平滚动改为水平向左
-
controller为滚动控制器,可以监听滚到的位置,设置滚动的位置等
-
primary设置为true时,不能设置controller,因为primarytrue时,controller使用PrimaryScrollController,这种机制带来的好处是父组件可以控制子树中可滚动组件的滚动行为,例如,Scaffold正是使用这种机制在iOS中实现了点击导航栏回到顶部的功能
-
physics表示可滚动组件的物理滚动特性
- ClampingScrollPhysics:Android下微光效果。
- BouncingScrollPhysics:iOS下弹性效果。
-
dragStartBehavior:处理拖动开始行为的方式与时机
2、Scrollbar
Scrollbar是一个Material风格的滚动指示器(滚动条),如果要给可滚动组件添加滚动条,只需将Scrollbar作为可滚动组件的任意一个父级组件即可
Scrollbar(
child: ListView.builder(
reverse: false,
itemBuilder: (BuildContext context, int index) {
return Card(
child: Container(
height: 45,
alignment: Alignment.center,
child: Text('$index'),
),
);
},
itemCount: 30,
itemExtent: 50,
),
)
3、ScrollController
ScrollController构造函数如下:
ScrollController({
double initialScrollOffset = 0.0, //初始滚动位置
this.keepScrollOffset = true,//是否保存滚动位置
...
})
常用的属性和方法:
- offset:可滚动组件当前的滚动位置。
- jumpTo(double offset)、animateTo(double offset,...):这两个方法用于跳转到指定的位置,它们不同之处在于,后者在跳转时会执行一个动画,而前者不会。
滚动监听 ScrollController间接继承自Listenable,我们可以根据ScrollController来监听滚动事件
controller.addListener(()=>print(controller.offset))
ScrollPosition
ScrollPosition是用来保存可滚动组件的滚动位置的。一个ScrollController对象可以同时被多个可滚动组件使用,ScrollController会为每一个可滚动组件创建一个ScrollPosition对象,这些ScrollPosition保存在ScrollController的positions属性中(List)。ScrollPosition是真正保存滑动位置信息的对象,offset只是一个便捷属性
double get offset => position.pixels;
一个ScrollController虽然可以对应多个可滚动组件,但是有一些操作,如读取滚动位置offset,则需要一对一!但是我们仍然可以在一对多的情况下,通过其它方法读取滚动位置,举个例子,假设一个ScrollController同时被两个可滚动组件使用,那么我们可以通过如下方式分别读取他们的滚动位置
controller.positions.elementAt(0).pixels
controller.positions.elementAt(1).pixels
我们可以通过controller.positions.length来确定controller被几个可滚动组件使用。
class _ScrollControllerPageState extends State<ScrollControllerPage> {
ScrollController _controller = new ScrollController();
bool showToTopBtn = false; //是否显示“返回到顶部”按钮
@override
void initState() {
super.initState();
//监听滚动事件,打印滚动位置
_controller.addListener(() {
print(_controller.offset); //打印滚动位置
if (_controller.offset < 1000 && showToTopBtn) {
setState(() {
showToTopBtn = false;
});
} else if (_controller.offset >= 1000 && showToTopBtn == false) {
setState(() {
showToTopBtn = true;
});
}
});
}
@override
void dispose() {
//为了避免内存泄露,需要调用_controller.dispose
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Scrollbar(
child: ListView.builder(
itemCount: 100,
itemExtent: 50.0, //列表项高度固定时,显式指定高度是一个好习惯(性能消耗小)
controller: _controller,
itemBuilder: (context, index) {
return ListTile(title: Text("$index"),);
}
),
),
floatingActionButton: !showToTopBtn ? null : FloatingActionButton(
child: Icon(Icons.arrow_upward),
onPressed: () {
//返回到顶部时执行动画
_controller.animateTo(.0,
duration: Duration(milliseconds: 200),
curve: Curves.ease
);
}
),
);
}
}
1、ListView
1、SingleChildScrollView
SingleChildScrollView只能接受一个子组件。
当遇到内容较多时,需要滚动组件进行展示,SingleChildScrollView是一个只能包含单个组件的滚动组件,如果内容较多,建议使用ListView等,因为SingleChildScrollView没有“懒加载”模式,性能不如ListView。
Container(
child: SingleChildScrollView(
child: Column(
children: List.generate(10, (index) {
return Container(
height: 100,
color: randomColor(),
);
}).toList(),
),
),
)
2、ListView
ListView 是滚动组件,常用组件之一,用于展示大量数据的列表。 ListView有4种初始化方式
- 1、默认方式:ListView()
- 2、ListView.builder
- 3、ListView.separated
- 4、ListView.custom
我们来介绍几个参数
- 1、temExtent:该参数如果不为null,则会强制children的“长度”为itemExtent的值;这里的“长度”是指滚动方向上子组件的长度,也就是说如果滚动方向是垂直方向,则itemExtent代表子组件的高度;如果滚动方向为水平方向,则itemExtent就代表子组件的宽度。在ListView中,指定itemExtent比让子组件自己决定自身长度会更高效,这是因为指定itemExtent后,滚动系统可以提前知道列表的长度,而无需每次构建子组件时都去再计算一下,尤其是在滚动位置频繁变化时(滚动系统需要频繁去计算列表高度)。
- 2、shrinkWrap:该属性表示是否根据子组件的总长度来设置ListView的长度,默认值为false 。默认情况下,ListView的会在滚动方向尽可能多的占用空间。当ListView在一个无边界(滚动方向上)的容器中时,shrinkWrap必须为true。
- 3、addAutomaticKeepAlives:该属性表示是否将列表项(子组件)包裹在AutomaticKeepAlive 组件中;典型地,在一个懒加载列表中,如果将列表项包裹在AutomaticKeepAlive中,在该列表项滑出视口时它也不会被GC(垃圾回收),它会使用KeepAliveNotification来保存其状态。如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
- 4、addRepaintBoundaries:该属性表示是否将列表项(子组件)包裹在RepaintBoundary组件中。当可滚动组件滚动时,将列表项包裹在RepaintBoundary中可以避免列表项重绘,但是当列表项重绘的开销非常小(如一个颜色块,或者一个较短的文本)时,不添加RepaintBoundary反而会更高效。和addAutomaticKeepAlive一样,如果列表项自己维护其KeepAlive状态,那么此参数必须置为false。
1、默认构造函数
默认构造函数有一个children参数,它接受一个Widget列表(List)。这种方式适合只有少量的子组件的情况,因为这种方式需要将所有children都提前创建好(这需要做大量工作),而不是等到子widget真正显示的时候再创建,也就是说通过默认构造函数构建的ListView没有应用基于Sliver的懒加载模型。实际上通过此方式创建的ListView和使用SingleChildScrollView+Column的方式没有本质的区别
ListView(
padding: const EdgeInsets.all(20.0),
shrinkWrap: true,
children: [
Text("默认构造函数"),
Text("默认构造函数"),
Text("默认构造函数"),
Text("默认构造函数"),
],
)
2、ListView.builder
ListView.builder适合列表项比较多(或者无限)的情况,因为只有当子组件真正显示的时候才会被创建,也就说通过该构造函数创建的ListView是支持基于Sliver的懒加载模型的
ListView.builder({
// ListView公共参数已省略
...
@required IndexedWidgetBuilder itemBuilder,
int itemCount,
...
})
- itemBuilder:它是列表项的构建器,类型为IndexedWidgetBuilder,返回值为一个widget。当列表滚动到具体的index位置时,会调用该构建器构建列表项。
typedef IndexedWidgetBuilder = Widget Function(BuildContext context, int index);
- itemCount:列表项的数量,如果为null,则为无限列表
ListView.builder(
itemCount: 100,
itemExtent: 100.0, //强制高度为100.0
itemBuilder: (BuildContext context, int index){
return CatItem();
}
)
我们创建一个catItem作为一个cell
class CatItem extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Container(
height: 100,
color: Colors.white,
padding: EdgeInsets.all(15),
child: Row(
children: <Widget>[
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: Image.asset(
'images/cat.png',
fit: BoxFit.cover,
width: 80,
height: 80,
),
),
Padding(padding: EdgeInsets.only(left: 15)),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Text(
'小猫',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.w500,
color: Color(0xFF333333),
),
),
Text(
'2021.01.21',
style: TextStyle(
fontSize: 13,
color: Color(0xFF999999),
),
)
],
),
Padding(padding: EdgeInsets.only(top: 20)),
Text(
'我要学习Flutter列表了',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
fontSize: 15,
color: Color(0xFF999999),
),
),
],
),
),
],
),
);
}
}
3、ListView.separated
ListView.separated可以在生成的列表项之间添加一个分割组件,它比ListView.builder多了一个separatorBuilder参数,该参数是一个分割组件生成器。
ListView.separated(
itemBuilder: (BuildContext context, int index){
return CatItem();
},
separatorBuilder: (BuildContext context, int index){
return index%2==0?Divider(color: Colors.red):Divider(color: Colors.black);
},
itemCount: 100)
4、ListView.custom
ListView.custom没有啥特殊的。
我们这里来介绍几个列表样式吧
样式
1、ListTile
const ListTile({
Key key,
this.leading, // item 前置图标
this.title, // item 标题
this.subtitle, // item 副标题
this.trailing, // item 后置图标
this.isThreeLine = false, // item 是否三行显示
this.dense, // item 直观感受是整体大小
this.contentPadding, // item 内容内边距
this.enabled = true,
this.onTap, // item onTap 点击事件
this.onLongPress, // item onLongPress 长按事件
this.selected = false, // item 是否选中状态
})
ListView.builder(
itemCount: 50,
itemBuilder:(BuildContext context, int index){
return ListTile(
leading: new Icon(Icons.keyboard),
title: new Text("Flutter"),
subtitle: new Text("ListTile学习"),
trailing: new Icon(Icons.arrow_forward_ios),
contentPadding: EdgeInsets.symmetric(horizontal: 20.0),
enabled: true,
onTap: () => print("$index被点击了"),
onLongPress: () => print("$index被长按了"),
);
}
)
2、卡片(Card)
Card是让你的列表看起来酷炫最简单的方法了。只需要让Card包裹ListTile
ListView.builder(
itemCount: 50,
itemBuilder:(BuildContext context, int index){
return Card(
child: ListTile(
leading: new Icon(Icons.keyboard),
title: new Text("Flutter"),
subtitle: new Text("ListTile学习"),
trailing: new Icon(Icons.arrow_forward_ios),
contentPadding: EdgeInsets.symmetric(horizontal: 20.0),
enabled: true,
onTap: () => print("$index被点击了"),
onLongPress: () => print("$index被长按了"),
),
);
}
)
5、刷新&加载
实际的产品中,我们还会遇到列表下拉刷新和上拉加载等需求。接下来,就让我们学习下Flutter中应该如何实现此类交互操作。
1、刷新(RefreshIndicator)
下拉刷新
下拉刷新我们可以借助一个RefreshIndicator组件
/**
* 下拉刷新组件
*const RefreshIndicator
({
Key key,
@required this.child,
this.displacement: 40.0, //触发下拉刷新的距离
//下拉回调方法,方法需要有async和await关键字,没有await,刷新图标立马消失,
//没有async,刷新图标不会消失
@required this.onRefresh,
this.color, //进度指示器前景色 默认为系统主题色
this.backgroundColor, //背景色
this.notificationPredicate: defaultScrollNotificationPredicate,
})
*/
要在Flutter中实现列表的下拉刷新效果,其实非常简单,因为Flutter给我们封装好了一个RefreshIndicator组件,使用起来也非常方便。
Future onRefresh() {
return Future.delayed(Duration(seconds: 1), () {
Toast.show('当前已是最新数据', context);
});
}
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
color: red,
child: ListView.builder(
itemBuilder: (context, index){
return Card(
child: CatItem());
},
itemCount: 10,
),
);
}
上拉加载
除了下拉刷新之外,上拉加载是经常会遇到的另一种列表操作。不过,这次Flutter倒是没有像下拉刷新那样提供现成的组件可以直接调用,上拉加载的交互需要我们自己完成
这里我们要借助ScrollController来监听各种状态
- 1、各种初始化数据
//是否正在加载中
bool isLoading = false;
//listview的控制器
ScrollController _scrollController = ScrollController();
//数据源
List<String> data = new List<String>.generate(10, (int index) => "${index}");
- 2、给ScrollController添加监听
@override
void initState() {
super.initState();
// 给列表滚动添加监听
_scrollController.addListener(() {
// 滑动到底部的关键判断
if (
!isLoading &&
_scrollController.position.pixels >= _scrollController.position.maxScrollExtent
) {
// 开始加载数据
setState(() {
isLoading = true;
loadMoreData();
});
}
});
}
- 3、加载跟多数据
///加载更多
Future loadMoreData(){
return Future.delayed(Duration(seconds: 1), () {
setState(() {
isLoading = false;
List<String> list = new List<String>.generate(10, (int index) => "${index}");
data.addAll(list);
});
});
}
- 4、底部加载更多widget
Widget renderBottom() {
if(this.isLoading) {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'努力加载中...',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
Padding(padding: EdgeInsets.only(left: 10)),
SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(strokeWidth: 3),
),
],
),
);
} else {
return Container(
padding: EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
child: Text(
'上拉加载更多',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
);
}
}
- 5、listView添加相关判断
@override
Widget build(BuildContext context) {
return RefreshIndicator(
onRefresh: onRefresh,
color: red,
child: ListView.builder(
controller: _scrollController,
itemCount: data.length + 1,
itemBuilder: (context, index){
if(index < data.length){
return Card(
child: CatItem());
}else{
return renderBottom();
}
},
),
);
}
2、刷新组件(flutter_easyrefresh)
EasyRefresh很容易就能在Flutter应用上实现下拉刷新以及上拉加载操作,它支持几乎所有的Flutter控件
- 1.在 pubspec.yaml 中添加依赖
//pub方式
dependencies:
flutter_easyrefresh: ^2.1.8
//导入方式
dependencies:
flutter_easyrefresh:
path: 项目路径
//git方式
dependencies:
flutter_easyrefresh:
git:
url: git://github.com/xuelongqy/flutter_easyrefresh.git
- 2.在布局文件中添加 EasyreFresh
import 'package:flutter_easyrefresh/easy_refresh.dart';
....
// 方式一
EasyRefresh(
child: ScrollView(),
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
// 方式二
EasyRefresh.custom(
slivers: <Widget>[],
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
// 方式三
EasyRefresh.builder(
builder: (context, physics, header, footer) {
return CustomScrollView(
physics: physics,
slivers: <Widget>[
...
header,
...
footer,
],
);
}
onRefresh: () async{
....
},
onLoad: () async {
....
},
)
- 3.触发刷新和加载动作
EasyRefreshController _controller = EasyRefreshController();
....
EasyRefresh(
controller: _controller,
....
);
....
_controller.callRefresh();
_controller.callLoad();
- 4.控制加载和刷新完成
EasyRefreshController _controller = EasyRefreshController();
....
EasyRefresh(
enableControlFinishRefresh: true,
enableControlFinishLoad: true,
....
);
....
_controller.finishRefresh(success: true);
_controller.finishLoad(success: true, noMore: false);
2、GridView
GridView一共有5个构造函数:
- GridView,
- GridView.builder,
- GridView.count,
- GridView.extent
- GridView.custom
1、默认构造器
其默认构造函数源码如下:
GridView({
Axis scrollDirection = Axis.vertical,
bool reverse = false,
ScrollController controller,
bool primary,
ScrollPhysics physics,
bool shrinkWrap = false,
EdgeInsetsGeometry padding,
@required SliverGridDelegate gridDelegate, //控制子widget layout的委托
bool addAutomaticKeepAlives = true,
bool addRepaintBoundaries = true,
double cacheExtent,
List<Widget> children = const <Widget>[],
})
其他参数大家一看就大概知道什么意思,主要有一个代理gridDelegate
需要注意一下。
gridDelegate参数,类型是SliverGridDelegate,它的作用是控制GridView子组件如何排列(layout)
SliverGridDelegate是一个抽象类,定义了GridView Layout相关接口,子类需要通过实现它们来实现具体的布局算法。
Flutter中提供了两个SliverGridDelegate的子类SliverGridDelegateWithFixedCrossAxisCount和SliverGridDelegateWithMaxCrossAxisExtent,我们可以直接使用
SliverGridDelegateWithFixedCrossAxisCount
该子类实现了一个横轴为固定数量子元素的layout算法,其构造函数为:
SliverGridDelegateWithFixedCrossAxisCount({
@required double crossAxisCount,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
- crossAxisCount:列数,即一行有几个子元素;
- mainAxisSpacing:主轴方向上的空隙间距;
- crossAxisSpacing:次轴方向上的空隙间距;
- childAspectRatio:子元素的宽高比例
class GridViewPage extends StatelessWidget {
const GridViewPage({Key key}) : super(key: key);
_createGridViewItem(Color color){
return Container(
height: 80,
color: color,
);
}
@override
Widget build(BuildContext context) {
return Container(
child: GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing:20,
crossAxisSpacing:10
),
children: [
_createGridViewItem(Colors.primaries[0]),
_createGridViewItem(Colors.primaries[1]),
_createGridViewItem(Colors.primaries[2]),
_createGridViewItem(Colors.primaries[3]),
_createGridViewItem(Colors.primaries[4]),
_createGridViewItem(Colors.primaries[5]),
_createGridViewItem(Colors.primaries[6]),
_createGridViewItem(Colors.primaries[7]),
],
)
);
}
}
2、GridView.count
GridView.count构造函数内部使用了SliverGridDelegateWithFixedCrossAxisCount,我们通过它可以快速的创建横轴固定数量子元素的GridView
@override
Widget build(BuildContext context) {
return Container(
child: GridView.count(
crossAxisCount: 3,
mainAxisSpacing:20,
crossAxisSpacing:10,
children: [
_createGridViewItem(Colors.primaries[0]),
_createGridViewItem(Colors.primaries[1]),
_createGridViewItem(Colors.primaries[2]),
_createGridViewItem(Colors.primaries[3]),
_createGridViewItem(Colors.primaries[4]),
_createGridViewItem(Colors.primaries[5]),
_createGridViewItem(Colors.primaries[6]),
_createGridViewItem(Colors.primaries[7]),
],
)
);
}
3、GridView.extent
GridView.extent构造函数内部使用了SliverGridDelegateWithMaxCrossAxisExtent,我们通过它可以快速的创建纵轴子元素为固定最大长度的的GridView
SliverGridDelegateWithMaxCrossAxisExtent
该子类实现了一个横轴子元素为固定最大长度的layout算法,其构造函数为:
SliverGridDelegateWithMaxCrossAxisExtent({
double maxCrossAxisExtent,
double mainAxisSpacing = 0.0,
double crossAxisSpacing = 0.0,
double childAspectRatio = 1.0,
})
4、GridView.builder
上面我们介绍的GridView都需要一个widget数组作为其子元素,这些方式都会提前将所有子widget都构建好,所以只适用于子widget数量比较少时,当子widget比较多时,我们可以通过GridView.builder来动态创建子widget。GridView.builder 必须指定的参数有两个
GridView.builder(
...
@required SliverGridDelegate gridDelegate,
@required IndexedWidgetBuilder itemBuilder,
)
class MyWidget extends StatelessWidget {
Widget _itemBuilder(BuildContext context, int index){
return Container(
height: 80,
color: Colors.red,
);
}
final SliverGridDelegate _gridDelegate = SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3,
mainAxisSpacing:20,
crossAxisSpacing:10
);
@override
Widget build(BuildContext context) {
return Container(
child: GridView.builder(
gridDelegate: _gridDelegate,
itemCount: 10,
itemBuilder: _itemBuilder)
);
}
}
5、瀑布流
一、简介 flutter staggered gridview是一个支持多列网格大小不同的布局,且Android、iOS、Web都适用 在这种布局中每个单元格都可以称为一个Tile。 它有以下几种特性:
- 可以像GridView一样设置多列
- 在纵轴和主轴上可以设置Tile的个数或者所占用的比例(如crossAxisCount:4,
StaggeredTile.fit(2)则表示在纵轴上有两列,如果StaggeredTile.fit(1)则表示在纵轴上有4列,如果使用StaggeredTile.Count(2,index==0?2.5:3)则表示纵轴有两列并且主轴方向上第一个Tile的大小其他Tile高度的2.5比3)
- 可以设置Tile间的行间距和列间距
- 能够在CustomScollerView内使用(可以用shrinkWrap:true,以及ScrollerController关联两个Widget)
- Tile能够在主轴方向上自适应高度(这是比GridView好的地方,不用设置宽高比,不担心溢出)
pubspec.yaml添加依赖
dependencies:
flutter_staggered_grid_view:
导包
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart’;
StaggeredTile的使用
- StaggeredTile.count:固定纵轴和主轴上的数量
- StaggeredTile.extent:纵轴上的数量和主轴上的最大范围
- StaggeredTile.fit:纵轴上的数量
StaggeredGridView.countBuilder(
crossAxisCount: 4,
itemCount: 8,
itemBuilder: (BuildContext context, int index) => new Container(
color: Colors.red,
child: new Center(
child: new CircleAvatar(
backgroundColor: Colors.white,
child: new Text('$index'),
),
)),
staggeredTileBuilder: (int index) =>
new StaggeredTile.count(2, index.isEven ? 2 : 1),
mainAxisSpacing: 4.0,
crossAxisSpacing: 4.0,
)
转载自:https://juejin.cn/post/6922257495147118605