实现效果

image.png

1 定义通讯录数据结构类Friends

  1. class Friends {
  2. final String imageUrl;// 通讯录头像的url
  3. final String name; //通讯录中显示的名字
  4. final String indexLetter; //在列表中的索引字母
  5. Friends({this.imageUrl, this.name, this.indexLetter});
  6. }
  7. List<Friends> datas = [
  8. Friends(
  9. imageUrl: 'https://randomuser.me/api/portraits/women/27.jpg',
  10. name: 'Lina',
  11. indexLetter: 'L'),
  12. Friends(
  13. imageUrl: 'https://randomuser.me/api/portraits/women/17.jpg',
  14. name: '菲儿',
  15. indexLetter: 'F'),
  16. Friends(
  17. imageUrl: 'https://randomuser.me/api/portraits/women/16.jpg',
  18. name: '安莉',
  19. indexLetter: 'A'),
  20. Friends(
  21. imageUrl: 'https://randomuser.me/api/portraits/men/31.jpg',
  22. name: '阿贵',
  23. indexLetter: 'A'),
  24. Friends(
  25. imageUrl: 'https://randomuser.me/api/portraits/women/22.jpg',
  26. name: '贝拉',
  27. indexLetter: 'B'),
  28. Friends(
  29. imageUrl: 'https://randomuser.me/api/portraits/women/37.jpg',
  30. name: 'Lina',
  31. indexLetter: 'L'),
  32. Friends(
  33. imageUrl: 'https://randomuser.me/api/portraits/women/18.jpg',
  34. name: 'Nancy',
  35. indexLetter: 'N'),
  36. Friends(
  37. imageUrl: 'https://randomuser.me/api/portraits/men/47.jpg',
  38. name: '扣扣',
  39. indexLetter: 'K'),
  40. Friends(
  41. imageUrl: 'https://randomuser.me/api/portraits/men/3.jpg',
  42. name: 'Jack',
  43. indexLetter: 'J'),
  44. Friends(
  45. imageUrl: 'https://randomuser.me/api/portraits/women/5.jpg',
  46. name: 'Emma',
  47. indexLetter: 'E'),
  48. Friends(
  49. imageUrl: 'https://randomuser.me/api/portraits/women/24.jpg',
  50. name: 'Abby',
  51. indexLetter: 'A'),
  52. Friends(
  53. imageUrl: 'https://randomuser.me/api/portraits/men/15.jpg',
  54. name: 'Betty',
  55. indexLetter: 'B'),
  56. Friends(
  57. imageUrl: 'https://randomuser.me/api/portraits/men/13.jpg',
  58. name: 'Tony',
  59. indexLetter: 'T'),
  60. Friends(
  61. imageUrl: 'https://randomuser.me/api/portraits/men/26.jpg',
  62. name: 'Jerry',
  63. indexLetter: 'J'),
  64. Friends(
  65. imageUrl: 'https://randomuser.me/api/portraits/men/36.jpg',
  66. name: 'Colin',
  67. indexLetter: 'C'),
  68. Friends(
  69. imageUrl: 'https://randomuser.me/api/portraits/women/12.jpg',
  70. name: 'Haha',
  71. indexLetter: 'H'),
  72. Friends(
  73. imageUrl: 'https://randomuser.me/api/portraits/women/11.jpg',
  74. name: 'Ketty',
  75. indexLetter: 'K'),
  76. Friends(
  77. imageUrl: 'https://randomuser.me/api/portraits/women/13.jpg',
  78. name: 'Lina',
  79. indexLetter: 'L'),
  80. Friends(
  81. imageUrl: 'https://randomuser.me/api/portraits/women/23.jpg',
  82. name: 'Lina',
  83. indexLetter: 'L'),
  84. ];

