当前位置: 首页 > news >正文

Flutter学习 滚动组件(2):ListView进阶使用

目录

  • 前言:
  • 一、实现复杂的ListView列表:
    • 1.1 Item布局封装
    • 1.2 ListView的使用
    • 1.3 增加分割线
  • 二、实现ListView下拉刷新:
  • 三、实现上拉加载更多:
  • 四、实现下拉刷新、上拉加载更多:
  • 五、ListView滚动方向和控制:
  • 六、总结:

前言:

上一篇文章介绍了,Flutter学习 滚动组件(1):ListView基本使用介绍了ListView基本使用,这篇文章介绍一下进阶使用的方法。

一、实现复杂的ListView列表:

先看效果图:
效果图

1.1 Item布局封装

// list item
class ListItem {ImageProvider image; // 图片var title; // 标题var author; // 作者var summary; // 摘要ListItem({required this.image, this.title, this.author, this.summary});
}// list item界面实现
typedef OnItemClickListener = void Function();class ListItemView extends StatelessWidget {final ListItem data;final OnItemClickListener onItemClickListener;const ListItemView({required Key key, required this.data, required this.onItemClickListener}): super(key: key);Widget build(BuildContext context) {var headIcon = Container(// 左边头部decoration: BoxDecoration(color: Colors.white,shape: BoxShape.circle,boxShadow: [BoxShadow(color: Colors.grey.withOpacity(0.3),offset: const Offset(0.0, 0.0),blurRadius: 3.0,spreadRadius: 0.0,),],),width: 70,height: 70,child: Padding(padding: const EdgeInsets.all(3),child: CircleAvatar(backgroundImage: data.image,),));var center = Column(// 中间介绍mainAxisAlignment: MainAxisAlignment.center,crossAxisAlignment: CrossAxisAlignment.start,mainAxisSize: MainAxisSize.min,children: [Text(data.title,style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),Padding(padding: const EdgeInsets.only(top: 8),child: Text("作者:${data.author}",style: const TextStyle(color: Colors.grey, fontSize: 12),),),],);var summary = Text(// 尾部摘要data.summary,maxLines: 3,overflow: TextOverflow.ellipsis,style: const TextStyle(color: Colors.grey, fontSize: 12),);var item = Row(// 条目拼合mainAxisAlignment: MainAxisAlignment.start,children: [const SizedBox(width: 10),headIcon,Padding(padding: const EdgeInsets.symmetric(horizontal: 20),child: center,),Expanded(child: summary,),const SizedBox(width: 10),],);var result = Card(// 卡片化+事件监听elevation: 5,child: InkWell(onTap: onItemClickListener,child: Padding(padding: const EdgeInsets.all(10),child: item,)));return result;}
}

1.2 ListView的使用

Widget showListView() {var data = [];for (var i = 0; i < 20; i++) {data.add(ListItem(image: const AssetImage("assets/images/android_fly.webp"),title: "$i:指鹿为马",author: "ddup",summary: "公元前210年,秦始皇病死,担任中车府令(掌管皇帝车马)的宦官赵高,不愿让秦始皇的大儿子扶苏继承皇位,而想让秦始皇的小儿子胡亥当皇帝。"));}return ListView.builder(padding: const EdgeInsets.all(8.0),itemCount: data.length, //条目的个数itemBuilder: (BuildContext context, int index) {return ListItemView(//数据填充条目data: data[index],onItemClickListener: () {//事件响应print(index);},key: UniqueKey());});
}class MyApp extends StatelessWidget {final List<String> items;const MyApp({super.key, required this.items});// This widget is the root of your application.Widget build(BuildContext context) {const title = 'ListView的使用';return MaterialApp(debugShowCheckedModeBanner: false,title: title,theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),useMaterial3: true,),home: Scaffold(appBar: AppBar(title: const Text(title),),body:MyHomeBody(),));}
}class MyHomeBody extends StatelessWidget {List<String> list = [];MyHomeBody({super.key}) {}Widget build(BuildContext context) {return showListView();}
}

1.3 增加分割线

// 显示自定义ListView
Widget showListView() {var data = [];for (var i = 0; i < 20; i++) {data.add(ListItem(image: const AssetImage("assets/images/android_fly.webp"),title: "$i:指鹿为马",author: "ddup",summary: "公元前210年,秦始皇病死,担任中车府令(掌管皇帝车马)的宦官赵高,不愿让秦始皇的大儿子扶苏继承皇位,而想让秦始皇的小儿子胡亥当皇帝。"));}return ListView.separated(padding: const EdgeInsets.all(8.0),itemCount: data.length, // 条目的个数itemBuilder: (BuildContext context, int index) {return ListItemView(// 数据填充条目data: data[index],onItemClickListener: () {// 事件响应print(index);},key: UniqueKey(),);},separatorBuilder: (BuildContext context, int index) {return const Padding(padding: EdgeInsets.only(left: 90),child: Divider(height: 1,color: Colors.blue,),);},);
}

效果如下:
增加分割线

二、实现ListView下拉刷新:

RefreshIndicator是Flutter用于实现下拉刷新的功能组件,RefreshIndicator可以包裹一个可以滚动的组件,如ListView、GridView,下拉到顶部时会触发刷新操作,调用onRefresh方法,这方法返回一个Future 的异步函数,用于执行刷新操作。RefreshIndicator常见属性如下:

  • onRefresh: 必须实现的回调函数,执行刷新时的操作。
  • child: 需要包裹的可滚动子组件。
  • color:刷新指示器的进度条颜色。
  • backgroundColor: 刷新指示器的背景色。
  • displacement:指示器开始显示时与顶部的距离。

示例如下:

class MyRefreshableList extends StatefulWidget {const MyRefreshableList({super.key});// ignore: library_private_types_in_public_api_MyRefreshableListState createState() => _MyRefreshableListState();
}class _MyRefreshableListState extends State<MyRefreshableList> {final List<String> items = List.generate(5, (i) => 'Item ${i + 1}');Future<void> _onRefresh() async {await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求// 更新数据setState(() {items.addAll(List.generate(10, (i) => 'New item ${i + items.length + 1}'));});}Widget build(BuildContext context) {return RefreshIndicator(onRefresh: _onRefresh,child: ListView.builder(itemCount: items.length,itemBuilder: (context, index) {return ListTile(title: Text(items[index]));},),);}
}class MyHomeBody extends StatelessWidget {MyHomeBody({super.key}) {}Widget build(BuildContext context) {return MyRefreshableList();}
}
class MyApp extends StatelessWidget {final List<String> items;const MyApp({super.key, required this.items});// This widget is the root of your application.Widget build(BuildContext context) {const title = 'ListView的使用';return MaterialApp(debugShowCheckedModeBanner: false,title: title,theme: ThemeData(colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),useMaterial3: true,),home: Scaffold(appBar: AppBar(title: const Text(title),),body:MyHomeBody(),));}
}

这个例子是在下拉刷新onRefresh回调中,模拟了一个2s延时的网络请求,增加了10个新的item条目。
效果如下:
下拉刷新


三、实现上拉加载更多:

上拉加载更多功能可以利用ScrollController判断是否滚动到底部,执行loadmore实现上拉加载更多功能:

class _MyLoadMoreListState extends State<MyLoadMoreList> {final List<String> items = List.generate(20, (i) => 'Item ${i + 1}');final ScrollController _scrollController = ScrollController();bool isLoadingMore = false;void initState() {super.initState();_scrollController.addListener(() {// 滑动到底部时触发加载更多(gif加载动画)if (_scrollController.position.pixels == // scrollController.position.pixels:表示当前滚动的位置_scrollController.position.maxScrollExtent) { // scrollController.position.maxScrollExtent:表示可滚动区域的最大值_loadMore();}});}// 回调函数,执行刷新时的操作Future<void> _loadMore() async {if (!isLoadingMore) {setState(() => isLoadingMore = true);// 模拟网络请求结束后加载更多数据await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求延迟setState(() {// 刷新操作:在底部增加10个itemitems.addAll(List.generate(10, (i) => 'New item ${items.length + i + 1}'));isLoadingMore = false;});}}// dispose字段主要用于在异步操作完成后,确保不会调用已经被销毁的State对象的setState方法void dispose() {_scrollController.dispose(); // 不要忘记在dispose方法中清理控制器super.dispose();}Widget build(BuildContext context) {return ListView.builder(controller: _scrollController,itemCount: items.length + 1, // 添加一个进度指示器作为最后一项itemBuilder: (context, index) {if (index == items.length) { // 最后一项作为进度指示器return Visibility(visible: isLoadingMore,child: const Center(child: CircularProgressIndicator(),),);}return ListTile(title: Text(items[index]));},);}
}

上述代码可以看到,我们在initState方法,增加ListView滚动监听,position.pixels == position.maxScrollExtent执行onLoadMore方法,增加一个isLoadingMore变量来控制重复刷新state,另外我们把loading条放在ListView最后一个条目加1,这样不会遮挡ListView条目,最后通过Visibility来控制loading条的显隐。最后需要注意的一点是,我们在dispose方法时,调用_scrollController.dispose()。
效果图如下:
上拉加载更多

四、实现下拉刷新、上拉加载更多:

我们把下拉刷新和上拉加载结合一起实现:

class PullToRefreshAndLoadMore extends StatefulWidget {const PullToRefreshAndLoadMore({super.key});// ignore: library_private_types_in_public_api_PullToRefreshAndLoadMoreState createState() =>_PullToRefreshAndLoadMoreState();
}class _PullToRefreshAndLoadMoreState extends State<PullToRefreshAndLoadMore> {final List<String> _items = List.generate(20, (i) => 'Item ${i + 1}');final ScrollController _scrollController = ScrollController();bool _isLoadingMore = false;bool _hasMore = true; // 表示是否还有更多数据可加载void initState() {super.initState();_scrollController.addListener(_onScroll);}Future<void> _onRefresh() async {await Future.delayed(const Duration(seconds: 2));setState(() {_items.clear();_items.addAll(List.generate(20, (i) => 'Refreshed item ${i + 1}'));});}void _onScroll() {// 检测是否滚动到底部if (_scrollController.position.pixels >= // scrollController.position.maxScrollExtent:表示可滚动区域的最大值_scrollController.position.maxScrollExtent && // scrollController.position.maxScrollExtent:表示可滚动区域的最大值!_isLoadingMore &&_hasMore) {_loadMore();}}Future<void> _loadMore() async {if (_isLoadingMore) return; // 如果已经在加载,则不执行后续操作setState(() {_isLoadingMore = true;});await Future.delayed(const Duration(seconds: 2));if (mounted) {setState(() {_items.addAll(List.generate(10, (i) => 'New item ${_items.length + i + 1}'));// 假设每次增加了10个数据,加载了5次后认为没有更多数据if (_items.length >= 70) {_hasMore = false;}_isLoadingMore = false;});}}// dispose字段主要用于在异步操作完成后,确保不会调用已经被销毁的State对象的setState方法void dispose() {_scrollController.removeListener(_onScroll); // 移除滚动监听_scrollController.dispose(); // 清理控制器资源super.dispose();}Widget build(BuildContext context) {return Scaffold(appBar: AppBar(title: const Text('Pull to Refresh & Load More'),),body: RefreshIndicator(onRefresh: _onRefresh,child: ListView.builder(controller: _scrollController,itemCount: _hasMore? _items.length + 1: _items.length, // 如果还有更多数据,添加额外一项来显示加载指示器itemBuilder: (context, index) {if (index == _items.length && _hasMore) { // 最后一项为加载进度指示器return const Center(child: Padding(padding: EdgeInsets.all(8.0),child: CircularProgressIndicator(),),);}return ListTile(title: Text(_items[index]));},),),);}
}

上述示例,_loadMore会在滚动到底部时才会触发,并用setState来管理状态变化,并且通过_isLoadingMore来防止重复加载操作,以及用_hasMore判断是否还有更多数据需要加载,需要注意的是mounted检查以确保不会在Widget树移除后调用setState方法。

五、ListView滚动方向和控制:

ListView有两种滚动方向,垂直(默认)和水平,我们可以通过修改scrollDirection属性来控制滚动方向:

ListView.builder(scrollDirection: Axis.horizontal,// ...
)

我们将scrollDirection属性设置为Axis.horizontal,创建一个水平滚动的ListView。
如何滚动指定position?看下面例子:

class ScrollToPositionPage extends StatefulWidget {const ScrollToPositionPage({super.key});// ignore: library_private_types_in_public_api_ScrollToPositionPageState createState() => _ScrollToPositionPageState();
}class _ScrollToPositionPageState extends State<ScrollToPositionPage> {final ScrollController _scrollController = ScrollController();final List<String> items = List.generate(100, (i) => 'Item $i');void dispose() {_scrollController.dispose();super.dispose();}void _scrollToIndex(int index) {// 滚动到指定索引的位置_scrollController.animateTo(_scrollController.positions.first.maxScrollExtent * (index / items.length),duration: const Duration(milliseconds: 300),curve: Curves.easeOut,);}Widget build(BuildContext context) {return Scaffold(body: ListView.builder(controller: _scrollController,itemCount: items.length,itemBuilder: (context, index) {return ListTile(title: Text(items[index]),);},),floatingActionButton: FloatingActionButton(onPressed: () => _scrollToIndex(15), // 假设我们想要滚动到第16个元素的位置child: const Icon(Icons.arrow_downward),),);}
}

我们可以看到,上述例子利用ScrollController执行animateTo动画,根据_scrollController.positions.first.maxScrollExtent * (index / items.length)计算,滚动指定position位置。
效果图如下:
滚动方向和控制

六、总结:

我们通过定义一个复杂的ListView布局和增加分割线,以及增加下拉刷新、上拉加载、修改ListView滚动方向,滚动到指定位置来介绍了一下ListView进阶使用,希望大家可以通过这些例子,更好的掌握ListView.

Thanks:
Flutter可滚动组件(3):ListView进阶使用

相关文章:

  • 如何防止接口被刷
  • Elasticsearch只返回指定的字段(用_source)
  • 数据可视化(Matplotlib和pyecharts)
  • 【Leetcode 每日一题】2563. 统计公平数对的数目
  • LeetCode 热题 100_乘积最大子数组(88_152_中等_C++)(动态规划)
  • rpcrt4!COMMON_AddressManager函数分析之和全局变量rpcrt4!AddressList的关系
  • 纯FPGA实现AD9361控制的思路和实现 UART实现AXI_MASTER
  • 《AI大模型应知应会100篇》第26篇:Chain-of-Thought:引导大模型进行步骤推理
  • 常见设计模式
  • Github 2025-04-19Rust开源项目日报 Top10
  • 清华《数据挖掘算法与应用》K-means聚类算法
  • Redis--主从复制
  • 记录一次项目中使用pdf预览过程以及遇到问题以及如何解决
  • 【Bluedroid】蓝牙存储模块配置管理:启动、读写、加密与保存流程解析
  • Unity webgl 获取图片或视频数据
  • UI键盘操作
  • 机器学习+深度学习
  • 开发基于python的商品推荐系统,前端框架和后端框架的选择比较
  • 青少年编程与数学 02-016 Python数据结构与算法 30课题、数据压缩算法
  • 基于DeepSeek与Excel的动态图表构建:技术融合与实践应用
  • 五一出境游火爆:境外包车订单增长25%,日本酒店价格贵了好几倍
  • 广西:启动旱灾防御三级应急响应
  • 黄仁勋结束年内第二次中国行:关键时刻,重申对中国市场承诺
  • 海口市美兰区委副书记、区长吴升娇去世,终年41岁
  • 雅生活服务:向雅居乐收购两家环保公司,总价约6060万元
  • 女子斥“老法师”涉嫌偷拍?街头摄影的边界应该怎么定?