实现效果
1 定义通讯录数据结构类Friends
class Friends {
final String imageUrl;// 通讯录头像的url
final String name; //通讯录中显示的名字
final String indexLetter; //在列表中的索引字母
Friends({this.imageUrl, this.name, this.indexLetter});
}
List<Friends> datas = [
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/27.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/17.jpg',
name: '菲儿',
indexLetter: 'F'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/16.jpg',
name: '安莉',
indexLetter: 'A'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/31.jpg',
name: '阿贵',
indexLetter: 'A'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/22.jpg',
name: '贝拉',
indexLetter: 'B'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/37.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/18.jpg',
name: 'Nancy',
indexLetter: 'N'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/47.jpg',
name: '扣扣',
indexLetter: 'K'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/3.jpg',
name: 'Jack',
indexLetter: 'J'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/5.jpg',
name: 'Emma',
indexLetter: 'E'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/24.jpg',
name: 'Abby',
indexLetter: 'A'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/15.jpg',
name: 'Betty',
indexLetter: 'B'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/13.jpg',
name: 'Tony',
indexLetter: 'T'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/26.jpg',
name: 'Jerry',
indexLetter: 'J'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/men/36.jpg',
name: 'Colin',
indexLetter: 'C'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/12.jpg',
name: 'Haha',
indexLetter: 'H'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/11.jpg',
name: 'Ketty',
indexLetter: 'K'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/13.jpg',
name: 'Lina',
indexLetter: 'L'),
Friends(
imageUrl: 'https://randomuser.me/api/portraits/women/23.jpg',
name: 'Lina',
indexLetter: 'L'),
];
2 定义索引条组件IndexBar(索引列表+选择索引项的气泡)
import 'package:flutter/material.dart';
final Color WeChatThemeColor = Color.fromRGBO(220, 220, 220, 1);
//屏幕宽/高
double ScreenWidth(BuildContext context) => MediaQuery.of(context).size.width;
double ScreenHeight(BuildContext context) => MediaQuery.of(context).size.height;
class IndexBar extends StatefulWidget {
final void Function(String str) indexBarCallBack;
IndexBar({this.indexBarCallBack});
@override
_IndexBarState createState() => _IndexBarState();
}
int getIndex(BuildContext context, Offset globalPosition) {
//拿到box
RenderBox box = context.findRenderObject();
//拿到y值
double y = box.globalToLocal(globalPosition).dy;
//算出字符高度
var itemHeight = ScreenHeight(context) / 2 / INDEX_WORDS.length;
//算出第几个item,并且给一个取值范围
int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
// print('现在选中的是${INDEX_WORDS[index]}');
return index;
}
class _IndexBarState extends State<IndexBar> {
Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0); //索引条的背景色
Color _textColor = Colors.black; //索引条中文字的颜色
double _indicatorY = 0.0; //选择索引的气泡显示位置
String _indicatorText = 'A'; //选择索引的文字
bool _indocatorHidden = true; //气泡是否显示
@override
Widget build(BuildContext context) {
//索引条列表项
List<Widget> words = [];
for (int i = 0; i < INDEX_WORDS.length; i++) {
words.add(Expanded(
child: Text(
INDEX_WORDS[i],
style: TextStyle(fontSize: 10, color: _textColor),
),
));
}
return Positioned(
right: 0.0,
height: ScreenHeight(context) / 2,
top: ScreenHeight(context) / 8,
width: 120,
child: Row(
children: <Widget>[
Container(
alignment: Alignment(0, _indicatorY),
width: 100,
child: _indocatorHidden
? null
: Stack(
alignment: Alignment(-0.2, 0),
children: <Widget>[
Image(
image: AssetImage('images/气泡.png'),
width: 60,
),
Text(
_indicatorText,
style: TextStyle(
fontSize: 25,
color: Colors.white,
),
),
],
), //气泡
),
GestureDetector(
child: Container(
width: 20,
color: _bkColor,
child: Column(
children: words,
),
),
onVerticalDragUpdate: (DragUpdateDetails details) {
print("onVerticalDragUpdate");
int index = getIndex(context, details.globalPosition);
setState(() {
_indicatorText = INDEX_WORDS[index];
//根据我们索引条的Alignment的Y值进行运算的.从-1.1 到 1.1
//整个的Y包含的值是2.2
_indicatorY = 2.2 / 28 * index - 1.1;
_indocatorHidden = false;
});
widget.indexBarCallBack(INDEX_WORDS[index]);
},
onVerticalDragDown: (DragDownDetails details) {
int index = getIndex(context, details.globalPosition);
print("onVerticalDragDown:${INDEX_WORDS[index]}");
_indicatorText = INDEX_WORDS[index];
_indicatorY = 2.2 / 28 * index - 1.1;
_indocatorHidden = false;
widget.indexBarCallBack(INDEX_WORDS[index]);
// print('现在点击的位置是${details.globalPosition}');
setState(() {
_bkColor = Color.fromRGBO(1, 1, 1, 0.5);
_textColor = Colors.white;
});
},
onVerticalDragEnd: (DragEndDetails details) {
setState(() {
_indocatorHidden = true;
_bkColor = Color.fromRGBO(1, 1, 1, 0.0);
_textColor = Colors.black;
});
},
), //这个是索引条!
],
));
}
}
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'
];
3 通讯录列表+索引检索显示
import 'package:flutter/material.dart';
import '../../const.dart';
import '../discover/discover_child_page.dart';
import 'friends_data.dart';
import 'index_bar.dart';
class FriendsPage extends StatefulWidget {
@override
_FriendsPageState createState() => _FriendsPageState();
}
class _FriendsPageState extends State<FriendsPage> {
//字典里面放item和高度的对应数据
final Map _groupOffsetMap = {
INDEX_WORDS[0]: 0.0,
INDEX_WORDS[1]: 0.0,
};
ScrollController _scrollController;
final List<Friends> _listDatas = [];
@override
void initState() {
super.initState();
// _listDatas.addAll(datas);
// _listDatas.addAll(datas);
//多弄一点数据!
_listDatas..addAll(datas)..addAll(datas);
//排序!
_listDatas.sort((Friends a, Friends b) {
return a.indexLetter.compareTo(b.indexLetter);
});
//计算并记录 分组头在列表的偏移量 供索引条点击时滚动通讯列表用
var _groupOffset = 54.5 * 4;
//经过循环计算,将每一个头的位置算出来.放入字典!
for (int i = 0; i < _listDatas.length; i++) {
if (i < 1) {
//第一个Cell
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
//保存完了再加_groupOffset偏移
_groupOffset += 84.5;
} else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
//此时没有头部,只需要加偏移量就好了!
_groupOffset += 54.5;
} else {
//这部分就是有头部的Cell了!
_groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
_groupOffset += 84.5;
}
}
_scrollController = ScrollController();
}
final List<Friends> _headerData = [
Friends(imageUrl: 'images/新的朋友.png', name: '新的朋友'),
Friends(imageUrl: 'images/群聊.png', name: '群聊'),
Friends(imageUrl: 'images/标签.png', name: '标签'),
Friends(imageUrl: 'images/公众号.png', name: '公众号'),
];
//每个通讯录行
Widget _itemForRow(BuildContext context, int index) {
//系统图标的Cell
if (index < _headerData.length) {
return _FriendCell(
imageAssets: _headerData[index].imageUrl,
name: _headerData[index].name,
);
}
//显示剩下的Cell
//如果当前和上一个Cell的IndexLetter一样,就不显示!
bool _hideIndexLetter = (index - 4 > 0 &&
_listDatas[index - 4].indexLetter == _listDatas[index - 5].indexLetter);
return _FriendCell(
imageUrl: _listDatas[index - 4].imageUrl,
name: _listDatas[index - 4].name,
groupTitle: _hideIndexLetter ? null : _listDatas[index - 4].indexLetter,
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
backgroundColor: WeChatThemeColor,
title: Text('通讯录'),
actions: <Widget>[
GestureDetector(
child: Container(
margin: EdgeInsets.only(right: 10),
child: Image(
image: AssetImage('images/icon_friends_add.png'),
width: 25,
),
),
onTap: () {
// Navigator.of(context).push(MaterialPageRoute(
// builder: (BuildContext context) => DiscoverChildPage(
// title: '添加朋友',
// )));
},
)
],
),
body: Stack(
children: <Widget>[
Container(
color: WeChatThemeColor,
child: ListView.builder(
controller: _scrollController,
itemCount: _listDatas.length + _headerData.length,
itemBuilder: _itemForRow,
)), //列表
IndexBar(
indexBarCallBack: (String str) {
print("map:$_groupOffsetMap\n");
print("scroll: ${_groupOffsetMap[str]}");
if (_groupOffsetMap[str] != null) {
_scrollController.animateTo(_groupOffsetMap[str],
duration: Duration(milliseconds: 1), curve: Curves.easeIn);
}
},
), //悬浮检索控件
],
),
);
}
}
class _FriendCell extends StatelessWidget {
final String imageUrl;
final String name;
final String groupTitle;
final String imageAssets;
const _FriendCell(
{this.imageUrl, this.name, this.groupTitle, this.imageAssets});
@override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.only(left: 10),
height: groupTitle != null ? 30 : 0,
color: Color.fromRGBO(1, 1, 1, 0.0),
child: groupTitle != null
? Text(
groupTitle,
style: TextStyle(color: Colors.grey),
)
: null,
), //cell的头
Container(
color: Colors.white,
child: Row(
children: <Widget>[
Container(
margin: EdgeInsets.all(10),
width: 34,
height: 34,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
image: DecorationImage(
image: imageUrl != null
? NetworkImage(imageUrl)
: AssetImage(imageAssets),
)),
), //图片
Container(
child: Text(
name,
style: TextStyle(fontSize: 17),
),
), //昵称
],
),
), //Cell内容
Container(
height: 0.5,
color: WeChatThemeColor,
child: Row(
children: <Widget>[
Container(
width: 50,
color: Colors.white,
)
],
),
) //分割线
],
);
}
}