Home > Mobile >  How to make circular loading animation color go from red to green as it loads
How to make circular loading animation color go from red to green as it loads

Time:12-09

I have implemented a circular loading animation exaclty as in this video:

https://www.youtube.com/watch?v=O3ltwjDJaMk

Here is my code:

class ViewController: UIViewController {

let shapeLayer = CAShapeLayer()

override func viewDidLoad() {
        super.viewDidLoad()
        
        let center = view.center
        let trackLayer = CAShapeLayer()
        
        let circularPath = UIBezierPath(arcCenter: center, radius: 100, startAngle: -CGFloat.pi / 2, endAngle: 2 * CGFloat.pi, clockwise: true)
        trackLayer.path = circularPath.cgPath
        trackLayer.strokeColor = UIColor.lightGray.cgColor
        trackLayer.lineWidth = 10
        trackLayer.fillColor = UIColor.clear.cgColor
        trackLayer.lineCap = kCALineCapRound
        view.layer.addSublayer(trackLayer)
        
        shapeLayer.path = circularPath.cgPath
        shapeLayer.strokeColor = UIColor.red.cgColor
        shapeLayer.lineWidth = 10
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.lineCap = kCALineCapRound
        shapeLayer.strokeEnd = 0
     
        view.layer.addSublayer(shapeLayer)
        
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(handleTap)))
    }
    
    @objc private func handleTap() {
 
        let basicAnimation = CABasicAnimation(keyPath: "strokeEnd")
        
        basicAnimation.toValue = 1
        basicAnimation.duration = 2
        
        basicAnimation.fillMode = kCAFillModeForwards
        basicAnimation.isRemovedOnCompletion = false
        
        shapeLayer.add(basicAnimation, forKey: "urSoBasic")
    }
}

When the animation happens i want the color of the circle to go from red towards green, if the circle is fully completed it should be green, if not then some color between red-green (depending on variable from the user-id). Any1 know how this could be done? Thankful for any help :)

CodePudding user response:

You can use CAAnimation to animate color as well. So you can follow that link.

The part that is missing for you is to compute the from and to colors which need to be interpolated based on current values.

Assuming that your progress view will at some point have updated interface to have

var minimumValue: CGFloat = 0.0
var maximumValue: CGFloat = 100.0
var currentValue: CGFloat = 30.0 // 30%

then with those values you can compute a current progress, a value between 0 and 1 which should define color interpolation scale. A basic math to compute it should be:

let progress = (currentValue-maximumValue)/(minimumValue-maximumValue) // TODO: handle division by zero. Handle current value out of bounds.

With progress you can now interpolate any numeric value by using

func interpolate(_ values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
    return values.from   (values.to - values.from)*scale
}

but a color is not a numeric value. In case of CAAnimation you should break it down to 4 numeric values as RGBA for consistency (at least I believe CAAnimation uses interpolation for color in RGBA space). Just as a note; in many cases it looks nicer when you interpolate in HSV space rather than RGB.

So interpolating a color should look something like this:

func rgba(_ color: UIColor) -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
    var r: CGFloat = 0.0
    var g: CGFloat = 0.0
    var b: CGFloat = 0.0
    var a: CGFloat = 0.0
    color.getRed(&r, green: &g, blue: &b, alpha: &a)
    return (r, g, b, a)
}
func interpolate(_ values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
    return values.from   (values.to - values.from)*scale
}
func interpolate(_ values: (from: UIColor, to: UIColor), scale: CGFloat) -> UIColor {
    let startColorComponents = rgba(values.from)
    let endColorComponents = rgba(values.to)
    return .init(red: interpolate((startColorComponents.r, endColorComponents.r), scale: scale),
                 green: interpolate((startColorComponents.g, endColorComponents.g), scale: scale),
                 blue: interpolate((startColorComponents.b, endColorComponents.b), scale: scale),
                 alpha: interpolate((startColorComponents.a, endColorComponents.a), scale: scale))
}

These should be all components that you need for your animation and I hope it will be enough to put you on the right track. I personally like to have more control plus I like to avoid tools such as CAAnimation and even shape layers. So this is how I would accomplish your task:

class ViewController: UIViewController {

    @IBOutlet private var progressView: ProgressView?
    
    override func viewDidLoad() {
        super.viewDidLoad()
     
        view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(onTap)))
    }
    
    @objc private func onTap() {
        guard let progressView = progressView else { return }
        progressView.animateValue(to: .random(in: progressView.minimumValue...progressView.maximumValue), duration: 0.3)
    }


}

@IBDesignable class ProgressView: UIView {
    
    @IBInspectable var minimumValue: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
    @IBInspectable var maximumValue: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
    @IBInspectable var value: CGFloat = 0.0 { didSet { setNeedsDisplay() } }
    @IBInspectable var colorAtZeroProgress: UIColor = .black { didSet { setNeedsDisplay() } }
    @IBInspectable var colorAtFullProgress: UIColor = .black { didSet { setNeedsDisplay() } }
    @IBInspectable var lineWidth: CGFloat = 10.0 { didSet { setNeedsDisplay() } }
    
    private var currentTimer: Timer?
    func animateValue(to: CGFloat, duration: TimeInterval) {
        currentTimer?.invalidate()
        let startTime = Date()
        let startValue = self.value
        currentTimer = Timer.scheduledTimer(withTimeInterval: 1.0/60.0, repeats: true, block: { timer in
            let progress = CGFloat(max(0.0, min(Date().timeIntervalSince(startTime)/duration, 1.0)))
            if progress >= 1.0 {
                // End of animation
                timer.invalidate()
            }
            self.value = startValue   (to - startValue)*progress
        })
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
         
        let progress = max(0.0, min((value-minimumValue)/(maximumValue-minimumValue), 1.0))
        
        let center = CGPoint(x: bounds.midX, y: bounds.midY)
        let radius = min(bounds.width, bounds.height)*0.5 - lineWidth*0.5
        let startAngle: CGFloat = -.pi*2.0
        let endAngle: CGFloat = startAngle   .pi*2.0*progress
        let circularPath = UIBezierPath(arcCenter: center, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
        
        
        
        let interpolatedColor: UIColor = {
            func rgba(_ color: UIColor) -> (r: CGFloat, g: CGFloat, b: CGFloat, a: CGFloat) {
                var r: CGFloat = 0.0
                var g: CGFloat = 0.0
                var b: CGFloat = 0.0
                var a: CGFloat = 0.0
                color.getRed(&r, green: &g, blue: &b, alpha: &a)
                return (r, g, b, a)
            }
            func interpolate(_ values: (from: CGFloat, to: CGFloat), scale: CGFloat) -> CGFloat {
                return values.from   (values.to - values.from)*scale
            }
            func interpolate(_ values: (from: UIColor, to: UIColor), scale: CGFloat) -> UIColor {
                let startColorComponents = rgba(values.from)
                let endColorComponents = rgba(values.to)
                return .init(red: interpolate((startColorComponents.r, endColorComponents.r), scale: scale),
                             green: interpolate((startColorComponents.g, endColorComponents.g), scale: scale),
                             blue: interpolate((startColorComponents.b, endColorComponents.b), scale: scale),
                             alpha: interpolate((startColorComponents.a, endColorComponents.a), scale: scale))
            }
            
            return interpolate((self.colorAtZeroProgress, self.colorAtFullProgress), scale: progress)
        }()
        
        interpolatedColor.setStroke()
        circularPath.lineCapStyle = .round
        circularPath.lineWidth = lineWidth
        circularPath.stroke()
    }
    
    
}

Feel free to use it and modify it anyway you please.

  • Related