Swift - 制作一个带动画效果的环形进度条
(本文代码已升级至Swift3)
(2)组件代码(OProgressView.swift)
(1)实现原理
使用关键帧动画(CAKeyframeAnimation)让圆点顺着圆弧路径移动。
(2)组件代码(OProgressView2.swift)
(1)实现原理
箭头图标在最开始位置时,默认方向向右。同时将 CAKeyframeAnimation 的 rotationMode 设为 kCAAnimationRotateAuto,这样随着进度改变,箭头会始终朝着前进方向。
(2)组件代码(OProgressView3.swift)
一、带动画效果的环形进度条
下面我们演示如何制作一个环状进度条组件,当进度改变时,进度条长度变化时是有动画效果的(我们还可以设置动画时间,或者关闭动画),效果图如下:
(1)动画实现原理
使用 Core Animation 动画根据进度改变进度条(CAShapeLayer)的 strokeEnd。
(2)组件代码(OProgressView.swift)
import UIKit @IBDesignable class OProgressView: UIView { struct Constant { //进度条宽度 static let lineWidth: CGFloat = 10 //进度槽颜色 static let trackColor = UIColor(red: 245/255.0, green: 245/255.0, blue: 245/255.0, alpha: 1) //进度条颜色 static let progressColoar = UIColor.orange } //进度槽 let trackLayer = CAShapeLayer() //进度条 let progressLayer = CAShapeLayer() //进度条路径(整个圆圈) let path = UIBezierPath() //当前进度 @IBInspectable var progress: Int = 0 { didSet { if progress > 100 { progress = 100 }else if progress < 0 { progress = 0 } } } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init(frame: CGRect) { super.init(frame: frame) } override func draw(_ rect: CGRect) { //获取整个进度条圆圈路径 path.addArc(withCenter: CGPoint(x: bounds.midX, y: bounds.midY), radius: bounds.size.width/2 - Constant.lineWidth, startAngle: angleToRadian(-90), endAngle: angleToRadian(270), clockwise: true) //绘制进度槽 trackLayer.frame = bounds trackLayer.fillColor = UIColor.clear.cgColor trackLayer.strokeColor = Constant.trackColor.cgColor trackLayer.lineWidth = Constant.lineWidth trackLayer.path = path.cgPath layer.addSublayer(trackLayer) //绘制进度条 progressLayer.frame = bounds progressLayer.fillColor = UIColor.clear.cgColor progressLayer.strokeColor = Constant.progressColoar.cgColor progressLayer.lineWidth = Constant.lineWidth progressLayer.path = path.cgPath progressLayer.strokeStart = 0 progressLayer.strokeEnd = CGFloat(progress)/100.0 layer.addSublayer(progressLayer) } //设置进度(可以设置是否播放动画) func setProgress(_ pro: Int,animated anim: Bool) { setProgress(pro, animated: anim, withDuration: 0.55) } //设置进度(可以设置是否播放动画,以及动画时间) func setProgress(_ pro: Int,animated anim: Bool, withDuration duration: Double) { progress = pro //进度条动画 CATransaction.begin() CATransaction.setDisableActions(!anim) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)) CATransaction.setAnimationDuration(duration) progressLayer.strokeEnd = CGFloat(progress)/100.0 CATransaction.commit() } //将角度转为弧度 fileprivate func angleToRadian(_ angle: Double)->CGFloat { return CGFloat(angle/Double(180.0) * M_PI) } }
二、进度条头部增加圆点
下面给进度条头部增加一个带阴影的圆点,其随着进度条的增长也会动态地移动位置。(1)实现原理
使用关键帧动画(CAKeyframeAnimation)让圆点顺着圆弧路径移动。
(2)组件代码(OProgressView2.swift)
import UIKit @IBDesignable class OProgressView2: UIView { struct Constant { //进度条宽度 static let lineWidth: CGFloat = 10 //进度槽颜色 static let trackColor = UIColor(red: 245/255.0, green: 245/255.0, blue: 245/255.0, alpha: 1) //进度条颜色 static let progressColoar = UIColor.orange } //进度槽 let trackLayer = CAShapeLayer() //进度条 let progressLayer = CAShapeLayer() //进度条路径(整个圆圈) let path = UIBezierPath() //头部圆点 var dot:UIView! //进度条圆环中点 var progressCenter:CGPoint { get{ return CGPoint(x: bounds.midX, y: bounds.midY) } } //进度条圆环中点 var radius:CGFloat{ get{ return bounds.size.width/2 - Constant.lineWidth } } //当前进度 @IBInspectable var progress: Int = 0 { didSet { if progress > 100 { progress = 100 }else if progress < 0 { progress = 0 } } } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init(frame: CGRect) { super.init(frame: frame) } override func draw(_ rect: CGRect) { //获取整个进度条圆圈路径 path.addArc(withCenter: progressCenter, radius: radius, startAngle: angleToRadian(-90), endAngle: angleToRadian(270), clockwise: true) //绘制进度槽 trackLayer.frame = bounds trackLayer.fillColor = UIColor.clear.cgColor trackLayer.strokeColor = Constant.trackColor.cgColor trackLayer.lineWidth = Constant.lineWidth trackLayer.path = path.cgPath layer.addSublayer(trackLayer) //绘制进度条 progressLayer.frame = bounds progressLayer.fillColor = UIColor.clear.cgColor progressLayer.strokeColor = Constant.progressColoar.cgColor progressLayer.lineWidth = Constant.lineWidth progressLayer.path = path.cgPath progressLayer.strokeStart = 0 progressLayer.strokeEnd = CGFloat(progress)/100.0 layer.addSublayer(progressLayer) //绘制进度条头部圆点 dot = UIView(frame:CGRect(x: 0, y: 0, width: Constant.lineWidth, height: Constant.lineWidth)) let dotPath = UIBezierPath(ovalIn: CGRect(x: 0,y: 0, width: Constant.lineWidth, height: Constant.lineWidth)).cgPath let arc = CAShapeLayer() arc.lineWidth = 0 arc.path = dotPath arc.strokeStart = 0 arc.strokeEnd = 1 arc.strokeColor = Constant.progressColoar.cgColor arc.fillColor = Constant.progressColoar.cgColor arc.shadowColor = UIColor.black.cgColor arc.shadowRadius = 5.0 arc.shadowOpacity = 0.5 arc.shadowOffset = CGSize.zero dot.layer.addSublayer(arc) dot.layer.position = calcCircleCoordinateWithCenter(progressCenter, radius: radius, angle: CGFloat(-progress)/100*360+90) addSubview(dot) } //设置进度(可以设置是否播放动画) func setProgress(_ pro: Int,animated anim: Bool) { setProgress(pro, animated: anim, withDuration: 0.55) } //设置进度(可以设置是否播放动画,以及动画时间) func setProgress(_ pro: Int,animated anim: Bool, withDuration duration: Double) { let oldProgress = progress progress = pro //进度条动画 CATransaction.begin() CATransaction.setDisableActions(!anim) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)) CATransaction.setAnimationDuration(duration) progressLayer.strokeEnd = CGFloat(progress)/100.0 CATransaction.commit() //头部圆点动画 let startAngle = angleToRadian(360*Double(oldProgress)/100 - 90) let endAngle = angleToRadian(360*Double(progress)/100 - 90) let clockWise = progress > oldProgress ? false : true let path2 = CGMutablePath() path2.addArc(center: CGPoint(x:bounds.midX,y: bounds.midY), radius: bounds.size.width/2 - Constant.lineWidth, startAngle: startAngle, endAngle: endAngle, clockwise: clockWise, transform: transform) let orbit = CAKeyframeAnimation(keyPath:"position") orbit.duration = duration orbit.path = path2 orbit.calculationMode = kCAAnimationPaced orbit.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) orbit.isRemovedOnCompletion = false orbit.fillMode = kCAFillModeForwards dot.layer.add(orbit,forKey:"Move") } //将角度转为弧度 fileprivate func angleToRadian(_ angle: Double)->CGFloat { return CGFloat(angle/Double(180.0) * M_PI) } //计算圆弧上点的坐标 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 CGPoint(x: center.x+x2, y: center.y-y2); } }
三、进度条头部再增加一个图片
下面给进度条头部再增加一个前进图标,其随着进度条的增长也会动态地改变朝向(一直朝向前进方向)。(1)实现原理
箭头图标在最开始位置时,默认方向向右。同时将 CAKeyframeAnimation 的 rotationMode 设为 kCAAnimationRotateAuto,这样随着进度改变,箭头会始终朝着前进方向。
(2)组件代码(OProgressView3.swift)
import UIKit @IBDesignable class OProgressView3: UIView { struct Constant { //进度条宽度 static let lineWidth: CGFloat = 10 //进度槽颜色 static let trackColor = UIColor(red: 245/255.0, green: 245/255.0, blue: 245/255.0, alpha: 1) //进度条颜色 static let progressColoar = UIColor.orange } //进度槽 let trackLayer = CAShapeLayer() //进度条 let progressLayer = CAShapeLayer() //进度条路径(整个圆圈) let path = UIBezierPath() //头部圆点 var dot = UIView() //头部箭头图片 var arrow = UIImageView(image: UIImage(named: "arrow")) //进度条圆环中点 var progressCenter:CGPoint { get{ return CGPoint(x: bounds.midX, y: bounds.midY) } } //进度条圆环中点 var radius:CGFloat{ get{ return bounds.size.width/2 - Constant.lineWidth } } //当前进度 @IBInspectable var progress: Int = 0 { didSet { if progress > 100 { progress = 100 }else if progress < 0 { progress = 0 } } } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) } override init(frame: CGRect) { super.init(frame: frame) } override func draw(_ rect: CGRect) { //获取整个进度条圆圈路径 path.addArc(withCenter: progressCenter, radius: radius, startAngle: angleToRadian(-90), endAngle: angleToRadian(270), clockwise: true) //绘制进度槽 trackLayer.frame = bounds trackLayer.fillColor = UIColor.clear.cgColor trackLayer.strokeColor = Constant.trackColor.cgColor trackLayer.lineWidth = Constant.lineWidth trackLayer.path = path.cgPath layer.addSublayer(trackLayer) //绘制进度条 progressLayer.frame = bounds progressLayer.fillColor = UIColor.clear.cgColor progressLayer.strokeColor = Constant.progressColoar.cgColor progressLayer.lineWidth = Constant.lineWidth progressLayer.path = path.cgPath progressLayer.strokeStart = 0 progressLayer.strokeEnd = CGFloat(progress)/100.0 layer.addSublayer(progressLayer) //绘制进度条头部圆点 dot.frame = CGRect(x: 0, y: 0, width: Constant.lineWidth, height: Constant.lineWidth) let dotPath = UIBezierPath(ovalIn: CGRect(x: 0,y: 0, width: Constant.lineWidth, height: Constant.lineWidth)).cgPath let arc = CAShapeLayer() arc.lineWidth = 0 arc.path = dotPath arc.strokeStart = 0 arc.strokeEnd = 1 arc.strokeColor = Constant.progressColoar.cgColor arc.fillColor = Constant.progressColoar.cgColor arc.shadowColor = UIColor.black.cgColor arc.shadowRadius = 5.0 arc.shadowOpacity = 0.5 arc.shadowOffset = CGSize.zero dot.layer.addSublayer(arc) addSubview(dot) //圆点中添加箭头图标 arrow.frame.size = CGSize(width: Constant.lineWidth, height: Constant.lineWidth) dot.addSubview(arrow) //设置圆点位置 dot.layer.position = calcCircleCoordinateWithCenter(progressCenter, radius: radius, angle: CGFloat(-progress)/100*360+90) } //设置进度(可以设置是否播放动画) func setProgress(_ pro: Int,animated anim: Bool) { setProgress(pro, animated: anim, withDuration: 0.55) } //设置进度(可以设置是否播放动画,以及动画时间) func setProgress(_ pro: Int,animated anim: Bool, withDuration duration: Double) { let oldProgress = progress progress = pro //进度条动画 CATransaction.begin() CATransaction.setDisableActions(!anim) CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut)) CATransaction.setAnimationDuration(duration) progressLayer.strokeEnd = CGFloat(progress)/100.0 CATransaction.commit() //头部圆点动画 let startAngle = angleToRadian(360*Double(oldProgress)/100 - 90) let endAngle = angleToRadian(360*Double(progress)/100 - 90) let clockWise = progress > oldProgress ? false : true let path2 = CGMutablePath() path2.addArc(center: CGPoint(x:bounds.midX,y: bounds.midY), radius: bounds.size.width/2 - Constant.lineWidth, startAngle: startAngle, endAngle: endAngle, clockwise: clockWise, transform: transform) let orbit = CAKeyframeAnimation(keyPath:"position") orbit.duration = duration orbit.path = path2 orbit.calculationMode = kCAAnimationPaced orbit.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) orbit.rotationMode = kCAAnimationRotateAuto orbit.isRemovedOnCompletion = false orbit.fillMode = kCAFillModeForwards dot.layer.add(orbit,forKey:"Move") } //将角度转为弧度 fileprivate func angleToRadian(_ angle: Double)->CGFloat { return CGFloat(angle/Double(180.0) * M_PI) } //计算圆弧上点的坐标 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 CGPoint(x: center.x+x2, y: center.y-y2); } }
四、测试代码
import UIKit class ViewController: UIViewController { @IBOutlet weak var oProgressView1: OProgressView! @IBOutlet weak var oProgressView2: OProgressView2! @IBOutlet weak var oProgressView3: OProgressView3! override func viewDidLoad() { super.viewDidLoad() } //增加进度 @IBAction func addProgress(_ sender: AnyObject) { oProgressView1.setProgress(oProgressView1.progress + 25, animated: true) oProgressView2.setProgress(oProgressView2.progress + 25, animated: true) oProgressView3.setProgress(oProgressView3.progress + 25, animated: true) } //减少进度 @IBAction func minusProgress(_ sender: AnyObject) { oProgressView1.setProgress(oProgressView1.progress - 20, animated: true) oProgressView2.setProgress(oProgressView2.progress - 20, animated: true) oProgressView3.setProgress(oProgressView3.progress - 20, animated: true) } override func didReceiveMemoryWarning() { super.didReceiveMemoryWarning() } }源码下载:hangge_1076.zip
ok,自己研究了你的代码搞定了
这是不是没法加一半的阴影,加了需要旋转是吗?没看到你做过旋转的代码
为啥我升级到3.0,按这个会在(OProgressView3)的 dot.layer.add(orbit,forKey:"Move") 上出错