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

如何实现线条穿圆效果的虚线式RoadMap UI

实现路线图UI(虚线从圆形下方穿过)

你原来的交替布局思路会导致虚线在圆形上层,要实现虚线从圆形下方穿过的效果,核心是调整层级:把垂直虚线放在底层,圆形覆盖在虚线上,具体实现方案如下:

核心思路

用Stack嵌套结构:

  • 底层:一条垂直的虚线,固定在左侧位置
  • 上层:包含所有带圆形的列表项,圆形用背景色挡住下方的虚线,视觉上形成"虚线从圆形下方穿过"的效果

代码实现

1. 自定义垂直虚线组件

class VerticalDashedLine extends StatelessWidget {
  final double height;
  final double width;
  final Color color;

  const VerticalDashedLine({
    Key? key,
    required this.height,
    this.width = 1,
    this.color = Colors.grey,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return CustomPaint(
      size: Size(width, height),
      painter: _DashedLinePainter(color: color),
    );
  }
}

class _DashedLinePainter extends CustomPainter {
  final Color color;

  _DashedLinePainter({required this.color});

  @override
  void paint(Canvas canvas, Size size) {
    Paint paint = Paint()
      ..color = color
      ..strokeWidth = size.width
      ..style = PaintingStyle.stroke;

    const double dashHeight = 4;
    const double dashSpace = 4;
    double startY = 0;

    while (startY < size.height) {
      canvas.drawLine(
        Offset(size.width / 2, startY),
        Offset(size.width / 2, startY + dashHeight),
        paint,
      );
      startY += dashHeight + dashSpace;
    }
  }

  @override
  bool shouldRepaint(CustomPainter oldDelegate) => false;
}

2. 路线图主组件

class RoadmapUI extends StatelessWidget {
  // 示例数据,可替换为你的业务数据
  final List<Map<String, dynamic>> roadmapItems = [
    {"title": "Initial Setup", "isActive": true},
    {"title": "Data Collection", "isActive": false},
    {"title": "Analysis & Optimization", "isActive": false},
    {"title": "Final Review", "isActive": false},
  ];

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
      child: LayoutBuilder(
        builder: (context, constraints) {
          return Stack(
            children: [
              // 底层垂直虚线
              Positioned(
                left: 20, // 和圆形中心对齐
                top: 20, // 第一个圆形的顶部偏移
                bottom: 20, // 最后一个圆形的底部偏移
                child: VerticalDashedLine(
                  height: constraints.maxHeight - 40, // 自动适配列表高度
                  color: Colors.grey.shade400,
                ),
              ),
              // 上层列表项
              Column(
                children: roadmapItems.asMap().entries.map((entry) {
                  int index = entry.key;
                  var item = entry.value;
                  bool isActive = item["isActive"];
                  return Padding(
                    padding: const EdgeInsets.symmetric(vertical: 18),
                    child: Row(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        // 圆形(覆盖在虚线上)
                        Container(
                          width: 40,
                          height: 40,
                          decoration: BoxDecoration(
                            color: isActive ? Colors.blue : Colors.white,
                            border: Border.all(
                              color: Colors.blue,
                              width: 2,
                            ),
                            borderRadius: BorderRadius.circular(20),
                          ),
                          child: Center(
                            child: Text(
                              "${index + 1}",
                              style: TextStyle(
                                color: isActive ? Colors.white : Colors.blue,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                        ),
                        const SizedBox(width: 16),
                        // 内容区域
                        Expanded(
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text(
                                item["title"],
                                style: TextStyle(
                                  fontSize: 16,
                                  fontWeight: FontWeight.w500,
                                ),
                              ),
                              const SizedBox(height: 8),
                              Text(
                                "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
                                style: TextStyle(
                                  fontSize: 14,
                                  color: Colors.grey.shade600,
                                ),
                              ),
                            ],
                          ),
                        ),
                      ],
                    ),
                  );
                }).toList(),
              ),
            ],
          );
        },
      ),
    );
  }
}

关键细节说明

  • 圆形用与背景一致的颜色(示例中是白色)填充,挡住下方的虚线,形成"穿过"的视觉效果
  • 用LayoutBuilder获取列表的实际高度,让虚线自动适配长度,避免出现过长/过短的问题
  • 支持激活状态的样式切换(比如填充蓝色圆形),和示例图的交互效果匹配

内容的提问来源于stack exchange,提问作者Moha Med

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.27 07:04:51