Swift - 三层环状进度条组件开发(仿Apple Watch上运动记录圆圈)
Apple Watch发布的时候,上面的有个类型仪表盘一样的圆形刻度盘让人印象深刻。它使用红绿蓝三色圆圈分别表示用户的活动,锻炼,站立情况。让人一眼就能了解自己当天的运动状况。

1,彩虹进度条开发
(1)同开发其它的自定义组件一样,我们还是通过继承 UIView 来实现。(2)整个组件由三个圆弧组成,在 drawRect() 方法中绘制添加。
(3)组件提供内、中、外三个圆圈的半径、颜色、进度属性,以及圆圈宽度属性让用户设置。
(4)组件类和属性使用IB标识,让其在StoryBoard中也能自由设置属性,并实时渲染效果。
效果图如下:

组件代码如下:
import Foundation
import UIKit
@IBDesignable
class RainbowProgressView: UIView {
//进度条颜色(内圈、中间、外圈)
@IBInspectable var firstColor: UIColor = UIColor(
red: (37.0/255.0), green: (252.0/255), blue: (244.0/255.0), alpha: 1.0)
@IBInspectable var secondColor: UIColor = UIColor(
red: (171.0/255.0), green: (250.0/255), blue: (81.0/255.0), alpha: 1.0)
@IBInspectable var thirdColor: UIColor = UIColor(
red: (238.0/255.0), green: (32.0/255), blue: (53.0/255.0), alpha: 1.0)
//进度条半径(内圈、中间、外圈)
@IBInspectable var innerCircleRadius:CGFloat = 20
@IBInspectable var middleCircleRadius:CGFloat = 45
@IBInspectable var outerCircleRadius:CGFloat = 70
//进度0~1(内圈、中间、外圈)
@IBInspectable var firstPercent:CGFloat = 0.75
@IBInspectable var secondPercent:CGFloat = 0.75
@IBInspectable var thirdPercent:CGFloat = 0.75
//进度条宽度
@IBInspectable var circleWeight:CGFloat = 16
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clearColor()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
// 绘制
override func drawRect(rect: CGRect) {
// 添加三个环形进度条
self.addCirle(innerCircleRadius, color: firstColor, percent: firstPercent)
self.addCirle(middleCircleRadius, color: secondColor, percent: secondPercent)
self.addCirle(outerCircleRadius, color: thirdColor, percent: thirdPercent)
}
//添加环形进度
func addCirle(arcRadius: CGFloat, color: UIColor, percent:CGFloat) {
let X = CGRectGetMidX(self.bounds)
let Y = CGRectGetMidY(self.bounds)
// 进度条圆弧
let barPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barPath, strokeStart: 0, strokeEnd: percent,
strokeColor: color, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
}
//添加圆弧
func addOval(lineWidth: CGFloat, path: CGPathRef, strokeStart: CGFloat,
strokeEnd: CGFloat, strokeColor: UIColor, fillColor: UIColor,
shadowRadius: CGFloat, shadowOpacity: Float, shadowOffsset: CGSize) {
let arc = CAShapeLayer()
arc.lineWidth = lineWidth
arc.path = path
arc.strokeStart = strokeStart
arc.strokeEnd = strokeEnd
arc.strokeColor = strokeColor.CGColor
arc.fillColor = fillColor.CGColor
arc.shadowColor = UIColor.blackColor().CGColor
arc.shadowRadius = shadowRadius
arc.shadowOpacity = shadowOpacity
arc.shadowOffset = shadowOffsset
layer.addSublayer(arc)
}
}
测试代码:
2,组件功能改进之一:圆弧两端增加圆头
为了让圆弧没有那么生硬,我们在圆弧的两端分别添加两个圆形,这样会更圆润些。同时终点上的圆形增加了个阴影,这样效果会更好些。

