Flutter中如何绘制带自定义图标与标题的虚线曲线?
Flutter实现带自定义Widget的虚线曲线UI
你当前的CustomPainter已经实现了虚线曲线和圆点标记,但需要将这些圆点替换为传入的自定义Widget(startWidget、endWidget、midWidget)。由于CustomPainter无法直接渲染Widget,必须先将Widget转换为图像,再绘制到Canvas上,以下是具体实现方案:
实现思路
- 通过
RepaintBoundary和RenderRepaintBoundary将传入的Widget转换为Image对象 - 修改
CurveLineMyPainter,接收转换后的Image参数,替换原有的圆点绘制逻辑 - 封装父Widget处理Widget转Image的异步逻辑,并将Image传递给CustomPainter
完整代码实现
1. 修改后的CustomPainter
class CurveLineMyPainter extends CustomPainter { final Image? startImage; final Image? endImage; final Image? midImage; CurveLineMyPainter({this.startImage, this.endImage, this.midImage}); @override void paint(Canvas canvas, Size size) { Paint paint = Paint() ..color = Colors.black.withOpacity(0.6) ..strokeWidth = 1.0 ..style = PaintingStyle.stroke ..strokeCap = StrokeCap.round; Path path = Path(); final startPoint = Offset(size.width / 2 + 20, size.height * 0.1); final endPoint = Offset(size.width * 0.8 - 80, size.height * 0.95); path.moveTo(startPoint.dx, startPoint.dy); path.cubicTo( size.width * -0.4 + 20, size.height * 0.9, // 控制点1 size.width * 1 + 80, size.height * 0.15, // 控制点2 endPoint.dx, endPoint.dy, // 终点 ); drawDashedPath(canvas, paint, path); // 绘制自定义图像标记 if (startImage != null) { drawImageMarker(canvas, startPoint, startImage!); } if (endImage != null) { drawImageMarker(canvas, endPoint, endImage!); } PathMetrics pathMetrics = path.computeMetrics(); for (PathMetric pathMetric in pathMetrics) { Tangent tangent = pathMetric.getTangentForOffset(pathMetric.length * 0.65)!; if (midImage != null) { drawImageMarker(canvas, tangent.position, midImage!); } } } void drawDashedPath(Canvas canvas, Paint paint, Path path) { const double dashWidth = 5.0; const double dashSpace = 5.0; PathMetrics pathMetrics = path.computeMetrics(); for (PathMetric pathMetric in pathMetrics) { double distance = 0.0; while (distance < pathMetric.length) { final double start = distance; final double end = distance + dashWidth; final double finalEnd = end > pathMetric.length ? pathMetric.length : end; canvas.drawPath( pathMetric.extractPath(start, finalEnd), paint, ); distance = finalEnd + dashSpace; } } } void drawImageMarker(Canvas canvas, Offset position, Image image) { // 计算偏移量,让图像中心对齐曲线标记点 final offset = Offset( position.dx - image.width / 2, position.dy - image.height / 2, ); canvas.drawImage(image, offset, Paint()); } @override bool shouldRepaint(CurveLineMyPainter oldDelegate) { return oldDelegate.startImage != startImage || oldDelegate.endImage != endImage || oldDelegate.midImage != midImage; } }
2. 封装父Widget处理Widget转Image
class CurveLineWithWidgets extends StatefulWidget { final Widget? startWidget; final Widget? endWidget; final Widget? midWidget; final Size size; const CurveLineWithWidgets({ super.key, this.startWidget, this.endWidget, this.midWidget, required this.size, }); @override State<CurveLineWithWidgets> createState() => _CurveLineWithWidgetsState(); } class _CurveLineWithWidgetsState extends State<CurveLineWithWidgets> { Image? _startImage; Image? _endImage; Image? _midImage; @override void initState() { super.initState(); _convertWidgetsToImages(); } @override void didUpdateWidget(CurveLineWithWidgets oldWidget) { super.didUpdateWidget(oldWidget); if (oldWidget.startWidget != widget.startWidget || oldWidget.endWidget != widget.endWidget || oldWidget.midWidget != widget.midWidget) { _convertWidgetsToImages(); } } Future<void> _convertWidgetsToImages() async { if (widget.startWidget != null) { _startImage = await _widgetToImage(widget.startWidget!); } if (widget.endWidget != null) { _endImage = await _widgetToImage(widget.endWidget!); } if (widget.midWidget != null) { _midImage = await _widgetToImage(widget.midWidget!); } if (mounted) setState(() {}); } Future<Image> _widgetToImage(Widget widget) async { final repaintBoundary = RenderRepaintBoundary(); final renderObject = repaintBoundary; final view = WidgetsBinding.instance.platformDispatcher.views.first; final logicalSize = view.physicalSize / view.devicePixelRatio; RenderObjectToWidgetAdapter<RenderBox> adapter = RenderObjectToWidgetAdapter( container: renderObject, child: Directionality( textDirection: TextDirection.ltr, child: widget, ), ); adapter.attachToRenderTree(view); await repaintBoundary.paintScene(RasterizedScene( size: logicalSize, devicePixelRatio: view.devicePixelRatio, )); final image = await repaintBoundary.toImage(pixelRatio: view.devicePixelRatio); final byteData = await image.toByteData(format: ImageByteFormat.png); return Image.memory(byteData!.buffer.asUint8List()); } @override Widget build(BuildContext context) { return CustomPaint( size: widget.size, painter: CurveLineMyPainter( startImage: _startImage, endImage: _endImage, midImage: _midImage, ), ); } }
3. 使用方式
CurveLineWithWidgets( size: const Size(300, 400), // 自定义画布尺寸 startWidget: Container( width: 20, height: 20, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(10), ), child: const Icon(Icons.play_arrow, color: Colors.white, size: 12), ), midWidget: const CircleAvatar( backgroundColor: Colors.green, child: Text("M", style: TextStyle(color: Colors.white)), radius: 15, ), endWidget: const Icon(Icons.stop, color: Colors.red, size: 20), )
关键说明
- Widget转Image:通过
RenderRepaintBoundary将Widget渲染为图像,这是CustomPainter中使用Widget的唯一可行方案 - 图像对齐:
drawImageMarker方法通过偏移计算,确保图像中心与曲线上的标记点精准对齐 - 重绘优化:修改
shouldRepaint方法,仅当图像资源变化时触发重绘,提升性能
内容的提问来源于stack exchange,提问作者Dhaval Chaudhari
相关产品推荐
相关产品推荐

