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

Flutter中如何绘制带自定义图标与标题的虚线曲线?

Flutter实现带自定义Widget的虚线曲线UI

你当前的CustomPainter已经实现了虚线曲线和圆点标记,但需要将这些圆点替换为传入的自定义Widget(startWidget、endWidget、midWidget)。由于CustomPainter无法直接渲染Widget,必须先将Widget转换为图像,再绘制到Canvas上,以下是具体实现方案:

实现思路

  1. 通过RepaintBoundary和RenderRepaintBoundary将传入的Widget转换为Image对象
  2. 修改CurveLineMyPainter,接收转换后的Image参数,替换原有的圆点绘制逻辑
  3. 封装父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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 02:08:12