具体实现效果:
使用的插件:
//获取汉字拼音
lpinyin: ^2.0.3
主要注意点:
1、在从接口获取好友列表后,需要保存每个好友名字的拼音首字母
单独写了一个对象用来保存每个好友名字的拼音首字母:
class FriendVo {
V2TimFriendInfo info;
//名字拼音首字母
String pin;
FriendVo({
this.info,
this.pin,
});
}
从接口获取数据源之后的处理
List _friendList = [];
//_resultList是接口返回的列表 _friendList为最终的好友列表数据源
_resultList.forEach((element) {
String now = '#';
String nowPin = PinyinHelper.getFirstWordPinyin(_getShowName(element));
if (nowPin.length > 0) {
now = indexList.contains(nowPin.substring(0, 1).toUpperCase())
? nowPin.substring(0, 1)
: '#';
}
FriendVo vo = FriendVo(info: element,pin: now.toUpperCase());
_friendList.add(vo);
});
//处理好的数据源按照首字母进行重新排序
_friendList.sort((FriendVo a, FriendVo b) {
if (a.pin == '#'){
return 1;
}
if (b.pin == '#'){
return 0;
}
return a.pin.compareTo(b.pin);
});
默认创建多个GlobalKey对象,用来获取每个item距离顶部的距离
_keyList = List.generate(_friendList.length, (index) => GlobalKey());
UI数据源处理
List itemList = [];
//顶部菜单
if (_menuList.length > 0){
itemList.addAll(_menuList.map((e){
int index = _menuList.indexOf(e);
return MenuItem(
menuVo: e,
isHiddenLine: index == _menuList.length - 1,
onSelect: (int value){
if (value == 1){
Navigator.push(context, MaterialPageRoute(builder: (context){
return GroupListPage();
}));
}
},
);
}).toList()
);
}
itemList.addAll(_friendList.map((e) {
int index = _friendList.indexOf(e);
//此处判断是都显示字母分组,判断逻辑为,每个item只需跟上一个item的首字母对比,如果相同就不显示,否则显示
FriendVo vo = e;
bool isShow = true;
if (nextVo != null){
if (vo.pin == nextVo.pin) {
isShow = false;
}
}
nextVo = vo;
return FriendItem(
key: _keyList[index],
vo: vo,
isShowPin: isShow,
isHiddenLine: index == _friendList.length - 1,
);
}).toList());
2、由于需要做到左边好友列表跟右边字母列表联动滚动,所以需要获取item的位置。整个页面刷新完成即可通过key获取每个item距离顶部的距离,然后保存
//保存的当前数据源包含的字母(不重复)
List _filterList = [];
//保存每个显示字母的item距离顶部的距离
List _locList = [];
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
_subInitState();
});
_subInitState() {
FriendVo _nextVo;
for (int i = 0; i < _friendList.length; i++) {
FriendVo vo = _friendList[i];
bool isShow = true;
String now = vo.pin;
if (_nextVo != null){
String back = _nextVo.pin;
if (back == now) {
isShow = false;
}
}
_nextVo = vo;
if (isShow) {
var globalKey = _keyList[i];
var offsetY = getY(globalKey.currentContext)-_top;
print('now:${now}');
print('offsetY:${offsetY}');
if (!_filterList.contains(vo.pin)){
_filterList.add(vo.pin);
_locList.add(offsetY);
}
}
}
_indexKey.currentState.updateIndex(_filterList);
}
3、选中字母列表某个item
onVerticalDragUpdate: (DragUpdateDetails details) {
print('onVerticalDragUpdate');
print('globalPosition:${details.globalPosition}');
_selectIndex = getIdex(context, details.globalPosition);
double _bubbleY = details.globalPosition.dy;
bool _bubbleHidden = false;
if (widget.onSelect != null) {
widget.onSelect(
_itemList[_selectIndex < 0
? 0
: (_selectIndex > (_itemList.length - 1)
? _itemList.length - 1
: _selectIndex)],
_bubbleY,
_bubbleHidden);
}
setState(() {});
},
//按住屏幕移动手指实时更新触摸的位置坐标
onVerticalDragDown: (DragDownDetails details) {
print('onVerticalDragDown');
_selectIndex = getIdex(context, details.globalPosition);
double _bubbleY = details.globalPosition.dy;
bool _bubbleHidden = false;
if (widget.onSelect != null) {
widget.onSelect(
_itemList[_selectIndex < 0
? 0
: (_selectIndex > (_itemList.length - 1)
? _itemList.length - 1
: _selectIndex)],
_bubbleY,
_bubbleHidden);
}
print('现在点击的位置是${details.globalPosition}');
setState(() {});
},
//触摸开始
onVerticalDragEnd: (DragEndDetails details) {
print('onVerticalDragEnd');
double _bubbleY = 0;
bool _bubbleHidden = true;
if (widget.onSelect != null) {
widget.onSelect(
_itemList[_selectIndex < 0
? 0
: (_selectIndex > (_itemList.length - 1)
? _itemList.length - 1
: _selectIndex)],
_bubbleY,
_bubbleHidden);
} //触摸结束
setState(() {});
},
onTapDown: (TapDownDetails details) {
print('onTapDown');
},
onTapUp: (TapUpDetails details) {
print('onTapUp');
double _bubbleY = 0;
bool _bubbleHidden = true;
if (widget.onSelect != null) {
widget.onSelect(
_itemList[_selectIndex < 0
? 0
: (_selectIndex > (_itemList.length - 1)
? _itemList.length - 1
: _selectIndex)],
_bubbleY,
_bubbleHidden);
} //触摸结束
setState(() {});
},
4、选中字母列表某个item的回调:
int index = _filterList.indexWhere((element) => element == value);
if (index > -1){
double max = _scrollController.position.maxScrollExtent;
double offset = _locList[index];
if (offset > max){
_scrollController.jumpTo(max);
}else{
_scrollController.jumpTo(offset);
}
}
5、整体页面结构
@override
Widget build(BuildContext context) {
double height = MediaQuery.of(context).size.height;
return Stack(
children: [
Positioned(
left: 0,
right: 0,
top: 0,
bottom: 0,
child: SingleChildScrollView(
controller: _scrollController,
child: Column(
children: _getItemList(),
),
)
),
Positioned(
top: height/8,
height: height/2,
right: 0,
child: IndexItem(
key: _indexKey,
onSelect: (String value,double offset,bool type) {
if (type == false){
int index = _filterList.indexWhere((element) => element == value);
if (index > -1){
double max = _scrollController.position.maxScrollExtent;
double offset = _locList[index];
if (offset > max){
_scrollController.jumpTo(max);
}else{
_scrollController.jumpTo(offset);
}
}
}
//悬浮气泡
_bubbleKey.currentState.updateLoc(value,offset,type);
},
)),
Bubble(
key: _bubbleKey,
)
],
);
}
注意:此处一定要用SingleChildScrollView,因为ListView会重新创建、布局、绘制可见区域内的item,一般会多绘制可见区域以外2-4个item,所以不在屏幕的item通过key获取的位置是0。