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

剛好最近上班在畫各種圖表,來看看怎麼用程式畫出美麗的圖表

圓環進度條(circular progress ring)

首先 pi = 180度,圓的零是從水平右側開始算,如果要從正上方開始跑數值的話等於從270度開始

import UIKitlet aDegree: CGFloat = .pi/180  
let baseLineWidth: CGFloat = 2
let radius: CGFloat = 100
let startDegree: CGFloat = 270
let lineWidth: CGFloat = 8

首先let aDegree = CGFloat.pi / 180定義一度為何
let circleLineWidth: CGFloat = 1底部圓圈框線的粗度
let radius: CGFloat = 100 定義半徑
let startDegree: CGFloat = 270定義圓環條起始位置

一開始會有點困惑為何每個東西都要存進自己定義的常數裡,後來發現這樣之後修改真的很方便,例如修改框線的寬度就只要改一個地方,其他都會跟著變動,加上每個數值都是定義過的東西也會好理解很多

//開始畫底部的圓
let circlePath = UIBezierPath(ovalIn: CGRect(x: lineWidth, y: lineWidth, width: radius * 2, height: radius * 2))

UIBezierPath 中的ovalIn可以畫出圓形,定位xy從粗度一半的位置開始畫不然圖形會被截掉,這邊的border是center的,所以如果粗度為10的時候圓外面是5,圓裡面是5,直接用x,y定義粗度開始畫的話,四邊都還會留一點空間(粗度一半的空間)

畫好了圓但顯示不出來,需要一個CAShapLayer將它顯示,不然就像在空中畫圖,一場空

let circleLayer = CAShapeLayer()
circleLayer.path = circlePath.cgPath
circleLayer.strokeColor = CGColor(red: 220 / 255, green: 220 / 255, blue: 220 / 255, alpha: 1)
circleLayer.lineWidth = baseLineWidth
circleLayer.fillColor = UIColor.clear.cgColor //如果填顏色就是一個有顏色的圓形

這邊的circleLayer.strokeColor是CGColor 因此UIColor要指給他的話,必須用.cgColor轉換

接下來繪製百分比的那圈環型

let percentage: CGFloat = 70.25
let percentagePath = UIBezierPath(arcCenter: CGPoint(x: lineWidth + radius, y: lineWidth + radius), radius: radius, startAngle: aDegree * startDegree, endAngle: (startDegree + 360 * percentage / 100) * aDegree, clockwise: true)
let percentageLayer = CAShapeLayer()
percentageLayer.path = percentagePath.cgPath
percentageLayer.strokeColor = CGColor(red: 163.0 / 255.0, green: 1, blue: 1, alpha: 1)
percentageLayer.lineWidth = lineWidth
percentageLayer.fillColor = UIColor.clear.cgColor
percentageLayer.lineCap = .round
//使進度條兩邊變成圓弧(.round)/方形(.square)

UIBezierPath 中的arcCenter可以畫出被切過的圓形
arcCenter: 圓心座標

直接從startAngle連去endAngle

幫環形圖加上美麗的漸層 要運用到CAGradientLayer與mask
要來就要來點高級漸層,環形漸層!!!俗稱Angular Gradient or Conic Gradient

let gradientLayer = CAGradientLayer()//選擇漸層類型
gradientLayer.type = .conic
//在[]中放進顯示的顏色
gradientLayer.colors = [CGColor(red: 71/255, green: 1, blue: 1, alpha: 1), CGColor(red: 39/255, green: 91/255, blue: 1, alpha: 1)]
gradientLayer.frame = CGRect(x: 0, y: 0, width: (radius+lineWidth) * 2 , height: (radius+lineWidth) * 2)//圓心起始點定位 左上角是0,0 右上角是1,0
gradientLayer.startPoint = CGPoint(x: 0.5, y: 0.5)
//顏色結束位置 正上方270度的位置
gradientLayer.endPoint = CGPoint(x: 0.5, y: 0)
//繪製的形狀是你的mask 想像繪製的圖形遮罩底部的漸層
gradientLayer.mask = percentageLayer

CAGradientLayerType 裡面有三種: axial/conic/radial

axial/axial45度角/radial/conic

如果想要顯示文字百分比於圓形中央

let label = UILabel(frame: CGRect(x: 0, y: 0, width: 2*(radius+lineWidth), height: 2*(radius+lineWidth)))
label.textAlignment = .center
label.text = "\(percentage)%"
label.textColor = UIColor(red: 39/255, green: 91/255, blue: 1, alpha: 1)
label.font = UIFont.systemFont(ofSize: 22)

這時候還是什麼都看不到,運用view 及 addSubview 將所有塗層加進view內

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

成品如下~~~

甜甜圈圖表(donut chart)

這是一個土法煉鋼的版本,等之後會高級的for迴圈再來修改