2 定义索引条组件IndexBar(索引列表+选择索引项的气泡)

  1. import 'package:flutter/material.dart';
  2. final Color WeChatThemeColor = Color.fromRGBO(220, 220, 220, 1);
  3. //屏幕宽/高
  4. double ScreenWidth(BuildContext context) => MediaQuery.of(context).size.width;
  5. double ScreenHeight(BuildContext context) => MediaQuery.of(context).size.height;
  6. class IndexBar extends StatefulWidget {
  7. final void Function(String str) indexBarCallBack;
  8. IndexBar({this.indexBarCallBack});
  9. @override
  10. _IndexBarState createState() => _IndexBarState();
  11. }
  12. int getIndex(BuildContext context, Offset globalPosition) {
  13. //拿到box
  14. RenderBox box = context.findRenderObject();
  15. //拿到y值
  16. double y = box.globalToLocal(globalPosition).dy;
  17. //算出字符高度
  18. var itemHeight = ScreenHeight(context) / 2 / INDEX_WORDS.length;
  19. //算出第几个item,并且给一个取值范围
  20. int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
  21. // print('现在选中的是${INDEX_WORDS[index]}');
  22. return index;
  23. }
  24. class _IndexBarState extends State<IndexBar> {
  25. Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0); //索引条的背景色
  26. Color _textColor = Colors.black; //索引条中文字的颜色
  27. double _indicatorY = 0.0; //选择索引的气泡显示位置
  28. String _indicatorText = 'A'; //选择索引的文字
  29. bool _indocatorHidden = true; //气泡是否显示
  30. @override
  31. Widget build(BuildContext context) {
  32. //索引条列表项
  33. List<Widget> words = [];
  34. for (int i = 0; i < INDEX_WORDS.length; i++) {
  35. words.add(Expanded(
  36. child: Text(
  37. INDEX_WORDS[i],
  38. style: TextStyle(fontSize: 10, color: _textColor),
  39. ),
  40. ));
  41. }
  42. return Positioned(
  43. right: 0.0,
  44. height: ScreenHeight(context) / 2,
  45. top: ScreenHeight(context) / 8,
  46. width: 120,
  47. child: Row(
  48. children: <Widget>[
  49. Container(
  50. alignment: Alignment(0, _indicatorY),
  51. width: 100,
  52. child: _indocatorHidden
  53. ? null
  54. : Stack(
  55. alignment: Alignment(-0.2, 0),
  56. children: <Widget>[
  57. Image(
  58. image: AssetImage('images/气泡.png'),
  59. width: 60,
  60. ),
  61. Text(
  62. _indicatorText,
  63. style: TextStyle(
  64. fontSize: 25,
  65. color: Colors.white,
  66. ),
  67. ),
  68. ],
  69. ), //气泡
  70. ),
  71. GestureDetector(
  72. child: Container(
  73. width: 20,
  74. color: _bkColor,
  75. child: Column(
  76. children: words,
  77. ),
  78. ),
  79. onVerticalDragUpdate: (DragUpdateDetails details) {
  80. print("onVerticalDragUpdate");
  81. int index = getIndex(context, details.globalPosition);
  82. setState(() {
  83. _indicatorText = INDEX_WORDS[index];
  84. //根据我们索引条的Alignment的Y值进行运算的.从-1.1 到 1.1
  85. //整个的Y包含的值是2.2
  86. _indicatorY = 2.2 / 28 * index - 1.1;
  87. _indocatorHidden = false;
  88. });
  89. widget.indexBarCallBack(INDEX_WORDS[index]);
  90. },
  91. onVerticalDragDown: (DragDownDetails details) {
  92. int index = getIndex(context, details.globalPosition);
  93. print("onVerticalDragDown:${INDEX_WORDS[index]}");
  94. _indicatorText = INDEX_WORDS[index];
  95. _indicatorY = 2.2 / 28 * index - 1.1;
  96. _indocatorHidden = false;
  97. widget.indexBarCallBack(INDEX_WORDS[index]);
  98. // print('现在点击的位置是${details.globalPosition}');
  99. setState(() {
  100. _bkColor = Color.fromRGBO(1, 1, 1, 0.5);
  101. _textColor = Colors.white;
  102. });
  103. },
  104. onVerticalDragEnd: (DragEndDetails details) {
  105. setState(() {
  106. _indocatorHidden = true;
  107. _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
  108. _textColor = Colors.black;
  109. });
  110. },
  111. ), //这个是索引条!
  112. ],
  113. ));
  114. }
  115. }
  116. const INDEX_WORDS = [
  117. '🔍',
  118. '☆',
  119. 'A',
  120. 'B',
  121. 'C',
  122. 'D',
  123. 'E',
  124. 'F',
  125. 'G',
  126. 'H',
  127. 'I',
  128. 'J',
  129. 'K',
  130. 'L',
  131. 'M',
  132. 'N',
  133. 'O',
  134. 'P',
  135. 'Q',
  136. 'R',
  137. 'S',
  138. 'T',
  139. 'U',
  140. 'V',
  141. 'W',
  142. 'X',
  143. 'Y',
  144. 'Z'
  145. ];

