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~

相关推荐
CrazyQ11 小时前
flutter_easy_refresh在3.38.3配合NestedScrollView的注意要点。
android·flutter·dart
无痕melody2 小时前
苹果ios手机ipad安装配置ish终端shell工具
ios·智能手机·ipad
三七吃山漆2 小时前
攻防世界——fakebook
android·网络安全·web·ctf
二川bro3 小时前
类型错误详解:Python TypeError排查手册
android·java·python
TeleostNaCl3 小时前
在小米 Hyper OS 2 上使用开发者选项关闭视频彩铃功能
android·经验分享
_李小白3 小时前
【Android FrameWork】延伸阅读:Activity生命周期
android
mike10234 小时前
swiftUI状态管理
ios·swiftui
_李小白4 小时前
【Android FrameWork】第二十五天:Service的启动
android
ZePingPingZe5 小时前
DriverManager、DataSource、数据库驱动以及数据库连接池的关系
android·数据库·adb