「这是我参与11月更文应战的第16天,活动概况查看:2021终究一次更文应战」

简略的通讯录列表页咱们已经完成了,今天咱们来完成通讯录的索引条;咱们都知道通讯录的索引条是悬浮在列表上方的,在屏幕右侧;那么,咱们的构建方式就要进行修正;

构建索引条

咱们需求运用Stack来构建整个界面,使索引条显现在列表上面:

Flutter(十八)实战-通讯录索引条

显现索引条数据

咱们在索引条上显现一下信息:

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

索引条是竖着显现的,那么很明显,需求运用Column进行布局,咱们先将Columnchildren对应的Widget数组创立好:

final List<Widget> _indexList = []; // 索引条Widget数组
@override
  void initState() {
    super.initState();
    ......
    // 索引条
    for (int i = 0; i < INDEX_WORDS.length; i++) {
      _indexList.add(Text(INDEX_WORDS[i], style: const TextStyle(fontSize: 12),));
    }
  }

这样,咱们就讲所有需求显现的Widget放在_indexList中,只需求给Columnchildren赋值为_indexList即可:

Flutter(十八)实战-通讯录索引条

索引条布局优化

目前,所有的索引字母都是在最上面开端显现的,并且各个字母之间的距离过近,此刻咱们可运用ExpandedText包起来,让他们自适应布局:

// 索引条
for (int i = 0; i < INDEX_WORDS.length; i++) {
  _indexList.add(
      Expanded(
        child: Text(
          INDEX_WORDS[i],
          style: const TextStyle(fontSize: 12),
        ),
      )
  );
}

此刻,从头烘托界面,作用如下:

Flutter(十八)实战-通讯录索引条

咱们改一下索引条的方位如下:

Flutter(十八)实战-通讯录索引条

为了便于操作及扩展,咱们将索引条抽取出来,放在IndexBar中,完好代码如下:

Flutter(十八)实战-通讯录索引条

这样咱们在Stack中直接运用IndexBar即可,也便于后续扩展功用;

Flutter(十八)实战-通讯录索引条

索引条的状况改动

接下来,修正一下索引条在不同情况下的状况变化:默许透明布景,黑色字体,点击或许在上边滑动时布景变黑,字体变白;

咱们界说两个色彩,一个布景色,一个字体色彩:

  Color _bgColor = const Color.fromRGBO(1, 1, 1, 0.0); // 布景色 默许透明
  Color _textColor = Colors.black; // 字体色彩 默许黑色

接下来,在手势触发时,修正两个色彩:

Flutter(十八)实战-通讯录索引条

这个时分,咱们发现在手势触发时,布景色变了,而字体色彩没有发生改动,这是因为咱们字体运用的Text控件的初始化放在了initState办法中,而此办法只有在界面创立时才会调用;setState触发的是build办法,所以咱们应该将创立Text的循环进程放在build中进行,终究代码如下:

Flutter(十八)实战-通讯录索引条

作用如下:

需求注意的是,_indexList的界说也要放在build中,否则每一次烘托,列表都会被烘托一次,而数组没有从头创立,会一直往里面添加元素;

确认鼠标所在的索引方位

现在索引条咱们已经基本完成了,那么咱们假如知道点击的时分,当时是点击的哪一个字母呢?咱们注意到手势的事件是否参数的:

DragDownDetails({
  this.globalPosition = Offset.zero,
  Offset? localPosition,
})

那么这两个参数都是什么意思呢?咱们来打印一下:

Flutter(十八)实战-通讯录索引条

咱们通过打印信息可以确认,这两个都是坐标方位:

  • globalPosition:当时点击点在全局的方位;
  • localPosition:当时点击点在当时部件中的方位;

localPosition也是可以通过globalPosition核算得到的:

Flutter(十八)实战-通讯录索引条

接下来咱们就可以确认,当时点击的是第几个字符了:

Flutter(十八)实战-通讯录索引条

我么你讲此处的操作封装为办法:

String getIndexString(BuildContext context, Offset localPosition) {
  // 点击的坐标点在当时部件中的y坐标
  double y = localPosition.dy;
  // 算出字符的高度
  var itemHeight = screenHeight(context) / 2 / INDEX_WORDS.length;
  // 核算是第几个字符
  int index = y ~/ itemHeight; // ~/ 意为 取整
  // 当时选中的字符
  String str = INDEX_WORDS[index];
  return str;
}

但是此刻是有问题的,假如咱们滑动的区域超出了索引条的区域就会报错:

Flutter(十八)实战-通讯录索引条

所以,咱们的index取值规模是有约束的:

String getIndexString(BuildContext context, Offset localPosition) {
  // 点击的坐标点在当时部件中的y坐标
  double y = localPosition.dy;
  // 算出字符的高度
  var itemHeight = screenHeight(context) / 2 / INDEX_WORDS.length;
  // 核算是第几个字符
  int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1); // ~/ 意为 取整 区域约束在[0, length-1]
  // 当时选中的字符
  String str = INDEX_WORDS[index];
  return str;
}

