UIBezierPath圆弧创建圆角和间距的饼图

问题描述

我想知道如何创建一个饼图,如图所示,饼图之间有圆角和空格。

我的第一种方法:我将馅饼移出其中心点偏移量 = 10 以使其看起来像照片。但似乎最大的馅饼的半径小于较小的馅饼。 然后我对 Radius 进行了更改,但是间距有点奇怪 而且由于newCenter点不在superview的中心,所以在一边被截断了。

outerRadius = outerRadius - offset * 2 * (1 - percentage)

(百分比是饼图在图表中所占的比例)

我的第二种方法:我计算每个饼图的中心点,而不是将它移出原来的中心点。想象一下,中间有一个空的圆圈,每个馅饼的新中心点都在那个圆圈内。

大馅饼仍然存在问题。

我尝试的每张幻灯片的新中心点:

let middleAngle = ((startAngle + endAngle) / 2.0).toradians()
let center = CGPoint(x: bounds.midX,y: bounds.midY)
let newCenter = CGPoint(x: center.x + cos(middleAngle) * offset,y: center.y + sin(middleAngle) * offset)

关于半径和中心点的问题 |预期结果

这是我的代码 https://gist.github.com/phongngo511/dfd416aaad45fc0241cd4526d80d94d6

解决方法

is this what you're trying to achieve?如果是这样,我认为你的方法有几个问题。首先,查看您的代码要点,我对您的操作方式进行了一些更改:

  1. 更改了饼图段的大小(以便我可以测试 > 180° 的段)和颜色。
  2. 我在 CGFloat 扩展中添加了一个方便的 toRadians() 函数(这与您已经添加的 toRadians() 函数正好相反)。
  3. 我将半径变量更改为边界宽度/高度的最小值(不是您所做的最大值),以便它适合视图而无需裁剪。这只是个人喜好,不会改变代码的整体功能(例如,您可能需要它更大且可滚动,而我只是想调试这个特定问题)。我还添加了填充,这样当它们分开时它仍然适合这些段。
  4. 我沿着你原来的解决问题的路线;在饼图的中心绘制所有线段,然后将它们隔开,而不是试图将每个线段都绘制在偏离中心的位置。您可以执行任一路线,尽管在构建它们时将它们居中放置更简单,并且代码更易读。间隔是通过 createPath: 函数末尾的仿射变换实现的,该变换将它们间隔开给定线段的中角。您可能希望在现实生活中比这更聪明一点(这有点原始),因为根据屏幕截图,非常大的段看起来比小段彼此分离得更远(红色段出现绿色和蓝色的距离比绿色和蓝色彼此的距离更远)。因此,您可能想要开发一种算法,该算法不仅包含线段的中角,还包含该线段的大小,以便不仅确定方向而且确定分隔线段的距离?或者在确定分离方向时可能会考虑段的邻居的中角?个人品味。
  5. 在您的 layoutSubviews() 中,您为每个段提供了具有不同 oRadius 的 createPath()。这就是为什么您的段具有彼此不同的半径。我只是为所有这些提供了“半径”。如果您在我的 createPath() 函数中注释掉仿射变换(将它们隔开),您将看到我的版本中的段的大小半径都相同。
  6. 我将 path.close() 移动到 createPath() 函数中,而不是在调用它之后。看起来更整洁。
  7. 在绘制给定线段方面,我采用了完全不同的方法(除了将其绘制在饼图中的中心,然后再移动)。我用 2 条直线和一个圆弧绘制了饼图的外圆周。对于圆角,我使用了二次贝塞尔曲线,而不是绘制弧线(注意:线段的中心圆角未正确绘制,导致奇怪的图形伪影)。这些只需要 1 个控制点,而不是像三次贝塞尔曲线那样需要 2 个控制点。因此,您可以将线段的角指定为该控制点,它会为您提供一个适合您要修圆的三角形角的圆角。因此,我只在每个角附近绘制线/弧,然后绘制四边形贝塞尔曲线以圆角,然后继续处理其余部分。