import UIKitlet aDegree:CGFloat = .pi/180
let lineWidth:CGFloat = 50
let radius:CGFloat = 100
var percentages: [CGFloat] = [20.75, 33.25, 46]
let arcPathA = UIBezierPath(arcCenter: CGPoint(x: radius+lineWidth, y: radius+lineWidth), radius: radius, startAngle: 0, endAngle: aDegree*(360/100*percentages[0]), clockwise: true)
let arcPathALayer = CAShapeLayer()
arcPathALayer.path = arcPathA.cgPath
arcPathALayer.fillColor = UIColor.clear.cgColor
arcPathALayer.lineWidth = lineWidth
arcPathALayer.strokeColor = CGColor(red: 163.0 / 255.0, green: 1, blue: 1, alpha: 1)
let arcPathB = UIBezierPath(arcCenter: CGPoint(x: radius+lineWidth, y: radius+lineWidth), radius: radius, startAngle: aDegree*(360/100*percentages[0]), endAngle: aDegree*(360/100*(percentages[1]+percentages[0])), clockwise: true)
let arcPathBLayer = CAShapeLayer()
arcPathBLayer.path = arcPathB.cgPath
arcPathBLayer.fillColor = UIColor.clear.cgColor
arcPathBLayer.lineWidth = lineWidth
arcPathBLayer.strokeColor = CGColor(red: 39/255, green: 91/255, blue: 1, alpha: 1)
let arcPathC = UIBezierPath(arcCenter: CGPoint(x: radius+lineWidth, y: radius+lineWidth), radius: radius, startAngle: aDegree*(360/100*(percentages[1]+percentages[0])), endAngle: 0, clockwise: true)
let arcPathCLayer = CAShapeLayer()
arcPathCLayer.path = arcPathC.cgPath
arcPathCLayer.fillColor = UIColor.clear.cgColor
arcPathCLayer.lineWidth = lineWidth
arcPathCLayer.strokeColor = CGColor(red: 90/255, green: 200/255, blue: 1, alpha: 1)
let view = UIView(frame: CGRect(x: 0, y: 0, width: 2*(radius+lineWidth), height: 2*(radius+lineWidth)))
view.layer.addSublayer(arcPathALayer)
view.layer.addSublayer(arcPathBLayer)
view.layer.addSublayer(arcPathCLayer)
view

加入文字 建立function

func createLabel(percentage:CGFloat, startDegree:CGFloat) -> UILabel {
let textCenterDegree = startDegree + 360 * percentage / 2 / 100
let textPath = UIBezierPath(arcCenter: CGPoint(x: radius+lineWidth, y: radius+lineWidth), radius: radius, startAngle: textCenterDegree * aDegree, endAngle: textCenterDegree * aDegree, clockwise: true)
let label = UILabel(frame: CGRect(x: 0, y: 0, width: 50, height: 30))
label.text = "\(percentage)%"
label.font = UIFont.systemFont(ofSize: 12)
label.sizeToFit()
label.center = textPath.currentPoint
return label
}
view.addSubview(createLabel(percentage: percentages[0], startDegree: 0))//UILabel繼承自UIView
view.addSubview(createLabel(percentage: percentages[1], startDegree: percentages[0]*360/100))
view.addSubview(createLabel(percentage: percentages[2], startDegree: 360-percentages[2]*360/100))

希望在每一段圓弧的中心放上 label,因此我們利用 startDegree + 360 * percentage / 2 / 100 計算圓弧中心的角度,然後在 UIBezierPath 繪製圓弧時,在 startAngle & endAngle 傳入此角度,此時 path 的 currentPoint 即為 label 的中心座標。

  • 記得先設定完fontsize再設定sizeTofit 接續.center 不然文字不會顯示在表圖中央

圓餅圖(pie chart)

跟甜甜圈圖很像,UIBezierPath 設定形狀時,但要先用 move 移動到圓心,然後再以 addArc 設定圓弧,如此畫出的形狀才會是從圓心連到圓弧兩端點的扇形

以下是土法煉鋼加上function的應用

import UIKitlet aDegree:CGFloat = .pi/180
let radius:CGFloat = 100
var percentages: [CGFloat] = [20.75, 33.25, 46]
let view = UIView(frame: CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2))
func createArcPath(percentage:CGFloat, startDegree:CGFloat) -> UIView {
let endDegree = (percentage + startDegree) * 360 / 100
let arcPath = UIBezierPath()
let arcPathLayer = CAShapeLayer()
let view = UIView(frame: CGRect(x: 0, y: 0, width: radius * 2, height: radius * 2))
arcPath.move(to: view.center)
//以 UIBezierPath 設定形狀時,先用 move 移動到圓心,然後再以 addArc 設定圓弧,如此畫出的形狀才會是從圓心連到圓弧兩端點的扇形
arcPath.addArc(withCenter: view.center, radius: radius, startAngle: startDegree * 360 / 100 * aDegree, endAngle: aDegree * endDegree, clockwise: true)
arcPathLayer.path = arcPath.cgPath
arcPathLayer.fillColor = CGColor(red: (255 - startDegree) / 255, green: 1, blue: 1, alpha: 1)
view.layer.addSublayer(arcPathLayer)
return view
}
view.addSubview(createArcPath(percentage: percentages[0], startDegree: 0))
view.addSubview(createArcPath(percentage: percentages[1], startDegree: percentages[0]))
view.addSubview(createArcPath(percentage: percentages[2], startDegree: percentages[0]+percentages[1]))

function內的顏色用了一點小巧思,變量為紅色255減去起始角度/255,這樣每個色塊顏色就會不一樣拉,還會越來越藍,可愛!

Ta Daaaaa!

參考自彼得潘大大

--

--