Flutter:实现ListView.builder()中子项Text组件首字符的竖线连接(含次级子项的横竖线衔接)
Flutter:实现ListView.builder()中子项Text组件首字符的竖线连接(含次级子项的横竖线衔接)
嘿,这个需求我之前做树形结构UI时刚好踩过坑!其实核心就是用自定义布局结合容器组件绘制线条,再根据子项的层级和位置来控制线条的显示逻辑。下面给你一套完整的实现方案,直接就能用:
第一步:定义数据模型
首先我们需要一个能表示层级、是否为最后一项的Item模型,方便后续控制线条:
class LineItem { final String text; final int level; // 层级:0是顶级,1是次级,以此类推 final bool isLast; // 是否为同级中的最后一个项 final List<LineItem>? children; // 次级子项 LineItem({ required this.text, required this.level, required this.isLast, this.children, }); // 复制方法,用于更新isLast标记 LineItem copyWith({bool? isLast}) { return LineItem( text: text, level: level, isLast: isLast ?? this.isLast, children: children, ); } }
第二步:构建示例数据源
我们先准备一个带层级的测试数据:
final List<LineItem> rawItems = [ LineItem( text: "主菜单1", level: 0, isLast: false, children: [ LineItem(text: "子菜单1-1", level: 1, isLast: false), LineItem(text: "子菜单1-2", level: 1, isLast: true, children: [ LineItem(text: "孙菜单1-2-1", level: 2, isLast: true), ], ), ], ), LineItem(text: "主菜单2", level: 0, isLast: true), ];
第三步:自定义带线条的Item组件
这是核心部分,用Stack叠加横竖线条,对齐Text的首字符位置:
class LineConnectedItem extends StatelessWidget { final LineItem item; final double indentWidth = 24; // 每级缩进的宽度 final double lineWidth = 2; // 线条粗细 final Color lineColor = Colors.grey.shade400; // 线条颜色 final double textCenterOffset = 12; // 对应Text首字符的垂直居中偏移量 const LineConnectedItem({super.key, required this.item}); @override Widget build(BuildContext context) { return Row( crossAxisAlignment: CrossAxisAlignment.start, children: [ // 左侧线条区域 SizedBox( width: item.level * indentWidth, child: Stack( alignment: Alignment.centerRight, children: [ // 竖线:如果是最后一项且没有子项,只显示上半部分 if (!item.isLast || (item.children?.isNotEmpty ?? false)) Positioned( top: 0, bottom: 0, right: 0, child: Container(width: lineWidth, color: lineColor), ) else Positioned( top: 0, bottom: textCenterOffset, right: 0, child: Container(width: lineWidth, color: lineColor), ), // 横线:非顶级项显示,连接到父级竖线 if (item.level > 0) Positioned( top: textCenterOffset, left: 0, right: 0, child: Container(height: lineWidth, color: lineColor), ), ], ), ), // 文本内容 Padding( padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 8.0), child: Text(item.text), ), ], ); } }
第四步:递归展开子项并在ListView中使用
因为原始数据有嵌套的子项,我们需要先递归展开所有项,并修正每个项的isLast标记:
// 递归展开所有嵌套项,同时设置正确的isLast标记 List<LineItem> _expandItems(List<LineItem> items) { List<LineItem> expanded = []; for (int i = 0; i < items.length; i++) { final currentItem = items[i]; // 标记当前项是否为同级最后一个 expanded.add(currentItem.copyWith(isLast: i == items.length - 1)); // 如果有子项,继续展开 if (currentItem.children != null && currentItem.children!.isNotEmpty) { expanded.addAll(_expandItems(currentItem.children!)); } } return expanded; } // 在页面的build方法中使用ListView @override Widget build(BuildContext context) { final expandedItems = _expandItems(rawItems); return Scaffold( body: ListView.builder( padding: const EdgeInsets.all(16.0), itemCount: expandedItems.length, itemBuilder: (context, index) { return LineConnectedItem(item: expandedItems[index]); }, ), ); }
关键逻辑说明
- 缩进与线条对齐:用
indentWidth控制每级的缩进距离,线条始终对齐到缩进区域的最右侧,保证和父项的竖线衔接 - 竖线截断逻辑:如果是同级最后一个项且没有子项,竖线只显示到文本首字符的位置,避免多余的线条延伸到下方
- 横线衔接:次级项的横线垂直居中对齐文本首字符,刚好和父项的竖线交汇,形成连贯的树形线条
可优化点
- 可以把线条颜色、粗细、缩进宽度这些参数提取成组件的可配置属性,方便不同场景自定义
- 如果Text的行高有变化,需要调整
textCenterOffset的值,或者通过TextStyle的height动态计算 - 对于超大量的嵌套数据,递归展开可能有性能问题,可以改成迭代方式展开,或者用树形ListView直接处理嵌套
备注:内容来源于stack exchange,提问作者Sameera Tennakoon
相关产品推荐
相关产品推荐