如果有任何需要澄清的地方,请告诉我,希望对您有所帮助!

    import UIKit
    
    class PieChartView: UIView {
    
    var onTouchPie: ((_ sliceIndex: Int) -> ())?
    var shouldHighlightPieOnTouch = false

    var shouldShowLabels: Bool = false {
        didSet { setNeedsLayout() }
    }
    var labelTextFont = UIFont.systemFont(ofSize: 12) {
        didSet { setNeedsLayout() }
    }
    var labelTextColor = UIColor.black {
        didSet { setNeedsLayout() }
    }
    
    var shouldShowTextPercentageFromFieFilledFigures = false {
        didSet { setNeedsLayout() }
    }
    
    var pieGradientColors: [[UIColor]] = [[.red,.red],[.cyan,.cyan],[.green,.green]] {
        didSet { setNeedsLayout() }
    }
    
    var pieFilledPercentages:[CGFloat] = [1,1,1] {
        didSet { setNeedsLayout() }
    }
    
    //var segments:[CGFloat] = [40,30,30] {
    var segments:[CGFloat] = [70,20,10] {
        didSet { setNeedsLayout() }
    }
    
    var offset:CGFloat = 15 {
        didSet { setNeedsLayout() }
    }
    
    var spaceLineColor: UIColor = .white {
        didSet { setNeedsLayout() }
    }
    
    private var labels: [UILabel] = []
    private var labelSize = CGSize(width: 100,height: 50)
    private var shapeLayers = [CAShapeLayer]()
    private var gradientLayers = [CAGradientLayer]()
    
    override func layoutSubviews() {
        super.layoutSubviews()

        labels.forEach({$0.removeFromSuperview()})
        labels.removeAll()

        shapeLayers.forEach({$0.removeFromSuperlayer()})
        shapeLayers.removeAll()

        gradientLayers.forEach({$0.removeFromSuperlayer()})
        gradientLayers.removeAll()

        let valueCount = segments.reduce(CGFloat(0),{$0 + $1})
        guard pieFilledPercentages.count >= 3,segments.count >= 3,pieGradientColors.count >= 3,valueCount > 0 else { return }
        let radius = min(bounds.width / 2,bounds.height / 2) * 0.9 //KEN CHANGED
        var startAngle: CGFloat = 360
        let proportions = segments.map({ ($0 / valueCount * 100).rounded()})

        for i in 0..<segments.count {
            let endAngle = startAngle - proportions[i] / 100 * 360

            let path = createPath(from: startAngle,to: endAngle,oRadius: radius,percentage: proportions[i])
            //path.close() //KEN CHANGED
            let shapeLayer = CAShapeLayer()
            shapeLayer.path = path.cgPath
            shapeLayers.append(shapeLayer)

            let gradientLayer = CAGradientLayer()
            gradientLayer.colors = pieGradientColors[i].map({$0.cgColor})
            if i == 0 {
                gradientLayer.locations = [0.5,1]
            } else {
                gradientLayer.locations = [0,0.5]
            }
            gradientLayer.mask = shapeLayer
            gradientLayer.frame = bounds
            if proportions[i] != 0 && pieFilledPercentages[i] != 0 {
                layer.addSublayer(gradientLayer)
                gradientLayers.append(gradientLayer)
            }

            let label = labelFromPoint(point: getCenterPointOfArc(startAngle: startAngle,endAngle: endAngle),andText: String(format: "%.f",shouldShowTextPercentageFromFieFilledFigures ? pieFilledPercentages[i] * 100 :segments[i]) + "%")
            label.isHidden = !shouldShowLabels
            if proportions[i] != 0 {
                addSubview(label)
                labels.append(label)
            }

            startAngle = endAngle
        }
    }
    
    private func labelFromPoint(point: CGPoint,andText text: String) -> UILabel {
        let label = UILabel(frame: CGRect(origin: point,size: labelSize))
        label.font = labelTextFont
        label.textColor = labelTextColor
        label.text = text
        return label
    }

    private func getCenterPointOfArc(startAngle: CGFloat,endAngle: CGFloat) -> CGPoint {
        let oRadius = max(bounds.width / 2,bounds.height / 2) * 0.8
        let center = CGPoint(x: oRadius,y: oRadius)
        let centerAngle = ((startAngle + endAngle) / 2.0).toRadians()
        let arcCenter = CGPoint(x: center.x + oRadius * cos(centerAngle),y: center.y - oRadius * sin(centerAngle))
        return CGPoint(x: (center.x + arcCenter.x) / 2,y: (center.y + arcCenter.y) / 2)
    }

    override func touchesBegan(_ touches: Set<UITouch>,with event: UIEvent?) {
        if let touch = touches.first,shouldHighlightPieOnTouch {
            shapeLayers.enumerated().forEach { (item) in
                if let path = item.element.path,path.contains(touch.location(in: self)) {
                    item.element.opacity = 1
                    onTouchPie?(item.offset)
                } else {
                    item.element.opacity = 0.3
                }
            }
        }
        super.touchesBegan(touches,with: event)
    }

    private func highlightLayer(index: Int) {
        shapeLayers.enumerated().forEach({$0.element.opacity = $0.offset == index ? 1: 0.3 })
    }

    private func createPath(from startAngle: CGFloat,to endAngle: CGFloat,oRadius: CGFloat,cornerRadius: CGFloat = 10,percentage: CGFloat) -> UIBezierPath {

        let radius: CGFloat = min(bounds.width,bounds.height) / 2.0 - (2.0 * offset)
        let center = CGPoint(x: bounds.midX,y: bounds.midY)
        let midPointAngle = ((startAngle + endAngle) / 2.0).toRadians() //used to spread the segment away from its neighbours after creation

        let startAngle = (360.0 - startAngle).toRadians()
        let endAngle = (360.0 - endAngle).toRadians()

        let circumference: CGFloat = CGFloat(2.0 * (Double.pi * Double(radius)))
        let arcLengthPerDegree = circumference / 360.0 //how many pixels long the outer arc is of the pie chart,per 1° of a pie segment
        let pieSegmentOuterCornerRadiusInDegrees: CGFloat = 4.0 //for a given segment (and if it's >4° in size),use up 2 of its outer arc's degrees as rounded corners.
        let pieSegmentOuterCornerRadius = arcLengthPerDegree * pieSegmentOuterCornerRadiusInDegrees
        
        let path = UIBezierPath()
        
        //move to the centre of the pie chart,offset by the corner radius (so the corner of the segment can be rounded in a bit)
        path.move(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius),y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)))
        //if the size of the pie segment isn't big enough to warrant rounded outer corners along its outer arc,don't round them off
        if ((endAngle - startAngle).toDegrees() <= (pieSegmentOuterCornerRadiusInDegrees * 2.0)) {
            //add line from centre of pie chart to 1st outer corner of segment
            path.addLine(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * radius),y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * radius)))
            //add arc for segment's outer edge on pie chart
            path.addArc(withCenter: center,radius: radius,startAngle: startAngle,endAngle: endAngle,clockwise: true)
            //move down to the centre of the pie chart,leaving room for rounded corner at the end
            path.addLine(to: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * cornerRadius),y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * cornerRadius)))
            //add final rounded corner in middle of pie chart
            path.addQuadCurve(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * cornerRadius),y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * cornerRadius)),controlPoint: center)
        } else { //round the corners on the outer arc
            //add line from centre of pie chart to circumference of segment,minus the space needed for the rounded corner
            path.addLine(to: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)),y: center.y + (sin(startAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius))))
            //add rounded corner onto start of outer arc
            let firstRoundedCornerEndOnArc = CGPoint(x: center.x + (cos(startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians() - CGFloat(360).toRadians()) * radius),y: center.y + (sin(startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians() - CGFloat(360).toRadians()) * radius))
            path.addQuadCurve(to: firstRoundedCornerEndOnArc,controlPoint: CGPoint(x: center.x + (cos(startAngle - CGFloat(360).toRadians()) * radius),startAngle: startAngle + pieSegmentOuterCornerRadiusInDegrees.toRadians(),endAngle: endAngle - pieSegmentOuterCornerRadiusInDegrees.toRadians(),clockwise: true)
            //add rounded corner onto end of outer arc
            let secondRoundedCornerEndOnLine = CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)),y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * (radius - pieSegmentOuterCornerRadius)))
            path.addQuadCurve(to: secondRoundedCornerEndOnLine,controlPoint: CGPoint(x: center.x + (cos(endAngle - CGFloat(360).toRadians()) * radius),y: center.y + (sin(endAngle - CGFloat(360).toRadians()) * radius)))
            //add line back to centre point of pie chart,controlPoint: center)
        }
        path.close()
        //spread the segments out around the pie chart centre
        path.apply(CGAffineTransform(translationX: cos(midPointAngle) * offset,y: -sin(midPointAngle) * offset))
        return path
    }
}

extension CGFloat {
    func toRadians() -> CGFloat {
        return self * CGFloat(Double.pi) / 180.0
    }
    func toDegrees() -> CGFloat {
        return self / (CGFloat(Double.pi) / 180.0)
    }
}

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...