改进后的组件代码(高亮处为修改的地方):
3,组件功能改进之二:添加进度条背景
看Apple Watch的运动状态圆盘可以发现,每个环形进度条后面还有一个相同色系的淡色进度槽,表示未完成的部分。
实现方式就是在各个进度条底部添加个透明圆环,颜色就是在其对应进度条颜色基础上增加透明度即可。
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rainbow1 = RainbowProgressView(frame: CGRectMake(5,20,160,160))
self.view.addSubview(rainbow1)
let rainbow2 = RainbowProgressView(frame: CGRectMake(175,20,140,140))
rainbow2.circleWeight = 18
rainbow2.innerCircleRadius = 20
rainbow2.middleCircleRadius = 40
rainbow2.outerCircleRadius = 60
rainbow2.firstPercent = 0.5
rainbow2.secondPercent = 0.65
rainbow2.thirdPercent = 0.75
self.view.addSubview(rainbow2)
}
override func didReceiveMemoryWarning() {
super.didReceiveMemoryWarning()
}
}
2,组件功能改进之一:圆弧两端增加圆头
为了让圆弧没有那么生硬,我们在圆弧的两端分别添加两个圆形,这样会更圆润些。同时终点上的圆形增加了个阴影,这样效果会更好些。

改进后的组件代码(高亮处为修改的地方):
import Foundation
import UIKit
@IBDesignable
class RainbowProgressViewEx: UIView {
//进度条颜色(内圈、中间、外圈)
@IBInspectable var firstColor: UIColor = UIColor(
red: (37.0/255.0), green: (252.0/255), blue: (244.0/255.0), alpha: 1.0)
@IBInspectable var secondColor: UIColor = UIColor(
red: (171.0/255.0), green: (250.0/255), blue: (81.0/255.0), alpha: 1.0)
@IBInspectable var thirdColor: UIColor = UIColor(
red: (238.0/255.0), green: (32.0/255), blue: (53.0/255.0), alpha: 1.0)
//进度条半径(内圈、中间、外圈)
@IBInspectable var innerCircleRadius:CGFloat = 20
@IBInspectable var middleCircleRadius:CGFloat = 45
@IBInspectable var outerCircleRadius:CGFloat = 70
//进度0~1(内圈、中间、外圈)
@IBInspectable var firstPercent:CGFloat = 0.75
@IBInspectable var secondPercent:CGFloat = 0.75
@IBInspectable var thirdPercent:CGFloat = 0.75
//进度条宽度
@IBInspectable var circleWeight:CGFloat = 16
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clearColor()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
// 绘制
override func drawRect(rect: CGRect) {
// 添加三个环形进度条
self.addCirle(innerCircleRadius, color: firstColor, percent: firstPercent)
self.addCirle(middleCircleRadius, color: secondColor, percent: secondPercent)
self.addCirle(outerCircleRadius, color: thirdColor, percent: thirdPercent)
}
//添加环形进度
func addCirle(arcRadius: CGFloat, color: UIColor, percent:CGFloat) {
let X = CGRectGetMidX(self.bounds)
let Y = CGRectGetMidY(self.bounds)
// 进度条圆弧
let barPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barPath, strokeStart: 0, strokeEnd: percent,
strokeColor: color, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条起点圆头
let startPath = UIBezierPath(ovalInRect: CGRectMake(X-circleWeight/2,
Y-arcRadius-circleWeight/2, circleWeight, circleWeight)).CGPath
self.addOval(0.0, path: startPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条终点圆头
let endDotPoint = calcCircleCoordinateWithCenter(CGPoint(x: X, y: Y),
radius: arcRadius, angle: -percent*360+90)
let endDotPath = UIBezierPath(ovalInRect: CGRectMake(endDotPoint.x-circleWeight/2,
endDotPoint.y-circleWeight/2, circleWeight, circleWeight)).CGPath
self.addOval(0.0, path: endDotPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 5.0, shadowOpacity: 0.5, shadowOffsset: CGSizeZero)
}
//计算圆弧上点的坐标
func calcCircleCoordinateWithCenter(center:CGPoint, radius:CGFloat, angle:CGFloat)
-> CGPoint {
let x2 = radius*CGFloat(cosf(Float(angle)*Float(M_PI)/Float(180)));
let y2 = radius*CGFloat(sinf(Float(angle)*Float(M_PI)/Float(180)));
return CGPointMake(center.x+x2, center.y-y2);
}
//添加圆弧
func addOval(lineWidth: CGFloat, path: CGPathRef, strokeStart: CGFloat,
strokeEnd: CGFloat, strokeColor: UIColor, fillColor: UIColor,
shadowRadius: CGFloat, shadowOpacity: Float, shadowOffsset: CGSize) {
let arc = CAShapeLayer()
arc.lineWidth = lineWidth
arc.path = path
arc.strokeStart = strokeStart
arc.strokeEnd = strokeEnd
arc.strokeColor = strokeColor.CGColor
arc.fillColor = fillColor.CGColor
arc.shadowColor = UIColor.blackColor().CGColor
arc.shadowRadius = shadowRadius
arc.shadowOpacity = shadowOpacity
arc.shadowOffset = shadowOffsset
layer.addSublayer(arc)
}
}
看Apple Watch的运动状态圆盘可以发现,每个环形进度条后面还有一个相同色系的淡色进度槽,表示未完成的部分。
实现方式就是在各个进度条底部添加个透明圆环,颜色就是在其对应进度条颜色基础上增加透明度即可。
为了适应各种舞台背景,组件增加新属性 barBgAlpha,用于设置进度条底图的透明度。(黑色舞台背景建议0.3,白色舞台背景建议0.15)

改进后的组件代码(高亮处为修改的地方):
4,组件功能改进之三:进度条终点只有突出的部分有阴影
前面的样例中,我门是在进度条终点处添加了一个圆来实现突出的部分,所以其阴影在这个圆点周围都是存在的。
如果想要只让突出的部分周围有阴影,我门在上方覆盖一小段圆弧即可,这样就将后半部分的阴影盖住。(注意:当进度条很短的时候还需在起点处覆盖个纯色的圆点才行)

改进后的组件代码(高亮处为修改的地方):
import Foundation
import UIKit
@IBDesignable
class RainbowProgressViewEx2: UIView {
//进度条颜色(内圈、中间、外圈)
@IBInspectable var firstColor: UIColor = UIColor(
red: (37.0/255.0), green: (252.0/255), blue: (244.0/255.0), alpha: 1.0)
@IBInspectable var secondColor: UIColor = UIColor(
red: (171.0/255.0), green: (250.0/255), blue: (81.0/255.0), alpha: 1.0)
@IBInspectable var thirdColor: UIColor = UIColor(
red: (238.0/255.0), green: (32.0/255), blue: (53.0/255.0), alpha: 1.0)
//进度条半径(内圈、中间、外圈)
@IBInspectable var innerCircleRadius:CGFloat = 20
@IBInspectable var middleCircleRadius:CGFloat = 45
@IBInspectable var outerCircleRadius:CGFloat = 70
//进度0~1(内圈、中间、外圈)
@IBInspectable var firstPercent:CGFloat = 0.75
@IBInspectable var secondPercent:CGFloat = 0.75
@IBInspectable var thirdPercent:CGFloat = 0.75
//进度条宽度
@IBInspectable var circleWeight:CGFloat = 16
//进度条背景槽透明度(黑色舞台背景建议0.3,白色舞台背景建议0.15)
@IBInspectable var barBgAlpha:CGFloat = 0.3
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clearColor()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
// 绘制
override func drawRect(rect: CGRect) {
// 添加三个环形进度条
self.addCirle(innerCircleRadius, color: firstColor, percent: firstPercent)
self.addCirle(middleCircleRadius, color: secondColor, percent: secondPercent)
self.addCirle(outerCircleRadius, color: thirdColor, percent: thirdPercent)
}
//添加环形进度
func addCirle(arcRadius: CGFloat, color: UIColor, percent:CGFloat) {
let X = CGRectGetMidX(self.bounds)
let Y = CGRectGetMidY(self.bounds)
// 进度条圆弧背景
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
let barBgColor = UIColor(red: r, green: g, blue: b, alpha: barBgAlpha);
let barBgPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barBgPath, strokeStart: 0, strokeEnd: 1,
strokeColor: barBgColor, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条圆弧
let barPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barPath, strokeStart: 0, strokeEnd: percent,
strokeColor: color, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条起点圆头
let startPath = UIBezierPath(ovalInRect: CGRectMake(X-circleWeight/2,
Y-arcRadius-circleWeight/2, circleWeight, circleWeight)).CGPath
self.addOval(0.0, path: startPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条终点圆头
let endDotPoint = calcCircleCoordinateWithCenter(CGPoint(x: X, y: Y),
radius: arcRadius, angle: -percent*360+90)
let endDotPath = UIBezierPath(ovalInRect: CGRectMake(endDotPoint.x-circleWeight/2,
endDotPoint.y-circleWeight/2, circleWeight, circleWeight)).CGPath
self.addOval(0.0, path: endDotPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 5.0, shadowOpacity: 0.5, shadowOffsset: CGSizeZero)
}
//计算圆弧上点的坐标
func calcCircleCoordinateWithCenter(center:CGPoint, radius:CGFloat, angle:CGFloat)
-> CGPoint {
let x2 = radius*CGFloat(cosf(Float(angle)*Float(M_PI)/Float(180)));
let y2 = radius*CGFloat(sinf(Float(angle)*Float(M_PI)/Float(180)));
return CGPointMake(center.x+x2, center.y-y2);
}
//添加圆弧
func addOval(lineWidth: CGFloat, path: CGPathRef, strokeStart: CGFloat,
strokeEnd: CGFloat, strokeColor: UIColor, fillColor: UIColor,
shadowRadius: CGFloat, shadowOpacity: Float, shadowOffsset: CGSize) {
let arc = CAShapeLayer()
arc.lineWidth = lineWidth
arc.path = path
arc.strokeStart = strokeStart
arc.strokeEnd = strokeEnd
arc.strokeColor = strokeColor.CGColor
arc.fillColor = fillColor.CGColor
arc.shadowColor = UIColor.blackColor().CGColor
arc.shadowRadius = shadowRadius
arc.shadowOpacity = shadowOpacity
arc.shadowOffset = shadowOffsset
layer.addSublayer(arc)
}
}
4,组件功能改进之三:进度条终点只有突出的部分有阴影
前面的样例中,我门是在进度条终点处添加了一个圆来实现突出的部分,所以其阴影在这个圆点周围都是存在的。
如果想要只让突出的部分周围有阴影,我门在上方覆盖一小段圆弧即可,这样就将后半部分的阴影盖住。(注意:当进度条很短的时候还需在起点处覆盖个纯色的圆点才行)

改进后的组件代码(高亮处为修改的地方):
源码下载:import Foundation
import UIKit
@IBDesignable
class RainbowProgressViewEx3: UIView {
//进度条颜色(内圈、中间、外圈)
@IBInspectable var firstColor: UIColor = UIColor(
red: (37.0/255.0), green: (252.0/255), blue: (244.0/255.0), alpha: 1.0)
@IBInspectable var secondColor: UIColor = UIColor(
red: (171.0/255.0), green: (250.0/255), blue: (81.0/255.0), alpha: 1.0)
@IBInspectable var thirdColor: UIColor = UIColor(
red: (238.0/255.0), green: (32.0/255), blue: (53.0/255.0), alpha: 1.0)
//进度条半径(内圈、中间、外圈)
@IBInspectable var innerCircleRadius:CGFloat = 20
@IBInspectable var middleCircleRadius:CGFloat = 45
@IBInspectable var outerCircleRadius:CGFloat = 70
//进度0~1(内圈、中间、外圈)
@IBInspectable var firstPercent:CGFloat = 0.75
@IBInspectable var secondPercent:CGFloat = 0.75
@IBInspectable var thirdPercent:CGFloat = 0.75
//进度条宽度
@IBInspectable var circleWeight:CGFloat = 16
//进度条背景槽透明度(黑色舞台背景建议0.3,白色舞台背景建议0.15)
@IBInspectable var barBgAlpha:CGFloat = 0.3
required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder)
backgroundColor = UIColor.clearColor()
}
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = UIColor.clearColor()
}
// 绘制
override func drawRect(rect: CGRect) {
// 添加三个环形进度条
self.addCirle(innerCircleRadius, color: firstColor, percent: firstPercent)
self.addCirle(middleCircleRadius, color: secondColor, percent: secondPercent)
self.addCirle(outerCircleRadius, color: thirdColor, percent: thirdPercent)
}
//添加环形进度
func addCirle(arcRadius: CGFloat, color: UIColor, percent:CGFloat) {
let X = CGRectGetMidX(self.bounds)
let Y = CGRectGetMidY(self.bounds)
// 进度条圆弧背景
var r: CGFloat = 0, g: CGFloat = 0, b: CGFloat = 0, a: CGFloat = 0
color.getRed(&r, green: &g, blue: &b, alpha: &a)
let barBgColor = UIColor(red: r, green: g, blue: b, alpha: barBgAlpha);
let barBgPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barBgPath, strokeStart: 0, strokeEnd: 1,
strokeColor: barBgColor, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条圆弧
let barPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barPath, strokeStart: 0, strokeEnd: percent,
strokeColor: color, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条起点圆头
let startPath = UIBezierPath(ovalInRect: CGRectMake(X-circleWeight/2,
Y-arcRadius-circleWeight/2, circleWeight, circleWeight)).CGPath
self.addOval(0.0, path: startPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条终点圆头
let endDotPoint = calcCircleCoordinateWithCenter(CGPoint(x: X, y: Y),
radius: arcRadius, angle: -percent*360+90)
let endDotPath = UIBezierPath(ovalInRect: CGRectMake(endDotPoint.x-circleWeight/2,
endDotPoint.y-circleWeight/2, circleWeight, circleWeight)).CGPath
self.addOval(0.0, path: endDotPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 5.0, shadowOpacity: 0.5, shadowOffsset: CGSizeZero)
// 进度条遮罩1(圆弧)
let barMaskPath = UIBezierPath(arcCenter: CGPoint(x: X, y: Y), radius: arcRadius,
startAngle: CGFloat(-M_PI_2), endAngle: CGFloat(M_PI*1.5),
clockwise: true).CGPath
self.addOval(circleWeight, path: barMaskPath,
strokeStart: percent/6, strokeEnd: percent,
strokeColor: color, fillColor: UIColor.clearColor(),
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
// 进度条遮罩2(圆)
if percent < 0.5{
self.addOval(0.0, path: startPath, strokeStart: 0, strokeEnd: 1.0,
strokeColor: color, fillColor: color,
shadowRadius: 0, shadowOpacity: 0, shadowOffsset: CGSizeZero)
}
}
//计算圆弧上点的坐标
func calcCircleCoordinateWithCenter(center:CGPoint, radius:CGFloat, angle:CGFloat)
-> CGPoint {
let x2 = radius*CGFloat(cosf(Float(angle)*Float(M_PI)/Float(180)));
let y2 = radius*CGFloat(sinf(Float(angle)*Float(M_PI)/Float(180)));
return CGPointMake(center.x+x2, center.y-y2);
}
//添加圆弧
func addOval(lineWidth: CGFloat, path: CGPathRef, strokeStart: CGFloat,
strokeEnd: CGFloat, strokeColor: UIColor, fillColor: UIColor,
shadowRadius: CGFloat, shadowOpacity: Float, shadowOffsset: CGSize) {
let arc = CAShapeLayer()
arc.lineWidth = lineWidth
arc.path = path
arc.strokeStart = strokeStart
arc.strokeEnd = strokeEnd
arc.strokeColor = strokeColor.CGColor
arc.fillColor = fillColor.CGColor
arc.shadowColor = UIColor.blackColor().CGColor
arc.shadowRadius = shadowRadius
arc.shadowOpacity = shadowOpacity
arc.shadowOffset = shadowOffsset
layer.addSublayer(arc)
}
}

不需要旋转,谢谢了。
航哥,你好,请问下如果想做示例中的一个圆环,只是圆环从0到persent为0.5(或者其它值)的时候加动画,并且加一张图片跟着这个动画的路径从0走,该怎么做呢?我现在用UIView的animation动画改变arc.strokeEnd可以实现圆环的行走动画,但是UIImageView的动画加上去很麻烦也无法跟圆环的头同步走。