Flutter —— 索引条

605 阅读4分钟

这是我参与11月更文挑战的第15天,活动详情查看:2021最后一次更文挑战

索引条

这里功能太多所以重新创建一个文件来写索引条。 索引条是叠在Listview上面的,那么朋友圈页面里面的body就应该使用stack。然后添加索引条。

 body: Stack(
       children: [
         Container(
           child: ListView.builder(
             itemBuilder: _itemForRow,
             itemCount: datas.length + _headerData.length,
           ),
           color: weChatThemColor,
         ), //列表
       IndexBar(), // 索引条
       ],
     ),

添加索引条数据

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' ];

创建一个保存Widget的list

  final List<Widget> words = [];

在initState中添加Widget

  for (int i = 0; i < INDEX_WORDS.length;i++) {
      words.add(
        Expanded(child: Text(INDEX_WORDS[i],style: TextStyle(fontSize: 10,color: Colors.grey),),)
      );
    }

然后在build里面大致写一下索引条的界面,这样索引条就能显示出来了。

 Positioned(
      right: 0.0,
      top: screenHeight(context)/8,
      height: screenHeight(context)/2,
      width: 30,
      child: Column(
        children: words,
      ),
    );

索引条还需要捕捉到选中的是哪一个,所以先定义两个属性。

  Color _bkColor = Color.fromRGBO(1, 1, 1, 0.0);
  Color _textColor =  Colors.black;

然后将Column用GestureDetector包住。

在这里插入图片描述

将之前在initState中创建的text的文字颜色改为_textColor。将Column用Container包住之后颜色改为_bkColor。

在这里插入图片描述

在这里插入图片描述

在GestureDetector监听onVerticalDragDown以及onVerticalDragEnd,然后setState改变背景色和文字颜色。

    onVerticalDragDown: (DragDownDetails details){
          setState(() {
            _bkColor =  const Color.fromRGBO(1, 1, 1, 0.5);
            _textColor = Colors.white;
          });
        },
  onVerticalDragEnd: (DragEndDetails details){
           setState(() {
             _bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
             _textColor = Colors.black;
           });
         },

这时候发现背景色变化来,但是文字颜色没有改变,这是因为文字颜色在initState里面。所以与界面相关的就不应该放在数据相关的地方,否则就容易出现bug,所以这里将words和循环放到build里面。

在这里插入图片描述

然后在GestureDetector监听onVerticalDragUpdate,使用context.findRenderObject找到最近的一个部件,然后用globalToLocal算出当前点击的地方距离部件的原点的y值,然后算出字符高度,算出是第几个item,然后得到点击的字母。

   onVerticalDragUpdate: (DragUpdateDetails details){
            //拿到当前小部件
            RenderBox box = context.findRenderObject() as RenderBox;
            //拿到y值,globalToLocal当前位置距离部件的原点的距离
            double y = box.globalToLocal(details.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]}');

          },

将里面的步骤封装成一个方法

String getIndex (BuildContext context,Offset globalPosition) {
  //拿到当前小部件
  RenderBox box = context.findRenderObject() as RenderBox;
  //拿到y值,globalToLocal当前位置距离部件的原点的距离
  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);
 return INDEX_WORDS[index];
}

onVerticalDragUpdate里面调用

 onVerticalDragUpdate: (DragUpdateDetails details) {
            getIndex(context, details.globalPosition);
          },

这里需要把数据返回到通讯录页面,那么这里写一个回调。

  final void Function(String str)? indexBarCallBack;
  IndexBar({this.indexBarCallBack});

在onVerticalDragUpdate里面调用indexBarCallBack。

 onVerticalDragUpdate: (DragUpdateDetails details) {
            if (widget.indexBarCallBack != null) {
              widget.indexBarCallBack!(
                  getIndex(context, details.globalPosition));
            }
          },

在外面使用IndexBar就耀传入这个indexBarCallBack

IndexBar(indexBarCallBack: (String str){
      
        }),

接下来要根据点击的字母进行滚动,那么就需要计算每个字母的滚动距离。 这里需要先声明一个ScrollController

   ScrollController _scrollController =  ScrollController();

