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,组件功能改进之三:进度条终点只有突出的部分有阴影
前面的样例中,我门是在进度条终点处添加了一个圆来实现突出的部分,所以其阴影在这个圆点周围都是存在的。
如果想要只让突出的部分周围有阴影,我门在上方覆盖一小段圆弧即可,这样就将后半部分的阴影盖住。(注意:当进度条很短的时候还需在起点处覆盖个纯色的圆点才行)
改进后的组件代码(高亮处为修改的地方):
源码下载:hangge_1021.zip
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的动画加上去很麻烦也无法跟圆环的头同步走。