索引条与联系人列表联动

通常咱们点击索引条的时分,是需求联系人列表翻滚到相应方位的,那么咱们就需求IndexBar对外保留一个接口:

// 点击索引字符的回调;
final void Function(String str)? indexBarCallBack;
IndexBar({this.indexBarCallBack});

然后在IndexBar触发手势事件时,回调该函数:

Flutter(十八)实战-通讯录索引条

这样,咱们就能在运用IndexBar时,在其结构函数中获取点击的字符:

Flutter(十八)实战-通讯录索引条

接下来,假如需求ListView同步翻滚的话,需求运用到ListView中的特点controller,咱们先界说一个_scrollController的特点,然后在initState中初始化:

Flutter(十八)实战-通讯录索引条

然后将_scrollController赋值给ListViewcontroller特点:

Flutter(十八)实战-通讯录索引条

这样的话,假如咱们需求翻滚ListView,那么咱们只需求拿到_scrollController进行翻滚即可;如下:

Flutter(十八)实战-通讯录索引条

点击索引字符之后,让ListView翻滚到260像素的方位,一共动画时长为1秒,Curves.easeIn的意思是动画开端和结束时速度快,中心速度慢;作用如下:

现在咱们翻滚到了一个固定值,那么只需咱们可以算出每一个分组的方位,就能完成点击字符,翻滚到某一个分组的作用;

核算索引分组的方位

咱们假如想要翻滚到分组的方位,那么就需求提早把方位信息核算出来,咱们创立一个Map用来寄存每一个分组与其高度的对应关系:

/*寄存分组的字符和其对应的高度*/
final Map _groupOffsetMap = {
  INDEX_WORDS[0]: 0.0,
  INDEX_WORDS[1]: 0.0,
};

01两个方位不是字母,可以不必翻滚,所以其高度定为0;然后在initState中循环核算每一个分组头的高度:

final double _rowHeight = 51;
final double _groupHeaderHeight = 30;
// 循环核算,将每一个分组头的字符方位核算出来,放入map中
var _groupOffset = _rowHeight * _headerList.length; // 头部四个的高度
for (int i = 0; i < _listDatas.length; i++) {
  if (i < 1) {
    // 第一个一定有头
    _groupOffsetMap.addAll(
        {_listDatas[i].indexLetter: _groupOffset}); // 第一个头部高度为 51 * 4
    // 改动偏移方位
    _groupOffset += _rowHeight + _groupHeaderHeight;
  } else if (_listDatas[i].indexLetter == _listDatas[i - 1].indexLetter) { // 前后两个indexLetter相同的话不必保存;
    // 不存map,只添加偏移
    _groupOffset += _rowHeight;
  } else {
    _groupOffsetMap.addAll({_listDatas[i].indexLetter: _groupOffset});
    // 改动偏移方位
    _groupOffset += _rowHeight + _groupHeaderHeight;
  }
}

在翻滚时,从Map中取出高度,然后翻滚到具体方位:

Flutter(十八)实战-通讯录索引条

运行作用如下:

索引条指示器

假如需求显现索引条的指示器,那么咱们就需求修正索引条的布局:

Flutter(十八)实战-通讯录索引条

咱们在添加上气泡,然后默许显现一个A,在气泡中居中显现:

Flutter(十八)实战-通讯录索引条

其间,文字在气泡中居中显现是运用了Stackalignment特点,用来调整方位;接下来就是操控气泡显现的方位了,警告咱们测验,气泡上下两头方位的显现区域如下:

咱们除了要操控指示器显现的方位,显现的文字还得判别指示器的隐藏和显现状况,咱们界说三个变量:

double _indicatorY = 0.0; // 指示器默许Y值
String _indicatorTitle = 'A'; // 指示器文字 默许A
bool _indicatorHidden = true; // 指示器显现与隐藏 默许隐藏

咱们将之前写的获取选中字符的办法修正一下,回来字符的索引值:

/*获取选中的字符索引*/
int getIndex(BuildContext context, Offset localPosition) {
  // 点击的坐标点在当时部件中的y坐标
  double y = localPosition.dy;
  // 算出字符的高度
  var itemHeight = screenHeight(context) / 2 / INDEX_WORDS.length;
  // 核算是第几个字符
  int index = (y ~/ itemHeight).clamp(0, INDEX_WORDS.length - 1); // ~/ 意为 取整 clamp约束取值规模在[0, length - 1]
  return index;
}

终究指示器代码如下:

Flutter(十八)实战-通讯录索引条

运行作用如下:

Flutter(十八)实战-通讯录索引条

至此,通讯录界面作用已彻底完成;