网上的第三方包,用的最多就是:azListView (opens new window)。
但是它有 bug:当列表到达页面底部的时候,滑动最后几个字母,还会触发回弹到屏幕顶部。如下:
经过分析,一开始以为是 ListView 的 physics 属性影响,physics (opens new window)介绍:
- ClampingScrollPhysics:Android下微光效果。
- BouncingScrollPhysics:iOS下弹性效果。
- NeverScrollableScrollPhysics:禁止滚动。
但是设置该属性其实没有效果,还是会出现上面的问题。
后面多次测试 ListView,发现是 ListView 跳转事件 jumpTo 的影响,后面改用 animateTo,animateTo 的用法如下:
_scrollController.animateTo( offsetHeight, duration: Duration(milliseconds: 10), curve: Curves.linear, )
但是又有新的问题:
- 当连续快速滑动的时候,这个 duration 相当于多了个防抖的作用。
- 点击字母列表的时候,也会有问题。
效果如下:
azListView 存在问题:
- azListView 包体较大;
- 存在的问题修改源码耗时较久。
自己实现,效果如下:
# 布局
整个页面用 Stack 布局,分为:
- 主列表(头部 + 城市列表)
- 主列表顶部字母提示
- 字母列表(字母列表 + 当前字母提示)
代码如下:
/// _buildBody
Widget _buildBody() {
return Stack(
children: [
_buildMainListView(),
_buildSusBarView(),
Positioned(
top: duSetHeight(120),
right: duSetWidth(10),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [_buildAlphabetListView()],
),
),
],
);
}
# GestureDetector
GestureDetector 文档链接 (opens new window)
- onVerticalDragUpdate:滑动更新
- onVerticalDragEnd:滑动结束
- ...
# 主要思路
# 1.初始化的时候,先计算获取以下数值。
- 计算主列表每个字母分组需要跳转的 offsetTopList 和每个字母分组所占用的高度 groupHeight;
【计算:遍历字母列表,字母的 offsetTop = 之前的所有字母所有的 offsetTop 累加;当前字母的 groupHeight = 每个主列表项 itemHeight * 每个字母分组数量。】
/// 计算主列表每个字母需要滚动的距离和每个字母项所占用的高度
void _calcEachLetterOffsetTop() {
List<String> _alphabetList = widget.alphabetList;
/// offset top
_alphabetListOffsetTopMap[_alphabetList[0]] = 0; // '#' header offsetTop
_alphabetListOffsetTopMap[_alphabetList[1]] =
widget.headerHeight; // 第一个字母,滚动距离为顶部 header 高度 offsettop
/// height
_alphabetListHeightMap[_alphabetList[0]] =
widget.headerHeight; // '#' header height
_alphabetListHeightMap[_alphabetList[1]] =
widget.alphabetCountMap[_alphabetList[1]] * widget.mainListItemHeight +
widget.guideBarHeight; // 第一个字母所占用的高度
// 第二项开始循环
for (int i = 2; i < _alphabetList.length; i++) {
String _curLetter = _alphabetList[i]; // 当前字母
// 保存当前字母项的高度
_alphabetListHeightMap[_curLetter] =
widget.alphabetCountMap[_curLetter] * widget.mainListItemHeight +
widget.guideBarHeight;
// 计算主列表每一个字母项需要滚动的 offsetTop(之前的所有字母所有的 offsetTop 累加)
double _offsetTopSum = 0.0;
for (int j = i - 1; j > 0; j--) {
String _preLetter = _alphabetList[j]; // 之前的字母
// 将之前所有字母的高度累加
// 主列表每个字母单元高度 = 子项数量 * 子项高度 + 主列表导航条高度
_offsetTopSum +=
widget.alphabetCountMap[_preLetter] * widget.mainListItemHeight +
widget.guideBarHeight;
}
_alphabetListOffsetTopMap[_curLetter] =
_offsetTopSum + widget.headerHeight; // 设置当前字母的滚动 offsetTop
}
}
- 获取主列表不需要滚动的字母列表。
【计算:从字母表后面遍历,将每个字母分组所占用的高度 groupHeight 进行累加,与屏幕的高度进行比较,如果累加结果小于屏幕高度,则对该字母进行标记。】
主要代码如下:
/// 获取主列表不需要滚动的字母列表
void _getNotScrollAlphebetList() {
final _screenHeight =
duSetHeight(MediaQuery.of(context).size.height); // 屏幕高度
// final _screenHeight = duSetHeight(size.height);
double _heightSum = 0.0;
// 判断那些字母不需要触发列表滚动,从后面字母开始循环累加,判断是否小于屏幕高度
for (int i = widget.alphabetList.length - 1; i >= 0; i--) {
String _curLetter = widget.alphabetList[i];
bool _ifLessThanScreenHeight =
_heightSum + _alphabetListHeightMap[_curLetter] <= _screenHeight;
if (_ifLessThanScreenHeight) {
_heightSum += _alphabetListHeightMap[_curLetter]; // 累加
_notScrollAlphabetLst.insert(0, _curLetter); // 添加到列表
} else {
break;
}
}
}
# 2. 滑动主列表,获取当前的字母,更新右侧字母表状态和更新顶部字母导航条。
- 获取主列表当前滑动位置的 screenOffsetTop;
- 遍历 offsetTopList,判断 screenOffsetTop 在哪两个字母的区间内,则可以定位到目标字母。
主要代码如下:
/// 更新字母表当前激活的字母
void _updateAlphebetListView() {
// 主列表上锁中(滚动执行中),直接返回
if (_mainListScrollLock == true) return;
List<String> _alphabetList = widget.alphabetList;
// 监听主列表滚动,判断主列表的 offsetTop 在哪两个字母的 offsetTop 区间内,将右侧字母导航列表激活为当前字母
for (int i = 0; i < widget.alphabetList.length; i++) {
// 不为最后一个字母 + 主列表 offsetTop 大于当前字母的 offsetTop + 主列表的 offsetTop 小于下一个字母的 offsetTop
bool _ifInCurRange = i != widget.alphabetList.length - 1 &&
_scrollController.offset <=
_alphabetListOffsetTopMap[_alphabetList[i + 1]] &&
_scrollController.offset >
_alphabetListOffsetTopMap[_alphabetList[i]];
if (_ifInCurRange) {
_selectedAlphabetIndex = i; // 设置字母表选中字母索引
_mainListScrollLock = false; // 解锁
setState(() {});
return;
}
}
}
# 3. 滑动字母表,获取当前的字母,主列表跳转到目标字母和更新顶部字母导航条。
- 获取字母表当前位置相对于字母表容器顶部的高度 paddingOffsetTop【获取字母表当前位置对于视窗的高度 - 字母表容器顶部的高度相对于视窗的高度】;
- 计算每个字母项占用字母表容器的高度 alphabetItemHeight 【(字母表容器的高度 - padding) / 字母数量】;
- 获取当前索引,paddingOffsetTop / alphabetItemHeight;
- 获取当前字母和主列表 offsetTopList 滚动高度,
- 更新视图。
主要代码如下:
/// 滑动字母表:滚动主列表 + 更新字母列表
void _onVerticalDragUpdateHandle(DragUpdateDetails event, int index) {
/// 更新字母表
if (_alphabetListOffset == null) {
// 获取字母表的 margin top
RenderBox _box = _alphabetListKey?.currentContext?.findRenderObject();
_alphabetListOffset = _box.localToGlobal(Offset.zero);
}
// 字母表的padding
double _alphabetPadding = widget.alphabetListItemHeight;
// 计算当前字母项索引:(当前活动的 dy - 字母表的 marginTop - 字母表的 padding) / 每个字母所占的高度
int _curActiveLetterIndex =
((event.globalPosition.dy - _alphabetListOffset.dy - _alphabetPadding) /
widget.alphabetListItemHeight)
.round();
if (_curActiveLetterIndex < 0) {
// 滑出字母表区域上面,保持为 0
_selectedAlphabetIndex = 0;
} else if (_curActiveLetterIndex >= widget.alphabetList.length) {
// 滑出字母表区域下面,保持为最后一个
_selectedAlphabetIndex = widget.alphabetList.length - 1;
} else {
_selectedAlphabetIndex = _curActiveLetterIndex;
}
setState(() {});
/// 滚动主列表
String _curLetter = widget.alphabetList[_selectedAlphabetIndex]; // 获取当前字母项
_scrollMainList(_curLetter);
}
# 4.当主列表已经滑动到底部,继续滑动字母表剩下的字母,判断当前字母时候标记为不需要滚动主列表,直接滑动列表底部,不滑动到字母分组位置。
主要代码如下:
/// 滚动主列表 void _scrollMainList(String letter) { if (_notScrollAlphabetLst.contains(letter)) { // 当前字母不需要滚动 _scrollController .jumpTo(_scrollController.position.maxScrollExtent); // 滚动到列表底部 return; } _scrollController.jumpTo( _alphabetListOffsetTopMap[letter], ); }
# 5. 主列表滚动锁。
为防止出现上次滚动还没结束,下次滚动就开始执行的问题,给主列表滚动上锁。
/// 更新字母表当前激活的字母 void _updateAlphebetListView() { // 主列表上锁中(滚动执行中),直接返回 if (_mainListScrollLock) return; // ... }