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进阶使用