本文微信公众号「AndroidTraveler」首发。git
本篇主要讲述如何快速在 Flutter 中实现 ListView。github
先上效果图感觉一下: json
首先咱们要先肯定咱们列表项的布局,咱们按照咱们效果图上面所显示的,能够写出以下代码:bash
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 以后会作相应的处理。网络
咱们根据列表项的显示状况能够获得以下 Bean:less
class ItemBean {
final String title;
final String description;
ItemBean(this.title, this.description);
}
复制代码
能够看到就是标题和描述而已。ide
同时咱们第一步的列表项能够更新以下:布局
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 了。post
另外若是你对于 ?. 和 ?? 不熟悉,能够看下我以前的文章 Dart 如何优雅的避空。
有了数据源和显示的 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 来区分加载不一样的布局。
也能够考虑继承和多态等方式,这里就不展开讲了。
相信小伙伴们都可以自行处理的。
咱们一开始的效果图就是这个代码,不过度隔线和视觉反馈的颜色值不同而已。
因为只是演示,所以有一些地方并无作额外处理,实际使用须要注意。