azlistview是一个抖动粘性标题和索引列表视图。基于scrollable_position_listAzListView,暂停视图,索引栏。

特性

  1. 轻松创建城市列表联系人列表界面。
  2. 列表项可以从A到Z分组。
  3. 带有浮动选项的粘性标题。
  4. 支持自定义标题。
  5. 支持指数联动。
  6. IndexBar支持自定义样式。
  7. IndexBar支持本地图像。
  8. 允许滚动到列表中的特定项目。

下面,简单封装下特性!

安装插件

azlistview

1
flutter pub add azlistview

lpinyin

1
flutter pub add lpinyin
  1. azlistview: 帮助我们完成整个listview功能的搭建!
  2. lpinyin: 帮助我们从中文中提取拼音

简单封装

页面使用

联系人页面

contacts.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
// import 'package:azlistview/azlistview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_easyloading/flutter_easyloading.dart';
// import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:get/get.dart';
// import 'package:lpinyin/lpinyin.dart';
import 'package:rent_house/model/contacts_model.dart';
import 'package:rent_house/modules/controllers/contacts_controller.dart';
// import 'package:rent_house/utils/index.dart';
import 'package:rent_house/widgets/azlistview_widget.dart';

class ContactPage extends StatefulWidget {
const ContactPage({super.key});

@override
State<ContactPage> createState() => _ContactPageState();
}

class _ContactPageState extends State<ContactPage> {
final controller = Get.put(ContactsContainer());
List<ContactsModel> contacts = [];
@override
void initState() {
// TODO: implement initState
EasyLoading.show(status: '加载中...', maskType: EasyLoadingMaskType.black);
controller.loadData().then((list) {
contacts.clear();
/* list.forEach((v) {
contacts.add(ContactsModel.fromJson(v));
}); */
contacts = list;
// handleListData(contacts);
EasyLoading.dismiss();
setState(() {});
});
super.initState();
}

/* handleListData(List<ContactsModel> dataList) {
if (dataList.isEmpty) return;
dataList.forEach((item) {
// 转换拼音
String pinyin = PinyinHelper.getPinyinE(item.name);
// 获取拼音首字母并转换大写
String tag = pinyin.substring(0, 1).toUpperCase();
item.namePinyin = pinyin;
if (RegExp("[A-Z]").hasMatch(tag)) {
item.tagIndex = tag;
} else {
item.tagIndex = "#";
}
});

// A-Z sort.
SuspensionUtil.sortListBySuspensionTag(dataList);

// show sus tag.
SuspensionUtil.setShowSuspensionStatus(dataList);
setState(() {});
} */

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text("联系人列表"),
),
/* body: contacts.isEmpty
? Center(
child: Text(
"暂无数据",
style: TextStyle(fontSize: 15.sp, color: Colors.grey[400]),
),
)
: AzListView(
data: contacts,
itemCount: contacts.length,
itemBuilder: (BuildContext context, int index) {
ContactsModel model = contacts[index];
return Utils.getWeChatListItem(
context,
model,
defHeaderBgColor: const Color(0xFFE5E5E5),
);
},
physics: const BouncingScrollPhysics(),
susItemBuilder: (BuildContext context, int index) {
ContactsModel model = contacts[index];
if ('↑' == model.getSuspensionTag()) {
return Container();
}
return Utils.getSusItem(context, model.getSuspensionTag());
},
indexBarData: const ['↑', '☆', ...kIndexBarData],
indexBarOptions: IndexBarOptions(
needRebuild: true,
ignoreDragCancel: true,
downTextStyle:
const TextStyle(fontSize: 12, color: Colors.white),
downItemDecoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.green),
indexHintWidth: ScreenUtil().setWidth(120 / 2),
indexHintHeight: ScreenUtil().setHeight(100 / 2),
/* indexHintDecoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(Utils.getImgPath('ic_index_bar_bubble_gray')),
fit: BoxFit.contain,
),
), */
indexHintAlignment: Alignment.centerRight,
indexHintChildAlignment: const Alignment(-0.25, 0.0),
indexHintOffset: const Offset(-20, 0),
),
)); */
body: AzListViewWidget(
dataList: contacts,
onTap: (p0) {
print("AzListViewWidget ${p0.name}");
},
));
}
}

提示: 注释掉的部分,是未封装前的一个使用!

新建contact_model.dart

lib/model/contact_model.dart

contactModel model,抽象出联系人相关的属性!

contact_model.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'package:azlistview/azlistview.dart';

class ContactsModel extends ISuspensionBean {
String name;
String? tagIndex;
String? namePinyin;

ContactsModel(this.name, this.tagIndex, this.namePinyin);

ContactsModel.fromJson(Map<String, dynamic> json)
: name = json["name"],
tagIndex = json["tagIndex"],
namePinyin = json["namePinyin"];

@override
String getSuspensionTag() => tagIndex!;
}

注意: ISuspensionBean: 该model extents 了这个抽象类,这个抽象类是azlistview定义的,用来标识每个标签的当前索引标签!

新建azlistview_widget.dart

lib/widgets/azlistview_widget.dart

