前言
flutter开发过程中用的除了文本等组件,用的比较多的就是滚动视图了,其中 ListView 使用频率是非常高的,这里介绍一下 ListView常用功能,以及简单定制一个下拉刷新、上拉加载更多
顺道上一张演示图片,下面案例中采用 tabbar 多种情况分开
ListView 常用属性
scrollDirection: 列表的滚动方向,可选值有Axis的horizontal和vertical,可以看到默认是垂直方向上滚动;shrinkWrap:列表都存在该属性,该属性默认为false,表示内部滚动方向无穷长,不会完全计算全部内容长度;如果该属性设置为true,会尽可能缩放滚动视图,一次性计算出所有内容,然后布局;一般嵌套滚动视图时内部设置为true,避免内部计算有问题,根据实际情况使用,一般不推荐,长列表过多计算可能会造成卡顿padding: 列表内边距;controller: 控制器,与列表滚动相关,比如监听列表的滚动事件(一般很少用);physics: 列表滚动至边缘后继续拖动的物理效果
Android与iOS效果不同。默认Android会呈现出一个波纹状(对应ClampingScrollPhysics),而iOS上有一个回弹的弹性效果(对应BouncingScrollPhysics),需要设置成一样的,可以设置,注意BouncingScrollPhysics不满屏不支持滚动。- 如果想不同的平台上呈现各自的效果,且无论内容是否占满,都支持滚动的话,可以使用
AlwaysScrollableScrollPhysics(默认也是这个,如果两端都像ios一样滚动,参数parent: BouncingScrollPhysics即可),它会根据不同平台自动选用各自的物理效果。 - 如果想禁用在边缘的拖动效果,那可以使用
NeverScrollableScrollPhysics;
shrinkWrap: 该属性将决定列表的长度是否仅包裹其内容的长度。当ListView嵌在一个无限长的容器组件中时,shrinkWrap必须为true,否则Flutter会给出警告;itemExtent: 子元素长度。当列表中的每一项长度是固定的情况下可以指定该值,有助于提高列表的性能(因为它可以帮助ListView在未实际渲染子元素之前就计算出每一项元素的位置);cacheExtent: 预渲染区域长度,ListView会在其可视区域的两边留一个cacheExtent长度的区域作为预渲染区域(对于ListView.build或ListView.separated构造函数创建的列表,不在可视区域和预渲染区域内的子元素不会被创建或会被销毁);children: 容纳子元素的组件数组
基础ListView
ListView 就和 ios 的 ScollView 一样,就是一个普通的滚动式图,用于相对比短的列表展示,避免出屏无法显示等问题(如果点击去就会发现,都用到了 SliverChildListDelegate,后面在介绍 Sliver)
基础 ListView 使用如下所示,使用比较简单,往 ListView 里面塞就是了
ListView(
//默认是垂直方向滑动,也可以竖直方向滑动,可以详细查看其它属性
scrollDirection: Axis.vertical,
//设置padding
padding: const EdgeInsets.all(0),
//设置滚动效果,这是比较常用的几个参数了
//都和ios一样支持回弹,不填默认ios回弹,android水波纹
physics: const AlwaysScrollableScrollPhysics(
//两端都拥有弹性效果,若不填这里和默认physics不写一样
parent: BouncingScrollPhysics()
),
//回弹效果,但不满屏不能滑动
// physics: const BouncingScrollPhysics(),
children: [
// const CardView(),
Padding(
padding: const EdgeInsets.all(10),
child: Container(
color: Colors.green,
height: 300,
),
),
Padding(
padding: const EdgeInsets.all(10),
child: Container(
color: Colors.blue,
height: 400,
),
),
],
);
ListView.builder
ListView.builder和 ios 的 UITableView 一样,是支持复用的 ListView,长列表下性能有明显提升,且支持复用,我们一般的长列表就是由多个不同的 item 组成的 builder
ps:flutter 里面没有 section 这个东西,需要的话,可以通过 builder 自行封装,或者是用一个 builder 解决也行
ListView.builder(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics() //两端都拥有弹性效果
),
//这里是ListView的基本属性,都在
padding: const EdgeInsets.all(5),
//数量以及items,必须选项
itemCount: 10,
//单个 item 必填,可以根据不同类型来返回不同的 item
//且 item 可以封装号传入
itemBuilder: (context, index) {
return SizedBox(
height: 60,
child: ListTile(
leading: const SizedBox(
width: 50,
height: 50,
child: CircleAvatar(
backgroundColor: Colors.cyanAccent,
),
),
title: Text(
"标题:$index",
style: const TextStyle(color: Colors.black, fontSize: 16),
),
subtitle: Text(
"我是第$index条内容",
style:
const TextStyle(color: Colors.black54, fontSize: 13),
),
),
);
},
);
ListView.separated
ListView.separated 和 ListView.builder类似,可以理解为item外面多套了一层column罢了(实际实现不是这样的),我们可以根据需要进行选择使用哪一个组件(比较懒,肯定选择代码少的呀😂)
ListView.separated(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics() //两端都拥有弹性效果
),
//这里是ListView的基本属性,都在
padding: const EdgeInsets.all(5),
itemCount: 100,
itemBuilder: (context, index) {
return Container(
color: Colors.white,
alignment: Alignment.centerLeft,
height: 60,
margin: const EdgeInsets.only(left: 6, right: 6),
child: Row(
children: [
Container(
width: 50,
height: 50,
decoration: const BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.all(Radius.circular(10))),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"标题:$index",
style: const TextStyle(color: Colors.black, fontSize: 16),
),
Text(
"我是第$index条内容",
style:
const TextStyle(color: Colors.black54, fontSize: 13),
),
],
),
),
),
],
),
);
},
separatorBuilder: (context, index) {
//返回分割线,widget,自定义container都可以
return const Divider(
height: 0.5,
indent: 60, //距离左侧
endIndent: 0, //距离右侧
color: Colors.grey,
);
},
);
下拉刷新、上拉加载更多
平常我们使用普通的 ListView 也就算了,一旦使用到 builder、separated那大概率会用到下拉刷新、上拉加载了,这里面我们讲解一个直接的简要的实现方式,只是提供一个方便,方便我们在使用类似的功能时,心里面有点数
ps: 实际上这种组件一般是使用三方,简单方便,侵入性差,而不用自己再次封装了走一遍流程了当然封装也没事😂,自定义可以参考这篇文章 juejin.cn
下面使用系统给定的 RefreshIndicator 构建下拉刷新,上拉就是底部多了一个 加载更多字样 提示而已
class RefreshListView extends StatefulWidget {
const RefreshListView({Key? key}) : super(key: key);
@override
State<RefreshListView> createState() => _RefreshListViewState();
}
class _RefreshListViewState extends State<RefreshListView> {
int count = 20;
bool isLoading = false;
Future<void> onRefresh() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
count = 20;
});
}
//直接加载更多
void loadMore() {
//加载标识,用于标识需要加载更多、加载中
if (isLoading) return;
setState(() {
isLoading = true;
});
Future.delayed(const Duration(seconds: 1), () {
setState(() {
count += 10;
isLoading = false;
});
});
}
@override
Widget build(BuildContext context) {
//使用系统给定的 RefreshIndicator 来构建下拉刷新
return RefreshIndicator(
//回调返回一个 Future 用来构建标记请求是否已经结束了
onRefresh: onRefresh,
child: NotificationListener(
//滑动监听,通过更新类名来区分监听信息
onNotification: (ScrollNotification notification) {
//采用通知的方式来监听滑动,避免手势冲突等问题
//下面通过 metrics 参数来判断,当前滑动位置是否已经接近底部,用来及时加载更多
if (notification.metrics.maxScrollExtent - notification.metrics.pixels <= 100) {
loadMore();
}
return false;
},
child: ListView.builder(
physics: const AlwaysScrollableScrollPhysics(
parent: BouncingScrollPhysics() //两端都拥有弹性效果
),
//这里是ListView的基本属性,都在
padding: const EdgeInsets.all(5),
//数量以及items,必须选项
itemCount: count + 1,
itemBuilder: (context, index) {
//当滑道最后一个时,显示加载更多item
if (index >= count) {
return renderBottom();
}
//默认显示我们自己的 item
return SizedBox(
height: 60,
child: ListTile(
leading: const SizedBox(
width: 50,
height: 50,
child: CircleAvatar(
backgroundColor: Colors.blueAccent,
),
),
title: Text(
"标题:$index",
style: const TextStyle(color: Colors.black, fontSize: 16),
),
subtitle: Text(
"我是第$index条内容",
style:
const TextStyle(color: Colors.black54, fontSize: 13),
),
),
);
},
),
),
);
}
Widget renderBottom() {
if(isLoading) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 15),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const <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: const EdgeInsets.symmetric(vertical: 15),
alignment: Alignment.center,
child: const Text(
'上拉加载更多',
style: TextStyle(
fontSize: 15,
color: Color(0xFF333333),
),
),
);
}
}
}
最后
快来试一下吧,后面我们撸其他的