3 通讯录列表+索引检索显示

  1. import 'package:flutter/material.dart';
  2. import '../../const.dart';
  3. import '../discover/discover_child_page.dart';
  4. import 'friends_data.dart';
  5. import 'index_bar.dart';
  6. class FriendsPage extends StatefulWidget {
  7. @override
  8. _FriendsPageState createState() => _FriendsPageState();
  9. }
  10. class _FriendsPageState extends State<FriendsPage> {
  11. //字典里面放item和高度的对应数据
  12. final Map _groupOffsetMap = {
  13. INDEX_WORDS[0]: 0.0,
  14. INDEX_WORDS[1]: 0.0,
  15. };
  16. ScrollController _scrollController;
  17. final List<Friends> _listDatas = [];
  18. @override
  19. void initState() {
  20. super.initState();
  21. // _listDatas.addAll(datas);
  22. // _listDatas.addAll(datas);
  23. //多弄一点数据!
  24. _listDatas..addAll(datas)..addAll(datas);
  25. //排序!
  26. _listDatas.sort((Friends a, Friends b) {
  27. return a.indexLetter.compareTo(b.indexLetter);
  28. });
  29. //计算并记录 分组头在列表的偏移量 供索引条点击时滚动通讯列表用
  30. var _groupOffset = 54.5 * 4;
  31. //经过循环计算,将每一个头的位置算出来.放入字典!
  32. for (int i = 0; i < _listDatas.length; i++) {
  33. if (i < 1) {
  34. //第一个Cell
  35. _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
  36. //保存完了再加_groupOffset偏移
  37. _groupOffset += 84.5;
  38. } else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) {
  39. //此时没有头部,只需要加偏移量就好了!
  40. _groupOffset += 54.5;
  41. } else {
  42. //这部分就是有头部的Cell了!
  43. _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
  44. _groupOffset += 84.5;
  45. }
  46. }
  47. _scrollController = ScrollController();
  48. }
  49. final List<Friends> _headerData = [
  50. Friends(imageUrl: 'images/新的朋友.png', name: '新的朋友'),
  51. Friends(imageUrl: 'images/群聊.png', name: '群聊'),
  52. Friends(imageUrl: 'images/标签.png', name: '标签'),
  53. Friends(imageUrl: 'images/公众号.png', name: '公众号'),
  54. ];
  55. //每个通讯录行
  56. Widget _itemForRow(BuildContext context, int index) {
  57. //系统图标的Cell
  58. if (index < _headerData.length) {
  59. return _FriendCell(
  60. imageAssets: _headerData[index].imageUrl,
  61. name: _headerData[index].name,
  62. );
  63. }
  64. //显示剩下的Cell
  65. //如果当前和上一个Cell的IndexLetter一样,就不显示!
  66. bool _hideIndexLetter = (index - 4 > 0 &&
  67. _listDatas[index - 4].indexLetter == _listDatas[index - 5].indexLetter);
  68. return _FriendCell(
  69. imageUrl: _listDatas[index - 4].imageUrl,
  70. name: _listDatas[index - 4].name,
  71. groupTitle: _hideIndexLetter ? null : _listDatas[index - 4].indexLetter,
  72. );
  73. }
  74. @override
  75. Widget build(BuildContext context) {
  76. return Scaffold(
  77. appBar: AppBar(
  78. backgroundColor: WeChatThemeColor,
  79. title: Text('通讯录'),
  80. actions: <Widget>[
  81. GestureDetector(
  82. child: Container(
  83. margin: EdgeInsets.only(right: 10),
  84. child: Image(
  85. image: AssetImage('images/icon_friends_add.png'),
  86. width: 25,
  87. ),
  88. ),
  89. onTap: () {
  90. // Navigator.of(context).push(MaterialPageRoute(
  91. // builder: (BuildContext context) => DiscoverChildPage(
  92. // title: '添加朋友',
  93. // )));
  94. },
  95. )
  96. ],
  97. ),
  98. body: Stack(
  99. children: <Widget>[
  100. Container(
  101. color: WeChatThemeColor,
  102. child: ListView.builder(
  103. controller: _scrollController,
  104. itemCount: _listDatas.length + _headerData.length,
  105. itemBuilder: _itemForRow,
  106. )), //列表
  107. IndexBar(
  108. indexBarCallBack: (String str) {
  109. print("map:$_groupOffsetMap\n");
  110. print("scroll: ${_groupOffsetMap[str]}");
  111. if (_groupOffsetMap[str] != null) {
  112. _scrollController.animateTo(_groupOffsetMap[str],
  113. duration: Duration(milliseconds: 1), curve: Curves.easeIn);
  114. }
  115. },
  116. ), //悬浮检索控件
  117. ],
  118. ),
  119. );
  120. }
  121. }
  122. class _FriendCell extends StatelessWidget {
  123. final String imageUrl;
  124. final String name;
  125. final String groupTitle;
  126. final String imageAssets;
  127. const _FriendCell(
  128. {this.imageUrl, this.name, this.groupTitle, this.imageAssets});
  129. @override
  130. Widget build(BuildContext context) {
  131. return Column(
  132. children: <Widget>[
  133. Container(
  134. alignment: Alignment.centerLeft,
  135. padding: EdgeInsets.only(left: 10),
  136. height: groupTitle != null ? 30 : 0,
  137. color: Color.fromRGBO(1, 1, 1, 0.0),
  138. child: groupTitle != null
  139. ? Text(
  140. groupTitle,
  141. style: TextStyle(color: Colors.grey),
  142. )
  143. : null,
  144. ), //cell的头
  145. Container(
  146. color: Colors.white,
  147. child: Row(
  148. children: <Widget>[
  149. Container(
  150. margin: EdgeInsets.all(10),
  151. width: 34,
  152. height: 34,
  153. decoration: BoxDecoration(
  154. borderRadius: BorderRadius.circular(6.0),
  155. image: DecorationImage(
  156. image: imageUrl != null
  157. ? NetworkImage(imageUrl)
  158. : AssetImage(imageAssets),
  159. )),
  160. ), //图片
  161. Container(
  162. child: Text(
  163. name,
  164. style: TextStyle(fontSize: 17),
  165. ),
  166. ), //昵称
  167. ],
  168. ),
  169. ), //Cell内容
  170. Container(
  171. height: 0.5,
  172. color: WeChatThemeColor,
  173. child: Row(
  174. children: <Widget>[
  175. Container(
  176. width: 50,
  177. color: Colors.white,
  178. )
  179. ],
  180. ),
  181. ) //分割线
  182. ],
  183. );
  184. }
  185. }