Flutter可滚动组件(3):ListView进阶使用
前一篇博客:Flutter可滚动组件(2):ListView基本使用 介绍了 ListView 的基本使用,下面通过一个示例介绍一下 ListView 的各种进阶用法。
一、实现复杂自定义ListView
先看下效果图:
1.1 完成条目的封装
// ignore_for_file: prefer_typing_uninitialized_variables, avoid_print
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: MyHomeBody(),
),
);
}
}
// 诗歌item
class PoemItem {
ImageProvider image; // 图片
var title; // 标题
var author; // 作者
var summary; // 摘要
PoemItem({required this.image, this.title, this.author, this.summary});
}
// 诗歌item界面实现
typedef OnItemClickListener = void Function();
class PoemItemView extends StatelessWidget {
final PoemItem data;
final OnItemClickListener onItemClickListener;
const PoemItemView(
{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;
}
}
// 显示自定义ListView
Widget showListView() {
var data = [];
for (var i = 0; i < 20; i++) {
data.add(PoemItem(
image: const AssetImage("assets/head.jpeg"),
title: "$i:以梦为马",
author: "海子",
summary: "我要做远方的忠诚的儿子,和物质的短暂情人,和所有以梦为马的诗人一样,我不得不和烈士和小丑走在同一道路上"));
}
return ListView.separated(
padding: const EdgeInsets.all(8.0),
itemCount: data.length, // 条目的个数
itemBuilder: (BuildContext context, int index) {
return PoemItemView(
// 数据填充条目
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.orangeAccent,
),
);
},
);
}
1.2 ListView的使用
在构造器构造条目时,使用数据对条目进行数据填充,以达到数据展示效果:
// 显示自定义ListView
Widget showListView() {
var data = [];
for (var i = 0; i < 20; i++) {
data.add(PoemItem(
image: const AssetImage("assets/head.jpeg"),
title: "$i:以梦为马",
author: "海子",
summary: "我要做远方的忠诚的儿子,和物质的短暂情人,和所有以梦为马的诗人一样,我不得不和烈士和小丑走在同一道路上"));
}
return ListView.builder(
padding: const EdgeInsets.all(8.0),
itemCount: data.length, //条目的个数
itemBuilder: (BuildContext context, int index) {
return PoemItemView(
//数据填充条目
data: data[index],
onItemClickListener: () {
//事件响应
print(index);
},
key: UniqueKey()
);
});
}
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});
@override
Widget build(BuildContext context) {
return showListView();
}
}
至此这个代码初步完成。
二、分隔线的添加
这里现将 Card 组件去掉。separated 可以很轻松的实现下划线分隔,并且容易改变长短,颜色 如果你愿意,也可以去定义分隔的组件,专门作为分隔线。而且还能使用索引进行个性化设计:
// 显示自定义ListView,添加分割线
Widget showListViewDivider() {
var data = [];
for (var i = 0; i < 20; i++) {
data.add(PoemItem(
image: const AssetImage("assets/head.jpeg"),
title: "$i:以梦为马",
author: "海子",
summary: "我要做远方的忠诚的儿子,和物质的短暂情人,和所有以梦为马的诗人一样,我不得不和烈士和小丑走在同一道路上"));
}
return ListView.separated(
padding: const EdgeInsets.all(8.0),
itemCount: data.length, // 条目的个数
itemBuilder: (BuildContext context, int index) {
return PoemItemView(
// 数据填充条目
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.orangeAccent,
),
);
},
);
}
class MyHomeBody extends StatelessWidget {
const MyHomeBody({super.key});
@override
Widget build(BuildContext context) {
return showListViewDivider();
}
}
效果图如下所示:
三、ListView下拉刷新
RefreshIndicator
是 Flutter 中用于实现下拉刷新功能的一个组件。RefreshIndicator
包裹一个可滚动的子组件,如 ListView 或 GridView,当用户下拉到列表顶部时,会触发刷新操作。onRefresh
是一个返回 Future
的异步函数,用于执行刷新操作。
RefreshIndicator
关键属性:
onRefresh
: 必须实现的回调函数,执行刷新时的操作。child
: 需要包裹的可滚动子组件。color
: 刷新指示器的进度条颜色。backgroundColor
: 刷新指示器的背景色。displacement
: 指示器开始显示时与顶部的距离。
示例如下:
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: MyRefreshableList(),
),
);
}
}
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(20, (i) => 'Item ${i + 1}');
Future<void> _onRefresh() async {
await Future.delayed(const Duration(seconds: 2)); // 模拟网络请求
// 更新数据
setState(() {
items
.addAll(List.generate(20, (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]));
},
),
);
}
}
在这个例子中,RefreshIndicator
配合ListView.builder
实现下拉刷新。_onRefresh
函数模拟了一个网络请求,并在完成后更新数据。效果图如下所示:
四、ListView上拉加载更多
RefreshIndicator
结合ScrollController
和底部加载指示器可以进一步实现上拉加载更多功能。
示例如下:
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: MyLoadMoreList(),
),
);
}
}
class MyLoadMoreList extends StatefulWidget {
const MyLoadMoreList({super.key});
@override
// ignore: library_private_types_in_public_api
_MyLoadMoreListState createState() => _MyLoadMoreListState();
}
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]));
},
);
}
}
在这个例子中,我们使用一个ScrollController
来检测列表是否滚动到底部。一旦到达底部,_loadMore
函数会被触发,模拟加载更多内容的过程。同时,为了防止多次触发加载更多操作,我们添加了一个isLoadingMore
变量作为标记。
ListView.builder
的itemCount
设置为items.length + 1
,这样列表的最后一项就是一个CircularProgressIndicator
,显示加载中的动画。用Visibility
控制它的显示,仅在加载更多数据时可见。
建议
- 始终要记得在实现上拉加载时,需要在加载过程中防止重复触发加载方法。
- 在生产环境中,用实际的网络请求替代示例中的延迟函数(即
Future.delayed()
),实现真正的分页加载数据。 - 为了提升用户体验,可以在数据加载完成后,稍微滚动列表(通过
ScrollController
),以避免CircularProgressIndicator
直接覆盖在最后一个列表项上。
效果图如下所示:
五、结合下拉刷新和上拉加载更多功能
下面是一个完整的实现示例,结合下拉刷新和上拉加载更多功能:
import 'package:flutter/material.dart';
main(List<String> args) {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
body: PullToRefreshAndLoadMore(),
),
);
}
}
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
。
通过结合RefreshIndicator
和ListView.builder
,你可以创建一个用户友好的列表,支持下拉刷新和上拉加载更多数据,从而模拟典型的移动应用中常见的列表行为。请根据实际的业务逻辑和数据来源,适当调整示例中的模拟延迟和加载逻辑。
六、ListView的滚动方向和控制
ListView有两种滚动方向可供选择:垂直(默认)和水平。你可以使用scrollDirection
属性来控制滚动方向。下面是一个例子:
ListView.builder(
scrollDirection: Axis.horizontal,
// ...
)
在这个例子中,我们将scrollDirection
属性设置为Axis.horizontal
,以创建一个水平滚动的ListView。
如果你想控制ListView的滚动行为,你可以使用controller
属性并配合ScrollController
来实现。
下面是一个示例,滚动 ListView 到指定的位置:
import 'package:flutter/material.dart';
void main() => runApp(const MyApp());
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: ScrollToPositionPage(),
);
}
}
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(25), // 假设我们想要滚动到第26个元素的位置
child: const Icon(Icons.arrow_downward),
),
);
}
}
在这个例子中,我们创建了一个ScrollController
对象,并将其分配给 ListView。我们还实现了一个_scrollToIndex
方法,该方法接受一个索引参数,并使用animateTo
方法滚动到该索引对应的位置。当点击浮动按钮时,我们调用_scrollToIndex(25)
,假设我们想要滚动到第 26 个元素的位置。
效果图如下所示:
参考:
- Flutter 基础篇 -] ListView的使用-腾讯云开发者社区-腾讯云
Flutter下拉刷新和上拉加载在Flutter中,可以通过多种方式实现下拉刷新和上拉加载的功能。其中一个非常流行的做法 - 掘金
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· 实操Deepseek接入个人知识库
· CSnakes vs Python.NET:高效嵌入与灵活互通的跨语言方案对比
· 【.NET】调用本地 Deepseek 模型
· Plotly.NET 一个为 .NET 打造的强大开源交互式图表库