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

Flutter中为StrokeCap.butt/Square实现端点圆角的半环形图表方案

实现带端点圆角的半环形图表(保留StrokeCap.butt/Square基础样式)

针对你的需求,这里提供两种可行的Flutter实现方案,无需依赖SVG经验即可快速达成目标效果:

方案一:叠加路径实现端点圆角

最直观的方式是先绘制基础半环形(用StrokeCap.butt保证端点平直),再在两端叠加小圆形模拟圆角,完美匹配目标样式。

完整实现代码

import 'dart:math' as math;
import 'package:flutter/material.dart';

class RoundedEndSemiCircle extends StatelessWidget {
  final double progress; // 0-1范围的进度值
  final double strokeWidth;
  final Color color;
  final Color backgroundColor;
  final double size;

  const RoundedEndSemiCircle({
    super.key,
    required this.progress,
    required this.strokeWidth,
    required this.color,
    required this.backgroundColor,
    required this.size,
  });

  @override
  Widget build(BuildContext context) {
    return SizedBox(
      width: size,
      height: size,
      child: CustomPaint(
        painter: _RoundedEndSemiCirclePainter(
          progress: progress,
          strokeWidth: strokeWidth,
          color: color,
          backgroundColor: backgroundColor,
        ),
      ),
    );
  }
}

class _RoundedEndSemiCirclePainter extends CustomPainter {
  final double progress;
  final double strokeWidth;
  final Color color;
  final Color backgroundColor;

  _RoundedEndSemiCirclePainter({
    required this.progress,
    required this.strokeWidth,
    required this.color,
    required this.backgroundColor,
  });

  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height / 2);
    final radius = (size.width - strokeWidth) / 2;
    final halfCircleAngle = math.pi; // 半环对应180度弧度

    // 绘制背景半环
    final backgroundPaint = Paint()
      ..color = backgroundColor
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.butt;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      math.pi, // 起始角度从左侧(180度)开始
      halfCircleAngle,
      false,
      backgroundPaint,
    );

    // 绘制进度半环(基础平直端点)
    final progressPaint = Paint()
      ..color = color
      ..style = PaintingStyle.stroke
      ..strokeWidth = strokeWidth
      ..strokeCap = StrokeCap.butt;
    final sweepAngle = halfCircleAngle * progress;
    canvas.drawArc(
      Rect.fromCircle(center: center, radius: radius),
      math.pi,
      sweepAngle,
      false,
      progressPaint,
    );

    // 计算并绘制两端圆角
    final roundedRadius = strokeWidth / 2; // 圆角半径等于线宽的一半,视觉协调

    // 起始端圆角(进度>0时才绘制)
    if (progress > 0) {
      final startOffset = Offset(
        center.dx + radius * math.cos(math.pi),
        center.dy + radius * math.sin(math.pi),
      );
      canvas.drawCircle(startOffset, roundedRadius, progressPaint);
    }

    // 结束端圆角(进度>0时才绘制)
    if (progress > 0) {
      final endAngle = math.pi + sweepAngle;
      final endOffset = Offset(
        center.dx + radius * math.cos(endAngle),
        center.dy + radius * math.sin(endAngle),
      );
      canvas.drawCircle(endOffset, roundedRadius, progressPaint);
    }
  }

  @override
  bool shouldRepaint(covariant _RoundedEndSemiCirclePainter oldDelegate) {
    return oldDelegate.progress != progress ||
        oldDelegate.strokeWidth != strokeWidth ||
        oldDelegate.color != color ||
        oldDelegate.backgroundColor != backgroundColor;
  }
}

使用示例

RoundedEndSemiCircle(
  progress: 0.7,
  strokeWidth: 20,
  color: Colors.blue,
  backgroundColor: Colors.grey.shade200,
  size: 200,
)

方案二:自定义闭合路径构建带圆角的半环

如果不想叠加多个绘制元素,可以直接构建包含端点圆角的闭合路径,通过填充路径实现效果,性能更优。

核心代码片段(替换方案一中的进度绘制逻辑)

// 构建带圆角的进度路径
final path = Path();
final innerRadius = radius - strokeWidth;
final roundedRadius = strokeWidth / 2;

if (progress > 0) {
  final startAngle = math.pi;
  final endAngle = math.pi + sweepAngle;

  // 起始端圆角:从内侧起始点画圆弧到外侧起始点
  final startInnerOffset = Offset(
    center.dx + innerRadius * math.cos(startAngle),
    center.dy + innerRadius * math.sin(startAngle),
  );
  final startOuterOffset = Offset(
    center.dx + radius * math.cos(startAngle),
    center.dy + radius * math.sin(startAngle),
  );
  path.moveTo(startInnerOffset.dx, startInnerOffset.dy);
  path.arcToPoint(
    startOuterOffset,
    radius: Radius.circular(roundedRadius),
    clockwise: false,
  );

  // 绘制外侧进度圆弧
  path.arcTo(
    Rect.fromCircle(center: center, radius: radius),
    startAngle,
    sweepAngle,
    false,
  );

  // 结束端圆角:从外侧结束点画圆弧到内侧结束点
  final endOuterOffset = Offset(
    center.dx + radius * math.cos(endAngle),
    center.dy + radius * math.sin(endAngle),
  );
  final endInnerOffset = Offset(
    center.dx + innerRadius * math.cos(endAngle),
    center.dy + innerRadius * math.sin(endAngle),
  );
  path.arcToPoint(
    endInnerOffset,
    radius: Radius.circular(roundedRadius),
    clockwise: false,
  );

  // 绘制内侧圆弧闭合路径
  path.arcTo(
    Rect.fromCircle(center: center, radius: innerRadius),
    endAngle,
    -sweepAngle, // 反向绘制内侧圆弧
    false,
  );
}

// 填充路径(注意这里把paint的style改成fill)
canvas.drawPath(path, progressPaint..style = PaintingStyle.fill);

关键提示

  • 圆角半径设置为strokeWidth / 2时,视觉效果最贴合目标样式;
  • 记得判断progress > 0再绘制圆角,避免进度为0时出现多余圆点;
  • 两种方案都保留了StrokeCap.butt的基础端点特性,仅通过额外绘制实现圆角效果。

内容的提问来源于stack exchange,提问作者Youssef Hanna

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 16:08:12