Flutter横向树形选择器实现方案

撸了个横向树形选择器,分享一下踩坑记录

最近项目里需要做一个多级分类选择,类似淘宝那种"电子产品 > 手机 > iPhone"的效果。找了一圈没看到合适的轮子,索性自己撸一个。

先看效果

大概就是这个样子,点击一级会展开下一级,顶部有面包屑导航可以回退,支持搜索。

为什么要横向?

说实话一开始我是想用纵向折叠的方案的,但产品觉得层级多了以后太丑了,而且用户不知道自己选到第几层。后来看了一些竞品,发现横向展开+面包屑这个方案确实清晰很多,用户一眼就知道自己选了什么。

核心思路

其实整个组件的核心就是维护两个状态:

  • _breadcrumbs - 面包屑路径,记录用户点了哪些父级
  • _currentOptions - 当前显示的选项列表

每次点击一个有子项的选项,就把它加到面包屑里,然后把它的 children 设成当前选项列表。点面包屑就反向操作,截取数组就行。

dart 复制代码
/// 导航到子项
void _navigateToChildren(T parent) {
  setState(() {
    _breadcrumbs.add(parent);
    _currentOptions = parent.children?.map((child) => child as T).toList() ?? [];
  });
}

/// 导航到面包屑指定位置
void _navigateToBreadcrumb(int index) {
  setState(() {
    _breadcrumbs = _breadcrumbs.sublist(0, index + 1);
    _currentOptions = _breadcrumbs[index].children?.map((child) => child as T).toList() ?? [];
  });
}

回显这个坑

最麻烦的其实是回显,就是用户之前选过的值要显示出来。这就需要在整棵树里递归查找,找到后把完整路径构建出来:

dart 复制代码
/// 递归查找项目路径
bool _findItemPath(List<T> options, T target, List<T> path) {
  for (var option in options) {
    path.add(option);

    if (option.value == target.value) {
      return true;
    }

    if (option.children != null && option.children!.isNotEmpty) {
      List<T> children = option.children!.map((child) => child as T).toList();
      if (_findItemPath(children, target, path)) {
        return true;
      }
    }

    path.removeLast();  // 回溯
  }
  return false;
}

这个回溯算法挺经典的,找到了就一路返回 true,没找到就把刚加的节点删掉继续找下一个。

搜索功能

搜索功能也是后来加的,需求是只搜叶子节点(最终可选的项),然后显示完整路径让用户知道这个选项在哪。

dart 复制代码
void _searchInTree(List<T> options, String query, List<T> results, List<T> path) {
  for (var option in options) {
    final bool isLeaf = option.children == null || option.children!.isEmpty;
    final bool nameMatches = option.name?.toLowerCase().contains(query) == true;
    
    if (isLeaf && nameMatches) {
      results.add(option);
    }

    // 递归搜索子项
    if (option.children != null && option.children!.isNotEmpty) {
      List<T> newPath = List.from(path)..add(option);
      List<T> children = option.children!.map((child) => child as T).toList();
      _searchInTree(children, query, results, newPath);
    }
  }
}

数据结构设计

为了通用性,我定义了一个抽象类,业务方继承一下就能用:

dart 复制代码
abstract class OptionChildrenItem {
  String? name;
  String? value;
  List<OptionChildrenItem>? children;

  OptionChildrenItem({this.name, this.value, this.children});
}

也提供了一个默认实现 OptionChildrenItemImpl,不想自定义可以直接用。

使用方式

用起来很简单,调一个方法就行:

dart 复制代码
final result = await showHorizontalTreePopup(
  context: context,
  options: _demoOptions,
  title: '请选择分类',
  themeColor: Colors.blue,
  showSearch: true,  // 要不要搜索框
  showConfirmButton: true,  // 要不要确定按钮,false 的话选中直接返回
  onConfirm: (selectedItem) {
    print('选中了: ${selectedItem?.name}');
  },
);

一些细节处理

  1. 底部安全区 - iPhone 那个小横条要留出来,用 MediaQuery.of(context).padding.bottom

  2. 泛型支持 - 整个组件是泛型的,T extends OptionChildrenItem,这样业务方自己的数据模型也能用

  3. 两种模式 - showConfirmButton 为 true 时需要点确定才返回,为 false 时点击叶子节点直接返回,适合不同场景

总结

整体下来代码量不大,700 多行,核心逻辑就是维护面包屑和当前列表这两个状态。最坑的地方是泛型转换,Dart 的泛型协变有点坑,children 那里需要手动 map 转一下类型。

有需要的可以直接用,pub.dev 搜 horizontal_tree_selector 就行:

yaml 复制代码
dependencies:
  horizontal_tree_selector: ^0.0.1

代码写得比较糙,有问题欢迎提 issue~

相关推荐
A-花开堪折1 小时前
RK3568 Android 11 驱动开发(四):添加产品配置和内核设备树选择
android·驱动开发
TheNextByte11 小时前
如何将照片从Android传输到闪存驱动器
android
JMchen1231 小时前
Android Activity管理工具类
android·java·学习·移动开发·android-studio
June bug1 小时前
【配环境】iOS项目开发环境
ios
shix .1 小时前
spiderdemo-T8字体反扒
android
前端不太难2 小时前
Flutter / RN / iOS 的状态策略,该如何取舍?
flutter·ios·状态模式
青小莫2 小时前
C++之模板
android·java·c++
装不满的克莱因瓶2 小时前
Android Studio 的模拟器如何上传本地图片到手机相册
android·智能手机·android studio
三金121383 小时前
深入解析MySQL EXPLAIN
android
_李小白3 小时前
【Android 美颜相机】第十一天:GPUImageFilter解析
android·数码相机