发布于2026-06-30 阅读(0)
扫一扫,手机访问
最近项目里需要实现一个多级分类选择器,类似淘宝那种“电子产品 > 手机 > iPhone”的横向展开效果。找了一圈没看到现成的轮子,索性自己动手做一个。

大概就是动图里展示的样子:点击一级项会展开下一级,顶部有面包屑导航可以回退,还支持搜索功能。
说实话,一开始想用纵向折叠的方案,但产品觉得层级多了以后界面太乱,用户也不清楚自己选到了第几层。后来调研了几款竞品,发现横向展开加面包屑的设计确实清晰很多——用户一眼就能看到当前路径,操作起来心里有数。
整个组件的核心是维护两个状态:
_breadcrumbs — 面包屑路径,记录用户点击过的父级节点_currentOptions — 当前显示的选项列表每次点击一个有子项的选项,就把这个选项加入面包屑,同时将它的 children 设为当前选项列表;点击面包屑则反向操作,截取数组即可。
/// 导航到子项
void _na vigateToChildren(T parent) {
setState(() {
_breadcrumbs.add(parent);
_currentOptions = parent.children?.map((child) => child as T).toList() ?? [];
});
}
/// 导航到面包屑指定位置
void _na vigateToBreadcrumb(int index) {
setState(() {
_breadcrumbs = _breadcrumbs.sublist(0, index + 1);
_currentOptions = _breadcrumbs[index].children?.map((child) => child as T).toList() ?? [];
});
}
最麻烦的反而是回显——用户之前选过的值需要重新展示。这就要求在整个树里递归查找,找到目标后把完整路径构建出来:
/// 递归查找项目路径 bool _findItemPath(Listoptions, T target, List path) { for (var option in options) { path.add(option); if (option.value == target.value) { return true; } if (option.children != null && option.children!.isNotEmpty) { List children = option.children!.map((child) => child as T).toList(); if (_findItemPath(children, target, path)) { return true; } } path.removeLast(); // 回溯 } return false; }
这个回溯算法挺经典的:找到了就一路返回 true,没找到就把刚加的节点删掉,继续尝试下一个。
搜索是后来加的,需求很明确:只搜索叶子节点(最终可选的项),然后显示完整路径,让用户知道这个选项在哪个分类下。
void _searchInTree(Listoptions, String query, List results, List 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 newPath = List.from(path)..add(option); List children = option.children!.map((child) => child as T).toList(); _searchInTree(children, query, results, newPath); } } }
为了通用性,定义了一个抽象类,业务方继承一下就能用:
abstract class OptionChildrenItem {
String? name;
String? value;
List? children;
OptionChildrenItem({this.name, this.value, this.children});
}
同时还提供了一个默认实现 OptionChildrenItemImpl,不想自定义的话可以直接用。
调用起来很简单,一行代码就能弹出来:
final result = await showHorizontalTreePopup(
context: context,
options: _demoOptions,
title: '请选择分类',
themeColor: Colors.blue,
showSearch: true, // 是否显示搜索框
showConfirmButton: true, // 是否显示确定按钮;false 则选中后直接返回
onConfirm: (selectedItem) {
print('选中了: ${selectedItem?.name}');
},
);
底部安全区 — iPhone 的小横条要留出来,用 MediaQuery.of(context).padding.bottom 处理。
泛型支持 — 整个组件是泛型的,T extends OptionChildrenItem,这样业务方自己的数据模型也能直接使用。
两种交互模式 — showConfirmButton 为 true 时需要点击确定才返回,为 false 时点击叶子节点直接返回,适应不同场景。
整体下来代码量不大,700 多行。核心逻辑就是维护面包屑和当前列表两个状态。最折腾的地方是泛型转换——Dart 的泛型协变有点坑,children 那里需要手动 map 转一下类型。
有需要的可以直接用,pub.dev 搜 horizontal_tree_selector 就行:
dependencies: horizontal_tree_selector: ^0.0.1
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
售后无忧
立即购买>office旗舰店
正版软件
正版软件
正版软件
正版软件
正版软件
1
2
3
7
8