最终效果

image.png

索引点击回调

经过上篇,我们把简单的布局已经弄好了,但是点击索引条的时候ListView的cell并不会偏移。那么此时我们这里来实现下:点击索引的时候,我们需要把当前点击的值传递给上一层friend_page页面,这里控制着所有的Widget, 那我们就在IndexBar里面新增一个回调

  1. class IndexBar extends StatefulWidget {
  2. final void Function(String str) indexBarCallBack;
  3. IndexBar({required this.indexBarCallBack});
  4. @override
  5. _IndexBarState createState() => _IndexBarState();
  6. }

onVerticalDragDownonVerticalDragUpdate�的时候调用

  1. onVerticalDragUpdate: (DragUpdateDetails details) {
  2. widget.indexBarCallBack(getIndexWord(context, details.globalPosition));
  3. },

同时,在FriendPage这个页面初始化构造IndexBar的时候传递这个indexBarCallBack

索引点击屏幕滚动

在Flutter中把当前的控制器移动一定的偏移量有一个函数

  1. Future<void> animateTo(
  2. double offset, {
  3. required Duration duration,
  4. required Curve curve,
  5. }) async {
  6. assert(_positions.isNotEmpty, 'ScrollController not attached to any scroll views.');
  7. await Future.wait<void>(<Future<void>>[
  8. for (int i = 0; i < _positions.length; i += 1) _positions[i].animateTo(offset, duration: duration, curve: curve),
  9. ]);
  10. }

所以我们需要计算出点击了侧边索引之后,主屏幕需要移动的偏移量,这个Index和对应的偏移量我们可以使用一个Map字典来保存

  1. // 字典,里面用来存放item和高度对应
  2. final Map _groupOffsetMap = {
  3. INDEX_WORDS[0]: 0.0, // 搜索
  4. INDEX_WORDS[1]: 0.0, // 五角星 点击这两个的时候屏幕不用滚动
  5. };

之前在initState函数中计算出了排序之后的_listData,我们可以通过对这个数据源的indexLetter逐个计算,当下一个与上一个的indexLetter相同的时候,这个便宜量是一个_cellHeight的大小,当下一个与上一个的indexLetter不相同的时候,这个偏移量就是_cellheight + _cellGroupTitleHeight

  1. @override
  2. void initState() {
  3. // TODO: implement initState
  4. super.initState();
  5. _viewController = ScrollController();
  6. _listData..addAll(datas)..addAll(datas);
  7. _listData.sort((Friends a, Friends b) {
  8. return a.indexLetter!.compareTo(b.indexLetter!);
  9. });
  10. // 第三个开始
  11. var _groupOffset = _cellHeight * _headerData.length;
  12. for (int i = 0; i < _listData.length; i++) {
  13. if (i < 1) {
  14. _groupOffsetMap.addAll({_listData[i].indexLetter: _groupOffset});
  15. _groupOffset += _cellGroupHeight + _cellHeight;
  16. } else if (_listData[i].indexLetter == _listData[i - 1].indexLetter) {
  17. _groupOffset += _cellHeight;
  18. } else {
  19. _groupOffsetMap.addAll({_listData[i].indexLetter: _groupOffset});
  20. _groupOffset += _cellGroupHeight + _cellHeight;
  21. }
  22. }
  23. }

这样操作了之后,这个_groupOffsetMap里面的每个字母和偏移量就一一对应好了,打印来观察下:
image.png
当然这里的_groupOffsetMap[indexStr]也需要判空一下,毕竟有些首字母可能没有。

  1. IndexBar(indexBarCallBack: (String str) {
  2. print('点击了$str');
  3. print('$_groupOffsetMap');
  4. if (_groupOffsetMap[str] != null) { // 注意啦 这里有个问题
  5. _viewController!.animateTo(_groupOffsetMap[str],
  6. duration: Duration(microseconds: 100),
  7. curve: Curves.easeIn);
  8. } ;
  9. })

当我像上面那样写的时候,一直提示type 'null' is not a subtype of type 'string' of 'function result',最后查了下资料,这个Map初始化的时候默认的是运行时类型,所以此时判断!= null时就会编译报错,解决的办法是直接类型转换new Map<String, double>.from(_groupOffsetMap)
image.png

气泡浮窗

分析:这里点击到索引条的时候会出现一个气泡的图案,这个气泡可以看做是一个Image + Text构成的Stack布局, 文字可通过获取当前的index拿到,这个上篇就已经实现了。图片的话是一个固定的气泡切图,通过动态控制这个Stack的显示和隐藏来达到效果,这里唯一比较难一点的是计算这个偏移量。

我们在Positioned中新增一个Row布局,第一个child是一个Stack,第二个child就是index_bar,先在中心(0,0)固定显示A

  1. Container(
  2. width: 100,
  3. color: Colors.red,
  4. child: Stack(
  5. // alignment: Alignment(-0.2, 0),
  6. children: [
  7. Container(
  8. child: Image.asset('images/气泡.png'),
  9. width: 60,
  10. ),
  11. Container(
  12. child: Text('A',style: TextStyle(fontSize: 35, color: Colors.white)),
  13. )
  14. ],
  15. ),
  16. ),

