当前位置: > > > Swift - 制作一个带动画效果的环形进度条

Swift - 制作一个带动画效果的环形进度条

(本文代码已升级至Swift3)

一、带动画效果的环形进度条

下面我们演示如何制作一个环状进度条组件,当进度改变时,进度条长度变化时是有动画效果的(我们还可以设置动画时间,或者关闭动画),效果图如下:

(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
评论3
  • 3楼
    2017-08-22 18:08
    CSXSky

    ok,自己研究了你的代码搞定了

    站长回复

    好的。

  • 2楼
    2017-08-22 17:45
    CSXSky

    这是不是没法加一半的阴影,加了需要旋转是吗?没看到你做过旋转的代码

    站长回复

    如果只要一半阴影的话可以在上面再加个东西将另一半给遮盖住。

  • 1楼
    2017-02-14 09:31
    does_n

    为啥我升级到3.0,按这个会在(OProgressView3)的 dot.layer.add(orbit,forKey:"Move") 上出错

    站长回复

    可能你哪里写错了,我把代码更新了,你可以再看下。