本文微信公众号「AndroidTraveler」首发。
背景
本篇主要讲述如何快速在 Flutter 中实现 ListView。
效果图
先上效果图感受一下:
基本实现
1. 确定 Item 项布局
首先我们要先确定我们列表项的布局,我们按照我们效果图上面所显示的,可以写出如下代码:
import 'package:flutter/material.dart';
class ItemWidget extends StatefulWidget {
@override
_ItemWidgetState createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('title'),
SizedBox(height: 6,),
Text('description')
],
);
}
}
显示效果如下:
当然这里的 title 和 description 目前是 hard code,我们第二步确定 Bean 之后会做相应的处理。
2. 确定数据源
我们根据列表项的显示情况可以得到如下 Bean:
class ItemBean {
final String title;
final String description;
ItemBean(this.title, this.description);
}
可以看到就是标题和描述而已。
同时我们第一步的列表项可以更新如下:
import 'package:flutter/material.dart';
import 'package:my_flutter/item_bean.dart';
class ItemWidget extends StatefulWidget {
final ItemBean itemBean;
ItemWidget(this.itemBean);
@override
_ItemWidgetState createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(widget?.itemBean?.title ?? ''),
SizedBox(height: 6,),
Text(widget?.itemBean?.description ?? '')
],
);
}
}
不再 hard code 了。
另外如果你对于 ?. 和 ?? 不熟悉,可以看下我之前的文章 Dart 如何优雅的避空。
3. 显示
有了数据源和显示的 Widget,那么显示也就水到渠成了。
如下:
import 'package:flutter/material.dart';
import 'package:my_flutter/item_bean.dart';
import 'package:my_flutter/item_widget.dart';
class ListViewWidget extends StatefulWidget {
@override
_ListViewWidgetState createState() => _ListViewWidgetState();
}
class _ListViewWidgetState extends State<ListViewWidget> {
final List<ItemBean> itemBeans = [];
@override
void initState() {
super.initState();
_initData();
}
/// 实际场景可能是从网络拉取,这里演示就直接填充数据源了
void _initData() {
itemBeans.add(ItemBean('第一句', '关注微信公众号「AndroidTraveler」'));
itemBeans.add(ItemBean('第二句', '星河滚烫,你是人间理想'));
itemBeans.add(ItemBean('第三句', '我明白你会来,所以我等。'));
itemBeans.add(ItemBean('第四句', '家人闲坐,灯火可亲。'));
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: ListView.builder(
itemCount: itemBeans.length,
itemBuilder: (context, index) {
return ItemWidget(itemBeans[index]);
},
),
);
}
}
列表的关键代码在于:
ListView.builder(
itemCount: itemBeans.length,
itemBuilder: (context, index) {
return ItemWidget(itemBeans[index]);
},
)
还是比较固定的。
最后我们把这个 ListViewWidget 加载到主页面,主页面代码如下:
import 'package:flutter/material.dart';
import 'package:my_flutter/listview_widget.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
body: Center(
child: _buildWidget(),
),
),
);
}
Widget _buildWidget() {
return ListViewWidget();
}
}
运行效果如下:
添加分隔线
看起来还是怪怪的,我们增加下分隔线看看效果。
Flutter 官方 sdk 里面自带了分隔线 Widget,为 Divider。
具体每个属性可以在代码里面看到详细注释,这里就不展开了。
我们的 Divider 代码如下:
Divider(color: Colors.grey,),
很简单,就是指定分隔线的颜色。
因为我们的 Item 本身就是一个 Column,我们直接追加就可以了。
ItemWidget 修改后如下:
···
class _ItemWidgetState extends State<ItemWidget> {
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.only(left: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(widget?.itemBean?.title ?? ''),
SizedBox(
height: 6,
),
Text(widget?.itemBean?.description ?? ''),
Divider(color: Colors.grey),
],
),
);
}
}
效果如下:
可能有小伙伴会说,你这个是刚好 item 布局是 Column,如果不是 Column 的话呢?
方法多种多样,这里就说其中的一种方法吧,比如你可以利用 Stack 来实现。
添加点击回调
我们知道,列表成功显示只是第一步而已,点击能够实现我们期望的效果才是常规操作。
因此,点击回调是必不可少的。
那么如何实现呢?
其实也很简单,就是跟普通 Widget 一样包裹一层 GestureDetector 就可以了。
修改后的 ItemWidget 如下:
···
class _ItemWidgetState extends State<ItemWidget> {
@override
Widget build(BuildContext context) {
Widget container = Container(
padding: EdgeInsets.only(left: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(widget?.itemBean?.title ?? ''),
SizedBox(
height: 6,
),
Text(widget?.itemBean?.description ?? ''),
Divider(color: Colors.grey),
],
),
);
return GestureDetector(
child: container,
onTap: (){
print('onTap');
},
);
}
}
点击 Item 时控制台确实输出了打印日志:
flutter: onTap
flutter: onTap
但是存在两个问题。
第一个就是不知道点击的是哪一个 item,第二个就是一般回调应该是在外层而不应该直接写在里面。
因此我们需要对 ItemWidget 做修改,传入 index 和监听回调。
我们定义的回调接口如下:
/// 定义一个回调接口
typedef OnItemClickListener = void Function(int position, ItemBean itemBean);
ItemWidget 修改后代码如下:
import 'package:flutter/material.dart';
import 'package:my_flutter/item_bean.dart';
/// 定义一个回调接口
typedef OnItemClickListener = void Function(int position, ItemBean itemBean);
class ItemWidget extends StatefulWidget {
final int position;
final ItemBean itemBean;
final OnItemClickListener listener;
ItemWidget(this.position, this.itemBean, this.listener);
@override
_ItemWidgetState createState() => _ItemWidgetState();
}
class _ItemWidgetState extends State<ItemWidget> {
@override
Widget build(BuildContext context) {
Widget container = Container(
padding: EdgeInsets.only(left: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(widget?.itemBean?.title ?? ''),
SizedBox(
height: 6,
),
Text(widget?.itemBean?.description ?? ''),
Divider(color: Colors.grey),
],
),
);
return GestureDetector(
child: container,
onTap: () => widget.listener(widget.position, widget.itemBean),
);
}
}
可以看到我们增加了 position 和 listener。
因此我们的 ListViewWidget 也需要做相应修改:
class ListViewWidget extends StatefulWidget {
final OnItemClickListener listener;
ListViewWidget(this.listener);
@override
_ListViewWidgetState createState() => _ListViewWidgetState();
}
class _ListViewWidgetState extends State<ListViewWidget> {
···
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: ListView.builder(
itemCount: itemBeans.length,
itemBuilder: (context, index) {
return ItemWidget(index, itemBeans[index], widget.listener);
},
),
);
}
}
可以看到改动项就是传入了 listener 并且在 itemBuilder 返回的时候对应传入参数给 ItemWidget。
然后我们在 main.dart 修改如下:
···
class MyApp extends StatelessWidget {
···
Widget _buildWidget() {
return ListViewWidget((position, itemBean){
print('pos=$position, title='+itemBean.title+",description="+itemBean.description);
});
}
}
点击列表,控制台输出期望效果如下:
flutter: pos=0, title=第一句,description=关注微信公众号「AndroidTraveler」
flutter: pos=1, title=第二句,description=星河滚烫,你是人间理想
添加点击视觉反馈
点击是实现了,但是点击之后没有一点点反馈,用户怎么知道自己是不是点击了呢?
因此点击后的视觉反馈也是必不可少的。
那么这个点击后的反馈怎么处理呢?
其实还是离不开 GestureDetector 的回调监听。
当按下时,我们更新颜色值,当抬起或取消时我们恢复颜色值。
因此我们可以修改 ItemWidget 如下:
···
class _ItemWidgetState extends State<ItemWidget> {
Color _color;
@override
void initState() {
super.initState();
_color = Colors.white;
}
@override
Widget build(BuildContext context) {
Widget container = Container(
color: _color,
padding: EdgeInsets.only(left: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(widget?.itemBean?.title ?? ''),
SizedBox(
height: 6,
),
Text(widget?.itemBean?.description ?? ''),
Divider(color: Colors.grey),
],
),
);
return GestureDetector(
child: container,
onTap: () => widget.listener(widget.position, widget.itemBean),
onTapDown: (_) => _updatePressedColor(),
onTapUp: (_) => _updateNormalColor(),
onTapCancel: () => _updateNormalColor(),
);
}
void _updateNormalColor() {
setState(() {
_color = Colors.white;
});
}
void _updatePressedColor() {
setState(() {
_color = Color(0xFFF0F1F2);
});
}
}
效果如下:
可以看到分隔线有点问题,主要原因是 Divider 默认高度是 16.0,所以我们调整下,同时改下 item 的上下间隔。
修改如下:
···
class _ItemWidgetState extends State<ItemWidget> {
···
@override
Widget build(BuildContext context) {
Widget container = Container(
color: _color,
padding: EdgeInsets.only(left: 16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
SizedBox(
height: 8,
),
Text(widget?.itemBean?.title ?? ''),
SizedBox(
height: 6,
),
Text(widget?.itemBean?.description ?? ''),
SizedBox(
height: 8,
),
Divider(color: Colors.grey, height: 0.5,),
],
),
);
return GestureDetector(
child: container,
onTap: () => widget.listener(widget.position, widget.itemBean),
onTapDown: (_) => _updatePressedColor(),
onTapUp: (_) => _updateNormalColor(),
onTapCancel: () => _updateNormalColor(),
);
}
void _updateNormalColor() {
setState(() {
_color = Colors.white;
});
}
void _updatePressedColor() {
setState(() {
_color = Colors.grey;
});
}
}
效果如下:
但是如果你不是长按,而是快速点击,会发现没有效果。
所以我们需要给抬起恢复来个延时,修改如下:
···
void _updateNormalColor() {
Future.delayed(Duration(milliseconds: 100), () {
setState(() {
_color = Colors.white;
});
});
}
···
效果如下:
多种布局处理
这个其实也不难。
我们知道 ListView 的核心代码是:
ListView.builder(
itemCount: itemBeans.length,
itemBuilder: (context, index) {
return ItemWidget(itemBeans[index]);
},
)
因此只需要在 itemBuilder 这里做文章。
举个例子,假设我要求要显示一个纯色块在顶部。
那么我们可以如下修改
···
class _ListViewWidgetState extends State<ListViewWidget> {
···
/// 实际场景可能是从网络拉取,这里演示就直接填充数据源了
void _initData() {
itemBeans.add(ItemBean('', ''));
itemBeans.add(ItemBean('第一句', '关注微信公众号「AndroidTraveler」'));
itemBeans.add(ItemBean('第二句', '星河滚烫,你是人间理想'));
itemBeans.add(ItemBean('第三句', '我明白你会来,所以我等。'));
itemBeans.add(ItemBean('第四句', '家人闲坐,灯火可亲。'));
}
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: ListView.builder(
itemCount: itemBeans.length,
itemBuilder: (context, index) {
if (index == 0) {
return Container(
color: Colors.blue,
height: 66,
);
} else {
return ItemWidget(index, itemBeans[index], widget.listener);
}
},
),
);
}
}
这里通过在一开始添加一个空 Bean,然后在 itemBuilder 做判断返回对应布局来实现。
当然你也可以不在集合添加,但是 index 需要更改,并且列表长度也要修改,等价代码如下:
···
class _ListViewWidgetState extends State<ListViewWidget> {
···
@override
Widget build(BuildContext context) {
return Container(
color: Colors.white,
child: ListView.builder(
itemCount: itemBeans.length + 1,
itemBuilder: (context, index) {
if (index == 0) {
return Container(
color: Colors.blue,
height: 66,
);
} else {
return ItemWidget(index, itemBeans[index - 1], widget.listener);
}
},
),
);
}
}
可以看到 itemCount 和 itemBuilder 都变化了。
效果图如下:
从这个小演示,我们也可以看到关键在于 itemCount 和 itemBuilder 的处理。
只要处理得当,可以实现各种各样的布局。
一般的方式都是通过在 Bean 添加一个 viewType 来区分加载不同的布局。
也可以考虑继承和多态等方式,这里就不展开讲了。
相信小伙伴们都能够自行处理的。
我们一开始的效果图就是这个代码,不过分隔线和视觉反馈的颜色值不一样而已。
说明
由于只是演示,因此有一些地方并没有做额外处理,实际使用需要注意。
- 代码结构,注意按业务或者功能等划分。
- 有些公用的地方可以进行封装,减少后续写多个 ListView 页面时重复代码。
- 代码里面的数据源是直接填充的,实际情况可能是从网络获取。因此需要增加 Bean 相关的 json 解析逻辑。
今天的文章Flutter ListView 实战快速上手分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/20357.html