azlistview_widget.dart
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
import 'package:azlistview/azlistview.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
// import 'package:lpinyin/lpinyin.dart';
// import 'package:rent_house/utils/index.dart';

class AzListViewWidget extends StatefulWidget {
final List<ISuspensionBean> dataList;
final void Function(dynamic) onTap;
const AzListViewWidget(
{super.key,
required this.dataList,
required this.onTap});

@override
State<AzListViewWidget> createState() => _AzListViewWidgetState();
}

class _AzListViewWidgetState extends State<AzListViewWidget> {
@override
void initState() {
// TODO: implement initState
print("initState ${widget.dataList}");
super.initState();
}

@override
Widget build(BuildContext context) {
return widget.dataList.isEmpty
? Center(
child: Text(
"暂无数据",
style: TextStyle(fontSize: 15.sp, color: Colors.grey[400]),
),
)
: AzListView(
data: widget.dataList,
itemCount: widget.dataList.length,
itemBuilder: (BuildContext context, int index) {
dynamic model = widget.dataList[index];
/* return Utils.getWeChatListItem(
context,
widget.dataList[index],
defHeaderBgColor: const Color(0xFFE5E5E5),
); */
return getWeChatItem(context, model, widget.onTap);
},
physics: const BouncingScrollPhysics(),
susItemBuilder: (BuildContext context, int index) {
dynamic model = widget.dataList[index];
if ('↑' == model.getSuspensionTag()) {
return Container();
}
return getSusItem(context, model.getSuspensionTag());
},
indexBarData: const ['↑', '☆', ...kIndexBarData],
indexBarOptions: IndexBarOptions(
needRebuild: true,
ignoreDragCancel: true,
downTextStyle: const TextStyle(fontSize: 12, color: Colors.white),
downItemDecoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.green),
indexHintWidth: ScreenUtil().setWidth(120 / 2),
indexHintHeight: ScreenUtil().setHeight(100 / 2),
/* indexHintDecoration: BoxDecoration(
image: DecorationImage(
image: AssetImage(Utils.getImgPath('ic_index_bar_bubble_gray')),
fit: BoxFit.contain,
),
), */
indexHintAlignment: Alignment.centerRight,
indexHintChildAlignment: const Alignment(-0.25, 0.0),
indexHintOffset: const Offset(-20, 0),
),
);
}
}

Widget getWeChatItem(
BuildContext context, dynamic model, void Function(dynamic item) onTap) {
DecorationImage? image;
return ListTile(
leading: Container(
width: 36.w,
height: 36.h,
decoration: BoxDecoration(
shape: BoxShape.rectangle,
borderRadius: BorderRadius.circular(4.0),
color: Colors.blue[200],
image: image,
),
child: const Icon(Icons.abc_sharp),
),
title: Text(model.name),
onTap: () {
onTap(model);
},
);
}

Widget getSusItem(BuildContext context, String tag, {double susHeight = 40}) {
if (tag == '★') {
tag = '★ 热门城市';
}
return Container(
height: ScreenUtil().setHeight(susHeight),
width: MediaQuery.of(context).size.width,
padding: const EdgeInsets.only(left: 16.0),
color: const Color(0xFFF3F4F5),
alignment: Alignment.centerLeft,
child: Text(
tag,
softWrap: false,
style: const TextStyle(
fontSize: 14.0,
color: Color(0xFF666666),
),
),
);
}

两个参数 dataList onTab!
dataList: 原数据
onTab: 点击的某一项 model

处理获取到的数据

下面通过后端获取到数据后做一层处理! 处理的逻辑放到了contacts_controller.dart里!
主要对数据做了 拼音 提取 排序等操作!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// import 'package:azlistview/azlistview.dart';
import 'package:azlistview/azlistview.dart';
import 'package:get/get.dart';
import 'package:lpinyin/lpinyin.dart';
import 'package:rent_house/api/system/user.dart';
// import 'package:lpinyin/lpinyin.dart';
import 'package:rent_house/model/contacts_model.dart';

class ContactsContainer extends GetxController {
RxList<ContactsModel> contacts = <ContactsModel>[].obs;

@override
void onInit() async {
// TODO: implemendataList
super.onInit();
}

Future<dynamic> loadData() async {
// String data = await rootBundle.loadString("assets/data/contacts.json");
// List<dynamic> resvole = jsonDecode(data);
Map res = await UserAPI.getUserList();
if (!res["success"]) return [];
contacts.clear();
res["data"].forEach((d) {
contacts.add(ContactsModel.fromJson(d));
});
return handleListData(contacts);
}

handleListData(List<ContactsModel> dataList) {
if (dataList.isEmpty) return;
dataList.forEach((item) {
// 转换拼音
String pinyin = PinyinHelper.getPinyinE(item.name);
// 获取拼音首字母并转换大写
String tag = pinyin.substring(0, 1).toUpperCase();
item.namePinyin = pinyin;
if (RegExp("[A-Z]").hasMatch(tag)) {
item.tagIndex = tag;
} else {
item.tagIndex = "#";
}
});

// A-Z sort.
SuspensionUtil.sortListBySuspensionTag(dataList);

// show sus tag.
SuspensionUtil.setShowSuspensionStatus(dataList);
return dataList;
}
}