用flutter模仿微信通讯录列表写的一个demo,具体效果看gif图
list .gif
下面附上代码和 Demo,注释写的还是比较清楚的,这里就不做一一介绍了,
入口是ListPage类,
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:list/list/model/user_name.dart';
import 'package:list/list/pages/common.dart';
import 'package:list/list/pages/index_bar.dart';
import 'package:list/list/pages/item_cell.dart';
import 'package:list/list/pages/search_widget.dart';
class ListPage extends StatefulWidget {
const ListPage({super.key});
@override
State<ListPage> createState() => _ListPageState();
}
class _ListPageState extends State<ListPage> {
List<DataList> _data = [];
final List<DataList> _dataList = []; //数据
late ScrollController _scrollController;
//字典 里面放item和高度对应的数据
final Map<String, double> _groupOffsetMap = {
INDEX_WORDS[0]: 0.0, //放大镜
INDEX_WORDS[1]: 0.0, //⭐️
};
String searchStr = '';
@override
void initState() {
_load();
super.initState();
_scrollController = ScrollController();
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _load() async {
String jsonData = await loadJsonFromAssets('assets/data.json');
Map<String, dynamic> dict = json.decode(jsonData);
List<dynamic> list = dict['data_list'];
_data = list.map((e) => DataList.fromJson(e)).toList();
// 排序
_data.sort((a, b) => a.indexLetter.compareTo(b.indexLetter));
_dataList.addAll(_data);
// 循环计算,将每个头的位置算出来,放入字典
var groupOffset = 0.0;
for (int i = 0; i < _dataList.length; i++) {
if (i < 1) {
//第一个cell一定有头
_groupOffsetMap.addAll({_dataList[i].indexLetter: groupOffset});
groupOffset += cellHeight + cellHeaderHeight;
} else if (_dataList[i].indexLetter == _dataList[i - 1].indexLetter) {
// 相同的时候只需要加cell的高度
groupOffset += cellHeight;
} else {
//第一个cell一定有头
_groupOffsetMap.addAll({_dataList[i].indexLetter: groupOffset});
groupOffset += cellHeight + cellHeaderHeight;
}
}
print('dc------$_groupOffsetMap');
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: Theme.of(context).colorScheme.inversePrimary,
title: const Text('通讯录'),
),
body: Stack(
children: [
//列表
Column(
children: [
// 搜索框
SearchWidget(
onSearchChange: (text) {
_dataList.clear();
searchStr = text;
if (text.isNotEmpty) {
for (int i = 0; i < _data.length; i++) {
String name = _data[i].name;
if (name.contains(text)) {
_dataList.add(_data[i]);
}
}
} else {
_dataList.addAll(_data);
}
setState(() {});
},
),
Expanded(
child: ListView.builder(
controller: _scrollController,
itemCount: _dataList.length,
itemBuilder: _itemForRow,
),
),
],
),
// 索引条
Positioned(
right: 0.0,
top: screenHeight(context) / 8,
height: screenHeight(context) / 2,
width: indexBarWidth,
child: IndexBarWidget(
indexBarCallBack: (str) {
print('拿到索引条选中的字符:$str');
if (_groupOffsetMap[str] != null) {
_scrollController.animateTo(
_groupOffsetMap[str]!,
duration: const Duration(microseconds: 100),
curve: Curves.easeIn,
);
} else {}
},
),
),
],
),
);
}
Widget? _itemForRow(BuildContext context, int index) {
DataList user = _dataList[index];
//是否显示组名字
bool hiddenTitle = index > 0 &&
_dataList[index].indexLetter == _dataList[index - 1].indexLetter;
return ItemCell(
imageUrl: user.imageUrl,
name: user.name,
groupTitle: hiddenTitle ? null : user.indexLetter,
);
}
}
这是每个cell的内容显示
import 'package:flutter/material.dart';
import 'package:list/list/pages/common.dart';
class ItemCell extends StatelessWidget {
final String? imageUrl;
final String name;
final String? groupTitle;
const ItemCell({
super.key,
this.imageUrl,
required this.name,
this.groupTitle,
});
@override
Widget build(BuildContext context) {
// TextStyle normalStyle = const TextStyle(
// fontSize: 16,
// color: Colors.black,
// );
// TextStyle highlightStyle = const TextStyle(
// fontSize: 16,
// color: Colors.green,
// );
return Column(
children: [
Container(
alignment: Alignment.centerLeft,
padding: const EdgeInsets.only(left: 10.0),
height: groupTitle != null ? cellHeaderHeight : 0.0,
color: Colors.grey,
child: groupTitle != null ? Text(groupTitle!) : null,
),
SizedBox(
height: cellHeight,
child: ListTile(
leading: Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: Colors.red,
image: imageUrl == null
? null
: DecorationImage(
image: NetworkImage(imageUrl!),
fit: BoxFit.cover,
),
borderRadius: const BorderRadius.all(
Radius.circular(4),
),
),
),
title: _title(name),
),
),
],
);
}
Widget _title(String name) {
//这里是让搜索的字体显示高亮状态
// List<TextSpan> spans = [];
// List<String> strs = name.split(searchStr);
// for (int i = 0; i < strs.length; i++) {
// String str = strs[i];
// if (str == ''&&i<strs.length-1) {
// spans.add(TextSpan(text: searchStr, style: highlightStyle));
// } else {
// spans.add(TextSpan(text: str, style: normalStyle));
// if (i < strs.length - 1) {
// spans.add(TextSpan(text: searchStr, style: highlightStyle));
// }
// }
// }
// return RichText(text: TextSpan(children: spans));
return Text(name);
}
}
这是搜索框
import 'package:flutter/material.dart';
import 'package:list/list/pages/common.dart';
class SearchWidget extends StatefulWidget {
final void Function(String) onSearchChange;
const SearchWidget({
super.key,
required this.onSearchChange,
});
@override
State<SearchWidget> createState() => _SearchWidgetState();
}
class _SearchWidgetState extends State<SearchWidget> {
bool _isShowClear = false;
final TextEditingController _textEditingController = TextEditingController();
@override
void dispose() {
_textEditingController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
height: 44,
color: Colors.red,
child: Row(
children: [
Container(
width: screenWidth(context) - 20,
height: 34,
margin: const EdgeInsets.only(left: 10, right: 10.0),
padding: const EdgeInsets.only(left: 10, right: 10.0),
decoration: const BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.all(Radius.circular(6)),
),
child: Row(
children: [
const Icon(Icons.search),
Expanded(
child: TextField(
onChanged: _onChange,
controller: _textEditingController,
decoration: const InputDecoration(
hintText: '请输入搜索内容',
border: InputBorder.none,
contentPadding: EdgeInsets.only(
left: 10,
bottom: 12,
),
),
),
),
if (_isShowClear)
GestureDetector(
onTap: () {
_textEditingController.clear();
setState(() {
_onChange('');
});
},
child: const Icon(Icons.cancel),
),
],
),
),
],
),
);
}
_onChange(String text) {
_isShowClear = text.isNotEmpty;
widget.onSearchChange(text);
}
}
这是右侧的索引条
import 'package:flutter/material.dart';
import 'package:list/list/pages/common.dart';
class IndexBarWidget extends StatefulWidget {
final void Function(String str) indexBarCallBack;
const IndexBarWidget({
super.key,
required this.indexBarCallBack,
});
@override
State<IndexBarWidget> createState() => _IndexBarWidgetState();
}
class _IndexBarWidgetState extends State<IndexBarWidget> {
Color _bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
Color _textColor = Colors.black;
double _indicatorY = 0.0;
String _indicatorStr = 'A';
bool _indicatorShow = false;
@override
void initState() {
super.initState();
}
// 获取选中的字符
int getIndex(BuildContext context, Offset globalPosition) {
// 拿到点前小部件(Container)的盒子
RenderBox renderBox = context.findRenderObject() as RenderBox;
// 拿到y值
double y = renderBox.globalToLocal(globalPosition).dy;
// 算出字符高度
double itemHeight = renderBox.size.height / INDEX_WORDS.length;
// 算出第几个item
// int index = y ~/ itemHeight;
// 为了防止滑出区域后出现问题,所以index应该有个取值范围
int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
return index;
}
@override
Widget build(BuildContext context) {
//索引条
final List<Widget> wordsList = [];
for (var i = 0; i < INDEX_WORDS.length; i++) {
wordsList.add(
Expanded(
child: Text(
INDEX_WORDS[i],
style: TextStyle(
color: _textColor,
fontSize: 14.0,
),
),
),
);
}
return Row(
children: [
Container(
alignment: Alignment(0.0, _indicatorY),
width: indexBarWidth - 20.0,
// color: Colors.red,
child: _indicatorShow
? Stack(
alignment: const Alignment(-0.1, 0),
children: [
//应该放一张图片,没找到合适的,就用Container代替
Container(
width: 60.0,
height: 60.0,
decoration: const BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.all(
Radius.circular(30.0),
),
),
),
Text(
_indicatorStr,
style: const TextStyle(
color: Colors.white,
fontSize: 28.0,
fontWeight: FontWeight.bold,
),
)
],
)
: null,
),
GestureDetector(
onVerticalDragDown: (details) {
int index = getIndex(context, details.globalPosition);
widget.indexBarCallBack(INDEX_WORDS[index]);
setState(() {
_bkColor = const Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
//显示气泡
_indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
_indicatorStr = INDEX_WORDS[index];
_indicatorShow = true;
});
},
onVerticalDragEnd: (details) {
setState(() {
_bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
_textColor = Colors.black;
// 隐藏气泡
_indicatorShow = false;
});
},
onVerticalDragUpdate: (details) {
int index = getIndex(context, details.globalPosition);
widget.indexBarCallBack(INDEX_WORDS[index]);
//显示气泡
setState(() {
_indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
_indicatorStr = INDEX_WORDS[index];
_indicatorShow = true;
});
},
child: Container(
color: _bkColor,
width: 20.0,
child: Column(
children: wordsList,
),
),
),
],
);
}
}
这是定义的宏
//cell头的高度
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
const double cellHeaderHeight = 30.0;
// cell的高度
const double cellHeight = 50.0;
// cell的高度
const double indexBarWidth = 130.0;
double screenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double screenHeight(BuildContext context) => MediaQuery.of(context).size.height;
const INDEX_WORDS = [
'🔍',
'⭐️',
'A',
'B',
'C',
'D',
'E',
'F',
'G',
'H',
'I',
'J',
'K',
'L',
'M',
'N',
'O',
'P',
'Q',
'R',
'S',
'T',
'U',
'V',
'W',
'X',
'Y',
'Z',
];
//从本地加载json数据
Future<String> loadJsonFromAssets(String fileName) async {
return await rootBundle.loadString(fileName);
}