如何实现线条穿圆效果的虚线式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
相关产品推荐
相关产品推荐

