You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.21 14:54:50