实现效果
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, ) ], ), ) //分割线 ], ); }}