#14 利用 UIBezierPath 實現圓環進度條,甜甜圈圖表 & 圓餅圖

目的:利用程式刻出各式不一樣的圓

圓環進度條(circular progress ring)

首先要先定義常數

//將pi轉換為度數
let aDegree = CGFloat.pi / 180
//線條粗度
let lineWidth: CGFloat = 10
//半徑
let radius: CGFloat = 50
//開始的角度
let startDegree: CGFloat = 270

接著繪製底層的圓環

let circlePath = UIBezierPath(ovalIn: CGRect(x: lineWidth, y: lineWidth, width: radius * 2, height: radius * 2))
let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.lineWidth = 3
circleLayer.strokeColor = UIColor.gray.cgColor
circleLayer.fillColor = UIColor.clear.cgColor

上層進度條

let percentage: CGFloat = 60
let percentagePath = UIBezierPath(arcCenter: CGPoint(x: lineWidth + radius, y: lineWidth + radius), radius: radius, startAngle: aDegree * startDegree, endAngle: aDegree * ( startDegree + 360 * percentage / 100), clockwise: true)
let percentageLayer = CAShapeLayer()
percentageLayer.path = percentagePath.cgPath
percentageLayer.lineWidth = lineWidth
percentageLayer.strokeColor = UIColor.green.cgColor
percentageLayer.fillColor = UIColor.clear.cgColor
percentageLayer.lineCap = .round

值得一提的是,在繪製底層跟上層圓的路徑時,他們UIBzierPath用到的參數不太一樣,一開始很納悶,所以去稍微查了一下才了解到:

//這組參數所繪製的圓是封閉式路徑
let circlePath = UIBezierPath(ovalIn: CGRect(x: lineWidth, y: lineWidth, width: radius * 2, height: radius * 2))
//這組參數所繪製的圓是開放式路徑
let percentagePath = UIBezierPath(arcCenter: CGPoint(x: lineWidth + radius, y: lineWidth + radius), radius: radius, startAngle: aDegree * startDegree, endAngle: aDegree * ( startDegree + 360 * percentage / 100), clockwise: true)

接著要在圓的中心加入Label

let label = UILabel(frame: CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2))
label.text = "\(percentage)%"
label.textColor = .white
label.textAlignment = .center

最重要的一步,將上述做好的東西加到view上顯示出來

let view = UIView(frame: label.frame)
view.layer.addSublayer(circleLayer)
view.layer.addSublayer(percentageLayer)
view.addSubview(label)
view

如果想讓圓環進度條再更有質感一點,可以利用CAGradientLayer()加上Mask,呈現出漸層的進度條

let gradientLayer = CAGradientLayer()
gradientLayer.type = .conic
gradientLayer.colors = [CGColor(red: 1, green: 1, blue: 1, alpha: 1),CGColor(red: 50/255, green: 10/255, blue: 1, alpha: 1)]
gradientLayer.frame = CGRect(x: 0, y: 0, width: (radius + lineWidth) * 2, height: (radius + lineWidth) * 2)
gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
gradientLayer.mask = percentageLayer

因為percentageLayer已經是gradientLayer的遮罩,所以必須將view.layer.addSublayer(percentageLayer) 更改為view.layer.addSublayer(gradientLayer)

成品如下:

甜甜圈圖表(donut chart)

一樣先建立需要用到的常數及變數

let aDegree = CGFloat.pi / 180
let lineWidth: CGFloat = 30
let radius: CGFloat = 50
var startDegree: CGFloat = 270
let view = UIView(frame: CGRect(x: 0, y: 0, width: 2 * (lineWidth + radius), height: 2 * (lineWidth + radius)))
let center = CGPoint(x: lineWidth + radius, y: lineWidth + radius)
var percentages: [CGFloat] = [20, 30, 50]

建立一個會回傳UILabel的Function

func createLabel (percentage: CGFloat, startDegree: CGFloat) -> UILabel {
let textCenterDegree = startDegree + 360 * percentage / 100 / 2
let textPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: aDegree * startDegree, endAngle: aDegree * textCenterDegree, clockwise: true)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30))
label.text = "\(percentage)%"
label.font = UIFont.systemFont(ofSize: 8)
label.sizeToFit()
label.center = textPath.currentPoint
return label
}

使用for迴圈讓包在裡面的程式執行三次,且每段的結束角度等於下一段的開始角度,CGFloat.random可以隨機生成浮點數,0…1代表可以從0到1之間隨機生成浮點數

for percentage in percentages {
let endDegree: CGFloat = startDegree + 360 * percentage / 100
let percentagePath = UIBezierPath(arcCenter: center, radius: radius, startAngle: aDegree * startDegree, endAngle: aDegree * endDegree, clockwise: true)
let percentageLayer = CAShapeLayer()
percentageLayer.path = percentagePath.cgPath
percentageLayer.lineWidth = lineWidth
percentageLayer.strokeColor = UIColor(red: CGFloat.random(in: 0...1), green: CGFloat.random(in: 0...1), blue: CGFloat.random(in: 0...1), alpha: 1).cgColor
percentageLayer.fillColor = UIColor.clear.cgColor
view.layer.addSublayer(percentageLayer)
view.addSubview(createLabel(percentage: percentage, startDegree: startDegree))
startDegree = endDegree
}

成品如下:

圓餅圖(pie chart)

圓餅圖的作法跟甜甜圈的作法幾乎一樣,只差在畫出扇形跟中間顏色填滿

  • 要畫出扇形必須利用move(to: view.center)先把起始點放在圓的中心,然後利用addArc設定圓弧,這樣就能描出扇形的路徑
  • 甜甜圈因為要做出中間空心的圓,所以要使用到lineWidthfillColor設定成透明,但圓餅圖不需要用到lineWidth,只需要設定fillColor的顏色填滿整個圓就好
let radius: CGFloat = 50
var startDegree: CGFloat = 270
let aDegree: CGFloat = CGFloat.pi / 180
let percentages: [CGFloat] = [29,40,31]
let center = CGPoint(x: radius, y: radius)
let view = UIView(frame: CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2))
func createLabel (percentage: CGFloat, startDegree: CGFloat) -> UILabel {
let textCenterDegree = startDegree + 360 * percentage / 100 / 2
let textPath = UIBezierPath(arcCenter: view.center, radius: radius/2 , startAngle: aDegree * startDegree, endAngle: aDegree * textCenterDegree, clockwise: true)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30))
label.text = "\(percentage)%"
label.font = UIFont.systemFont(ofSize: 8)
label.sizeToFit()
label.center = textPath.currentPoint
return label
}
for percentage in percentages {
let endDegree = startDegree + 360 * percentage / 100
let percentagePath = UIBezierPath()
percentagePath.move(to: view.center)
percentagePath.addArc(withCenter: view.center, radius: radius, startAngle: aDegree * startDegree, endAngle: aDegree * endDegree, clockwise: true)

let percentLayer = CAShapeLayer()
percentLayer.path = percentagePath.cgPath
percentLayer.fillColor = UIColor(red: CGFloat.random(in: 0...1), green: CGFloat.random(in: 0...1), blue: CGFloat.random(in: 0...1), alpha: 1).cgColor
view.layer.addSublayer(percentLayer)
view.addSubview(createLabel(percentage: percentage, startDegree: startDegree))
startDegree = endDegree
}
view

成品如下:

Reference

--

--