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

如何实现单侧重叠且端点层级正确的圆角Doughnut Chart?

问题:单侧重叠圆角环形图的层级显示异常

我希望创建一个单侧重叠且带有圆角的环形图(Doughnut Chart),要求每个分段的起始端点位于前一分段的下方,结束端点位于后一分段的上方。预期效果如下:
预期的单侧重叠圆角环形图

但我用以下代码实现后,最后一段(绿色)的两个端点都显示在顶层,不符合预期,实际输出效果:
实际输出的环形图(最后一段端点异常)

我当前的实现代码

private func drawCircle(){
        var startAngle: CGFloat = .pi / -2
        var endAngle: CGFloat = 0.0
        
        drawableItems.forEach { (item) in
            print(item.title)
            print(startAngle)
            let center = calculateCenter()
            endAngle = startAngle + calculateAngles(item: item)
            if endAngle > 2 * CGFloat.pi {
                endAngle -= 2 * .pi
            }
            
            // Radius of the view
            let radius: CGFloat = calculateRadius()
            let arcWidth: CGFloat = self.arcWidth
            let circlePath = UIBezierPath(arcCenter: center,
                                          radius: radius/2 - arcWidth/2,
                                          startAngle: startAngle,
                                          endAngle: endAngle,
                                          clockwise: true)
            
            circlePath.lineCapStyle = .round
            
            let shapeLayer: CAShapeLayer = CAShapeLayer()
            shapeLayer.path = circlePath.cgPath
            shapeLayer.strokeColor = item.color.cgColor
            shapeLayer.lineWidth = arcWidth
            shapeLayer.fillColor = UIColor.clear.cgColor
            shapeLayer.lineCap = .round
            layer.addSublayer(shapeLayer)
            startAngle = endAngle
        }
    }

解决方案

问题根源是环形闭合后,最后一段的起始端点会和第一段的结束端点重叠,而默认图层叠加顺序会让后添加的最后一段覆盖前面的内容。可以通过精准控制各分段的端点圆角和图层层级来修复:

修改后的代码

private func drawCircle(){
    var startAngle: CGFloat = .pi / -2
    var endAngle: CGFloat = 0.0
    let totalItems = drawableItems.count
    
    for (index, item) in drawableItems.enumerated() {
        let center = calculateCenter()
        endAngle = startAngle + calculateAngles(item: item)
        if endAngle > 2 * CGFloat.pi {
            endAngle -= 2 * .pi
        }
        
        let radius: CGFloat = calculateRadius()
        let arcWidth: CGFloat = self.arcWidth
        let shapeLayer: CAShapeLayer = CAShapeLayer()
        shapeLayer.strokeColor = item.color.cgColor
        shapeLayer.lineWidth = arcWidth
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = .round
        
        if index == totalItems - 1 {
            // 最后一段:只保留结束端点的圆角,起始端设为方形(让前一段覆盖)
            let adjustedPath = UIBezierPath(arcCenter: center,
                                            radius: radius/2 - arcWidth/2,
                                            startAngle: startAngle,
                                            endAngle: endAngle,
                                            clockwise: true)
            adjustedPath.lineCapStyle = .square
            shapeLayer.path = adjustedPath.cgPath
        } else {
            // 非最后一段:只保留起始端点的圆角,结束端设为方形(让下一段覆盖)
            let adjustedPath = UIBezierPath(arcCenter: center,
                                            radius: radius/2 - arcWidth/2,
                                            startAngle: startAngle,
                                            endAngle: endAngle,
                                            clockwise: true)
            adjustedPath.lineCapStyle = .square
            shapeLayer.path = adjustedPath.cgPath
            shapeLayer.zPosition = 1
        }
        
        layer.addSublayer(shapeLayer)
        startAngle = endAngle
    }
}

逻辑说明

  • 非最后一段:将结束端点的圆角改为方形,让下一段的起始圆角可以自然覆盖它的结束端,保证每个分段的结束端处于后一段的下方。
  • 最后一段:将起始端点的圆角改为方形,让前一段的结束端(方形)不会被它的起始圆角覆盖,同时保留结束端的圆角,完美匹配预期的层级关系。

如果需要更精细化的控制,也可以把每个分段拆成「起始圆角+主体弧+结束圆角」三个独立图层,分别设置zPosition:起始圆角设为0(底层)、主体弧设为1、结束圆角设为2(顶层),这种方式能完全精确控制每个部分的层级,但代码量会稍多。

内容的提问来源于stack exchange,提问作者Moinuddin Girach

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.12 07:30:52