将其设为_scrollController的controller。

 controller:  _scrollController,

之后声明map,_cellHeight以及_groupHeight

  final Map _groupOffsetMap = {};
    final double _cellHeight = 54.5;
    double _groupHeight = 30;

然后在initState里面计算滚动距离。

  var _groupOffset  = _cellHeight * _headerData.length;
    //通过循环计算,将每一个头的位置算出来,放入字典。
    for (int i = 0; i < _listDatas.length; i ++) {
      if (i < 1){//第一个cell一定有头
        _groupOffsetMap.addAll({_listDatas[i].indexLetter:_groupOffset});
        //保存完了在加_groupOffset
        _groupOffset += _cellHeight + _groupHeight;
      } else if (_listDatas[i].indexLetter == _listDatas[i-1].indexLetter ) {
        _groupOffset += _cellHeight;
      } else {
        _groupOffsetMap.addAll({_listDatas[i].indexLetter:_groupOffset});
        _groupOffset += _cellHeight + _groupHeight;
      }
    }

然后就可以在IndexBar回调中滚动了。

 IndexBar(indexBarCallBack: (String str){
          if (_groupOffsetMap[str] != null) {
            _scrollController.animateTo(
                _groupOffsetMap[str], duration: Duration(microseconds: 100),
                curve: Curves.easeIn);
          }
        }),

接下来要做左边的气泡,那么这里先将Indexbar的索引条包起来。

在这里插入图片描述

然后在Container里面添加图片以及文字。

 Container(
            alignment: Alignment(0,-1.1),
            width: 100,
            color: Colors.red,
            child: Stack(
              alignment:   Alignment(-0.2, 0),
              children:  [
                Image(
                  image: AssetImage('images/气泡.png'),
                  width: 60,
                ),
                Text(
                  'A',
                  style: TextStyle(
                    fontSize: 35,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
          ),

接下来需要根据点击的位置变换气泡文字,以及在释放时隐藏气泡,所以这里声明三个属性。

  double _indicatorY = 0.0;
  String _indicatorText = "A";
  bool _indicatorHidden = true;

修改getIndex成只返回index,然后在onVerticalDragDown和onVerticalDragUpdate里面修改_indicatorY和_indicatorText,并且把_indicatorHidden设为false,然后在onVerticalDragEnd里面把_indicatorHidden设为true。

onVerticalDragDown: (DragDownDetails details) {
                int index =  getIndex(context, details.globalPosition);

                if (widget.indexBarCallBack != null) {
                  widget.indexBarCallBack!(
                      INDEX_WORDS[index]);

                }
                
                setState(() {
                  _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                  _indicatorText = INDEX_WORDS[index];
                  _indicatorHidden = false;
                  _bkColor = const Color.fromRGBO(1, 1, 1, 0.5);
                  _textColor = Colors.white;
                });
              },
              onVerticalDragUpdate: (DragUpdateDetails details) {
                int index =  getIndex(context, details.globalPosition);

                if (widget.indexBarCallBack != null) {
                  widget.indexBarCallBack!(
                      INDEX_WORDS[index]);
                }
                setState(() {
                  _indicatorY = 2.2 / INDEX_WORDS.length * index - 1.1;
                  _indicatorText = INDEX_WORDS[index];
                  _indicatorHidden = false;
                });

              },
              onVerticalDragEnd: (DragEndDetails details) {
                setState(() {
                  _bkColor = const Color.fromRGBO(1, 1, 1, 0.0);
                  _textColor = Colors.black;
                  _indicatorHidden = true;
                });
              },

然后修改气泡这边,如果_indicatorHidden为true就返回null,否则就返回气泡,气泡的位置跟着_indicatorY变化而变化。

Container(
            alignment: Alignment(0,_indicatorY),
            width: 100,
            color: Colors.red,
            child: _indicatorHidden ? null : Stack(
              alignment:   Alignment(-0.2, 0),
              children:  [
                Image(
                  image: AssetImage('images/气泡.png'),
                  width: 60,
                ),
                Text(
                  _indicatorText,
                  style: TextStyle(
                    fontSize: 35,
                    color: Colors.white,
                  ),
                ),
              ],
            ),
          ),