如何解决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?如果是这样,我认为你的方法有几个问题。首先,查看您的代码要点,我对您的操作方式进行了一些更改:
- 更改了饼图段的大小(以便我可以测试 > 180° 的段)和颜色。
- 我在 CGFloat 扩展中添加了一个方便的 toRadians() 函数(这与您已经添加的 toRadians() 函数正好相反)。
- 我将半径变量更改为边界宽度/高度的最小值(不是您所做的最大值),以便它适合视图而无需裁剪。这只是个人喜好,不会改变代码的整体功能(例如,您可能需要它更大且可滚动,而我只是想调试这个特定问题)。我还添加了填充,这样当它们分开时它仍然适合这些段。
- 我沿着你原来的解决问题的路线;在饼图的中心绘制所有线段,然后将它们隔开,而不是试图将每个线段都绘制在偏离中心的位置。您可以执行任一路线,尽管在构建它们时将它们居中放置更简单,并且代码更易读。间隔是通过 createPath: 函数末尾的仿射变换实现的,该变换将它们间隔开给定线段的中角。您可能希望在现实生活中比这更聪明一点(这有点原始),因为根据屏幕截图,非常大的段看起来比小段彼此分离得更远(红色段出现绿色和蓝色的距离比绿色和蓝色彼此的距离更远)。因此,您可能想要开发一种算法,该算法不仅包含线段的中角,还包含该线段的大小,以便不仅确定方向而且确定分隔线段的距离?或者在确定分离方向时可能会考虑段的邻居的中角?个人品味。
- 在您的 layoutSubviews() 中,您为每个段提供了具有不同 oRadius 的 createPath()。这就是为什么您的段具有彼此不同的半径。我只是为所有这些提供了“半径”。如果您在我的 createPath() 函数中注释掉仿射变换(将它们隔开),您将看到我的版本中的段的大小半径都相同。
- 我将 path.close() 移动到 createPath() 函数中,而不是在调用它之后。看起来更整洁。
- 在绘制给定线段方面,我采用了完全不同的方法(除了将其绘制在饼图中的中心,然后再移动)。我用 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)
}
}
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。