Flutter - 实现通讯录字母索引滑动的核心功能

591 阅读4分钟

Flutter前文配图.webp

前文

在开发通讯录应用的过程中,使用者有时需要使用字母索引来快速找到相应的名称。在此篇文章中,我将讲解如何在 Flutter 中使用 GestureDetector 实现通讯录的字母索引功能,体验类似微信通讯录的滑动和精确定位。本文将简要说明 Flutter 页面布局结构,但更侧重于实现滑动、计算高度等核心内容。

效果展示

滑动通列表, 字母索引联动选中对应字母; 拖拽字母索引有气泡提示并且通讯录跳到对应的字母Title

sr07j-cnbz5.gif

页面布局

首先,我们建立了一个通讯录列表页面。 Flutter 的页面结构如下:

  • Stack
    • CustomScrollView (通讯录列表)
    • Align(centerRight)
      • ChatLetterIndexBar (字母指导部件)

字母指导部件 ChatLetterIndexBar 也是一个 Stack,内容包括一个可滑动的字母列表和一个用于带有透明效果的字母带等模拟器部件。ChatLetterIndexBar 通过构造方法传入字母集合和选中回调函数,实现与通讯录页面的数据通信。

通讯录列表页面与字母列表部件之间的通信我使用了 GetX 框架。当通讯录中字母被选中时,通过触发回调方法来更改当前 obs 的字母指导值。

注意哟:不是只能用GetX才能实现, 使用Provider, Redux等都可以

项目实现

在实现通讯录数据的滑动和字母跳转时,为了提高性能,需要对通讯录中的各个项进行高度计算和记录。通过计算每个字母或用户项的高度,并记录字母的起始位置,可以便于后续滑动到相应的位置。

例如,下面一段 Dart 代码用于计算字母项的高度,并依此记录位置:

// 记录字母头的偏移量,key是字母,value是偏移量
final Map<String, double> recordLetterStartPositions = {}; // 存储每个字母项的起始位置偏移量
final Map<String, double> recordLetterEndPositions = {}; // 存储每个字母项的结束位置偏移量

void calcAndRecordItemHeight(List<ChatContactItem> list, double newFriendLayoutHeight) {
  recordLetterStartPositions.clear();
  recordLetterEndPositions.clear();
  if (list.isEmpty) return;
  double positionOffset = contactListRectTop + newFriendLayoutHeight;
  String? letter;
  for (int i = 0; i < list.length; i++) {
    final ChatContactItem item = list[i];
    // 如果当前字母不为空且遇到一个新的字母项,记录上一个字母项的结束位置
    if (letter != null && item is ChatContactLetterItem && item.letter != letter) {
      recordLetterEndPositions[letter] = positionOffset;
    }
    // 如果是字母项,记录其起始位置
    if (item is ChatContactLetterItem) {
      letter = item.letter;
      recordLetterStartPositions[item.letter] = positionOffset;
      positionOffset += letterHeaderItemHeight;
    } else if (item is ChatContactUserItem) { // 如果是用户项,增加偏移量
      positionOffset += contactItemHeight;
    }
  }
}

对于通讯录的滑动事件,我们也使用了 scrollController 来跟踪滑动位置,为了防止断续的滚动激活与字母的相关测量:

// 防止滚动时频繁触发遍历
scrollController.addListener(_onScrollListener = () {
  if (!scrollController.hasClients) return;
  if (scrollController.position.maxScrollExtent == 0) return;
  double currentOffset = scrollController.offset;
  // 如果当前滚动位置还未超过下一节点的位置,则不需要更新
  if (nextNodePosition != null &&
      currentOffset < nextNodePosition! &&
      previousNodePosition != null &&
      currentOffset >= previousNodePosition!) {
    return;
  }
  double? firstLetterStartPosition = recordLetterStartPositions[letterList.first];
  if (firstLetterStartPosition == null) return;
  if (currentOffset < firstLetterStartPosition) return;
  final positionsKeys = recordLetterStartPositions.keys.toList();
  for (int i = 0; i < positionsKeys.length; i++) {
    String key = positionsKeys[i];
    double value = recordLetterStartPositions[key] ?? 0;
    // 更新选中的字母项
    if (currentOffset >= value && currentOffset < recordLetterEndPositions[key]!) {
      chatLetterIndexBarController?.selectedLetter.value = key;
      nextNodePosition =
          i + 1 < positionsKeys.length ? recordLetterStartPositions[positionsKeys[i + 1]] : double.infinity;
      previousNodePosition = recordLetterStartPositions[positionsKeys[i]] ?? double.negativeInfinity;
      return;
    }
  }
});

在实现字母索引滑动选中时,我们使用了 GestureDetector 来捕捉滑动手势事件,从而实现字母的选中:

GestureDetector(
 key: controller._overlayKey,
 onVerticalDragUpdate: (details) => ..., // 垂直拖动
 onVerticalDragEnd: (details) => ..., // 拖动结束
 child: ...,
)

这里的 GestureDetector 捕捉垂直拖动事件,通过 _updateLetter 方法更新当前选中的字母。

下面是 _updateLetter 方法的实现:

void _updateLetter(ChatLetterIndexBarController controller, Offset position) {
  double heightPerLetter =
      controller._overlayKey.size.height / controller.indexWordsList.length; // 计算每个字母的高度
  int index = position.dy ~/ heightPerLetter;
  // 根据垂直位置计算当前选中的字母索引,并更新 controller
  if (index > 0 && index < controller.indexWordsList.length) {
    controller.selectedIndex.value = index;
  } else {
    controller.selectedIndex.value = -1;
  }
}

controller.selectedIndex 发生变化时,使用 GetX 框架会主动触发字母选中回调方法,通知通讯录页面进行相应的滚动和更新。

而当按下字母索引的时候,我们通过跳转方法:

void jumpTo((int, String?) index) {
  if (index.$1 < 0) return;
  if (index.$1 > -1 && scrollController.hasClients && scrollController.position.maxScrollExtent > 0) {
    // 查找滚动视图的 RenderBox 确保跳转操作是在有效的渲染对象上执行
    final RenderBox? scrollRenderBox =
        scrollController.position.context.storageContext.findRenderObject() as RenderBox?;
    if (scrollRenderBox != null) {
      double? targetPosition = recordLetterStartPositions[index.$2];
      if (targetPosition != null) {
        if (targetPosition < scrollController.position.maxScrollExtent) {
          scrollController.jumpTo(targetPosition);
        } else {
          scrollController.jumpTo(scrollController.position.maxScrollExtent);
        }
      }
    }
  }
}

通过这样的实现,使用者在通讯录和字母索引间的交互也更加顺畅,体验到了滑动自然而且可靠的 Flutter 索引效果。

总结

本文通过对 Flutter 中字母索引功能的实现进行详细讲解,旨在为同样面临通讯录索引实现问题的开发者提供一个参考思路。无论是通过 GestureDetector 捕捉手势,还是通过高度计算和控制滑动跳转,本篇文章展示了如何将这些核心技术结合起来以实现类似微信的滑动体验。

希望这些内容能帮助开发者更快地实现自己的需求,并根据自己的项目特点选择适合的实现方式。无论你是使用 GetX、Provider,还是其他状态管理工具,这些实现思路都可以为你提供灵感,使你能够更高效地实现通讯录的高效滑动和索引功能。

撰写不易,请给个赞👍吧