目录
- 前言:
- 一、实现复杂的ListView列表:
-
- [1.1 Item布局封装](#1.1 Item布局封装)
- [1.2 ListView的使用](#1.2 ListView的使用)
- [1.3 增加分割线](#1.3 增加分割线)
- 二、实现ListView下拉刷新:
- 三、实现上拉加载更多:
- 四、实现下拉刷新、上拉加载更多:
- 五、ListView滚动方向和控制:
- 六、总结:
前言:
上一篇文章介绍了,Flutter学习 滚动组件(1):ListView基本使用介绍了ListView基本使用,这篇文章介绍一下进阶使用的方法。
一、实现复杂的ListView列表:
先看效果图:
1.1 Item布局封装
dart
// 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);
@override
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的使用
dart
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.
@override
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}) {}
@override
Widget build(BuildContext context) {
return showListView();
}
}
1.3 增加分割线
dart
// 显示自定义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:指示器开始显示时与顶部的距离。
示例如下:
dart
class MyRefreshableList extends StatefulWidget {
const MyRefreshableList({super.key});
@override
// 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}'));
});
}
@override
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}) {}
@override
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.
@override
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实现上拉加载更多功能:
dart
class _MyLoadMoreListState extends State<MyLoadMoreList> {
final List<String> items = List.generate(20, (i) => 'Item ${i + 1}');
final ScrollController _scrollController = ScrollController();
bool isLoadingMore = false;
@override
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个item
items.addAll(
List.generate(10, (i) => 'New item ${items.length + i + 1}'));
isLoadingMore = false;
});
}
}
// dispose字段主要用于在异步操作完成后,确保不会调用已经被销毁的State对象的setState方法
@override
void dispose() {
_scrollController.dispose(); // 不要忘记在dispose方法中清理控制器
super.dispose();
}
@override
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()。
效果图如下:
四、实现下拉刷新、上拉加载更多:
我们把下拉刷新和上拉加载结合一起实现:
dart
class PullToRefreshAndLoadMore extends StatefulWidget {
const PullToRefreshAndLoadMore({super.key});
@override
// 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; // 表示是否还有更多数据可加载
@override
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方法
@override
void dispose() {
_scrollController.removeListener(_onScroll); // 移除滚动监听
_scrollController.dispose(); // 清理控制器资源
super.dispose();
}
@override
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属性来控制滚动方向:
dart
ListView.builder(
scrollDirection: Axis.horizontal,
// ...
)
我们将scrollDirection属性设置为Axis.horizontal,创建一个水平滚动的ListView。
如何滚动指定position?看下面例子:
dart
class ScrollToPositionPage extends StatefulWidget {
const ScrollToPositionPage({super.key});
@override
// 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');
@override
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,
);
}
@override
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进阶使用