image.png
位置有点偏左,调整一下alignment,我这里使用的是alignment: Alignment(-0.2, 0)
image.png
修改这个stack在Container中的alignment,移动到底部alignment: Alignment(0, 1.1)
image.png
所以上下的间距是2.2,每一个index的偏移量就是 2.2/索引总个数 * 当前的index -1.1.此时三个控制变量的条件都齐全了

  1. double _indexBarOffsetY = 0.0; // -1.1到1.1之间的偏移量 中心点的是0
  2. bool _indexBarHidden = true; // 是否隐藏
  3. String _indexBarText = 'A'; // 当前正在显示的字母

此时完整的代码是:

  1. import 'package:flutter/material.dart';
  2. import 'const_data.dart';
  3. import 'friend_data.dart';
  4. class IndexBar extends StatefulWidget {
  5. final void Function(String str) indexBarCallBack;
  6. IndexBar({required this.indexBarCallBack});
  7. @override
  8. _IndexBarState createState() => _IndexBarState();
  9. }
  10. class _IndexBarState extends State<IndexBar> {
  11. Color _backColor = Color.fromRGBO(1, 1, 1, 0);
  12. Color _textColor = Colors.grey;
  13. double _indexBarOffsetY = 0.0; // -1.1到1.1之间的偏移量 中心点的是0
  14. bool _indexBarHidden = true; // 是否隐藏
  15. String _indexBarText = 'A'; // 当前正在显示的字母
  16. @override
  17. void initState() {
  18. // TODO: implement initState
  19. super.initState();
  20. }
  21. @override
  22. Widget build(BuildContext context) {
  23. final List<Widget> _widgetData = [];
  24. for (int i = 0; i < INDEX_WORDS.length; i++) {
  25. _widgetData.add(Expanded(
  26. child: Text(INDEX_WORDS[i],
  27. style: TextStyle(fontSize: 10, color: _textColor))));
  28. }
  29. return Positioned(
  30. right: 0,
  31. top: screenHeight(context) / 8,
  32. height: screenHeight(context) / 2,
  33. width: 120,
  34. child: Row(
  35. children: [
  36. Container(
  37. alignment: Alignment(0, -_indexBarOffsetY), // 最上面是-1.1 最下面是1.1
  38. width: 100,
  39. child: _indexBarHidden
  40. ? null
  41. : Stack(
  42. alignment: Alignment(-0.2, 0),
  43. children: [
  44. Container(
  45. child: Image.asset('images/气泡.png'),
  46. width: 60,
  47. ),
  48. Container(
  49. child: Text(
  50. _indexBarText,
  51. style: TextStyle(fontSize: 35, color: Colors.white),
  52. ),
  53. )
  54. ],
  55. ),
  56. ),
  57. Container(
  58. child: GestureDetector(
  59. onVerticalDragDown: (DragDownDetails details) {
  60. int index = getIndexWord(context, details.globalPosition);
  61. widget.indexBarCallBack(INDEX_WORDS[index]);
  62. setState(() {
  63. _backColor = Color.fromRGBO(1, 1, 1, 0.5);
  64. _textColor = Colors.white;
  65. _indexBarHidden = false;
  66. _indexBarOffsetY = 2.2 / INDEX_WORDS.length * index - 1.1;
  67. _indexBarText = INDEX_WORDS[index];
  68. });
  69. },
  70. onVerticalDragEnd: (DragEndDetails details) {
  71. setState(() {
  72. _backColor = Color.fromRGBO(1, 1, 1, 0);
  73. _textColor = Colors.grey;
  74. _indexBarHidden = true;
  75. });
  76. },
  77. onVerticalDragUpdate: (DragUpdateDetails details) {
  78. int index = getIndexWord(context, details.globalPosition);
  79. widget.indexBarCallBack(INDEX_WORDS[index]);
  80. setState(() {
  81. _indexBarHidden = false;
  82. _indexBarOffsetY = 2.2 / INDEX_WORDS.length * index - 1.1;
  83. _indexBarText = INDEX_WORDS[index];
  84. });
  85. },
  86. child: Container(
  87. width: 20,
  88. child: Column(children: _widgetData),
  89. color: _backColor,
  90. ),
  91. ),
  92. )
  93. ],
  94. ),
  95. );
  96. }
  97. }
  98. int getIndexWord(BuildContext context, Offset globalPosition) {
  99. RenderBox box = context.findRenderObject() as RenderBox;
  100. Offset y = box.globalToLocal(globalPosition);
  101. var itemHeight = screenHeight(context) / 2 / INDEX_WORDS.length;
  102. int index = (y.dy ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1);
  103. return index;
  104. }

好了,到这里索引指示器就正式告一段落了。