Home > front end >  How to create Circle with step progress (gaps in it) and animate it?
How to create Circle with step progress (gaps in it) and animate it?

Time:12-05

I need to create a progressive with gaps in it and Animate the layers. I have achieved it. But the problem is it is starting (0) from Right centre. But the requirement is it should start from top centre. In image You can see that it is started from right side.

I have attached my code sample along with Image for your understanding. Can somebody help me where I'm doing wrong or how should I make it from top.

extension ViewController {
func sampleProgress() {
    let totalSteps = 6
    let frame = CGRect(x: 50, y: 50, width: 120, height: 120)
    let circlePath = UIBezierPath(ovalIn: frame)
    let gapSize: CGFloat = 0.0125
    let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
    var startAngle = 0.0
    let lineWidth = 8.0
    
    for index in 0 ... totalSteps {
        // Background layer
        let backgroundLayer = CAShapeLayer()
        backgroundLayer.strokeStart = startAngle
        backgroundLayer.strokeEnd = backgroundLayer.strokeStart   segmentAngle - gapSize
        
        backgroundLayer.path = circlePath.cgPath
        backgroundLayer.name = String(index)
        backgroundLayer.strokeColor = UIColor.lightGray.cgColor
        backgroundLayer.lineWidth = lineWidth
        backgroundLayer.lineCap = CAShapeLayerLineCap.butt
        backgroundLayer.fillColor = UIColor.clear.cgColor
        self.view.layer.addSublayer(backgroundLayer)
        
        // Foreground layer
        let foregroundLayer = CAShapeLayer()
        foregroundLayer.strokeStart = startAngle
        foregroundLayer.strokeEnd = backgroundLayer.strokeStart   segmentAngle - gapSize
        
        foregroundLayer.isHidden = true
        foregroundLayer.name = String(index)   String(index)
        foregroundLayer.path = circlePath.cgPath
        foregroundLayer.strokeColor = UIColor.green.cgColor
        foregroundLayer.lineWidth = lineWidth
        foregroundLayer.lineCap = CAShapeLayerLineCap.butt
        foregroundLayer.fillColor = UIColor.clear.cgColor
        self.view.layer.addSublayer(foregroundLayer)
        
        print("Start angle: \(startAngle)")
        startAngle = startAngle   segmentAngle
    }
}

func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
    let segmentAngle: CGFloat = (360 * 0.166) / 360
    let gapSize: CGFloat = 0.0125
    var startAngle = 0.0
    
    for index in 0 ... stepsToAnimate {
        if let foregroundLayers = self.view.layer.sublayers {
            for animateLayer in foregroundLayers {
                if animateLayer.name == String(index)   String(index) {
                    if index == stepsToAnimate && isAnimate {
                        let animation = CABasicAnimation(keyPath: "strokeEnd")
                        animation.fromValue = startAngle
                        animation.toValue = startAngle   segmentAngle - gapSize
                        animation.duration = 1.0
                        animateLayer.add(animation, forKey: "foregroundAnimation")
                        animateLayer.isHidden = false
                    } else {
                        animateLayer.isHidden = false
                    }
                    startAngle = startAngle   segmentAngle
                }
            }
        }
    }
}

}

Progress view

CodePudding user response:

You can "move the start" to the top by rotating the layer(s) minus 90-degrees:

let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)

I would assume this would be wrapped into a UIView subclass, but to get your example (adding sublayers to the main view's layer) to work right, we'll want to use a Zero-based origin for the path rect:

// use 0,0 for the origin of the PATH frame
let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
let circlePath = UIBezierPath(ovalIn: frame)

and then an offset rect for the position:

let layerFrame = frame.offsetBy(dx: 50, dy: 50)

and we set the .anchorPoint of the layers to the center of that rect -- so it will rotate around its center:

// set the layer's frame
backgroundLayer.frame = layerFrame
// set the layer's anchor point
backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
backgroundLayer.transform = tr
        
// set the layer's frame
foregroundLayer.frame = layerFrame
// set the layer's anchor point
foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
// apply the rotation transform
foregroundLayer.transform = tr
        

So, slight modifications to your code:

extension ViewController {
    func sampleProgress() {
        let totalSteps = 6
        
        // use 0,0 for the origin of the PATH frame
        let frame = CGRect(x: 0, y: 0, width: 120, height: 120)
        let circlePath = UIBezierPath(ovalIn: frame)
        
        // use this for the POSITION of the path
        let layerFrame = frame.offsetBy(dx: 50, dy: 50)
        
        let gapSize: CGFloat = 0.0125
        let segmentAngle: CGFloat = 0.167 // (1/totalSteps)
        var startAngle = 0.0
        let lineWidth = 8.0

        // we want to rotate the layer by -90 degrees
        let tr = CATransform3DMakeRotation(-(.pi * 0.5), 0, 0, 1)

        for index in 0 ... totalSteps {
            // Background layer
            let backgroundLayer = CAShapeLayer()
            backgroundLayer.strokeStart = startAngle
            backgroundLayer.strokeEnd = backgroundLayer.strokeStart   segmentAngle - gapSize
            
            backgroundLayer.path = circlePath.cgPath
            backgroundLayer.name = String(index)
            backgroundLayer.strokeColor = UIColor.lightGray.cgColor
            backgroundLayer.lineWidth = lineWidth
            backgroundLayer.lineCap = CAShapeLayerLineCap.butt
            backgroundLayer.fillColor = UIColor.clear.cgColor
            self.view.layer.addSublayer(backgroundLayer)

            // set the layer's frame
            backgroundLayer.frame = layerFrame
            // set the layer's anchor point
            backgroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
            // apply the rotation transform
            backgroundLayer.transform = tr
            
            // Foreground layer
            let foregroundLayer = CAShapeLayer()
            foregroundLayer.strokeStart = startAngle
            foregroundLayer.strokeEnd = backgroundLayer.strokeStart   segmentAngle - gapSize
            
            foregroundLayer.isHidden = true
            foregroundLayer.name = String(index)   String(index)
            foregroundLayer.path = circlePath.cgPath
            foregroundLayer.strokeColor = UIColor.green.cgColor
            foregroundLayer.lineWidth = lineWidth
            foregroundLayer.lineCap = CAShapeLayerLineCap.butt
            foregroundLayer.fillColor = UIColor.clear.cgColor
            self.view.layer.addSublayer(foregroundLayer)
            
            // set the layer's frame
            foregroundLayer.frame = layerFrame
            // set the layer's anchor point
            foregroundLayer.anchorPoint = CGPoint(x: 0.5, y: 0.5)
            // apply the rotation transform
            foregroundLayer.transform = tr
            
            print("Start angle: \(startAngle)")
            startAngle = startAngle   segmentAngle
        }
    }
    
    func animateLayer(isAnimate: Bool, stepsToAnimate: Int) {
        let segmentAngle: CGFloat = (360 * 0.166) / 360
        let gapSize: CGFloat = 0.0125
        var startAngle = 0.0
        
        for index in 0 ... stepsToAnimate {
            if let foregroundLayers = self.view.layer.sublayers {
                for animateLayer in foregroundLayers {
                    if animateLayer.name == String(index)   String(index) {
                        if index == stepsToAnimate && isAnimate {
                            let animation = CABasicAnimation(keyPath: "strokeEnd")
                            animation.fromValue = startAngle
                            animation.toValue = startAngle   segmentAngle - gapSize
                            animation.duration = 1.0
                            animateLayer.add(animation, forKey: "foregroundAnimation")
                            animateLayer.isHidden = false
                        } else {
                            animateLayer.isHidden = false
                        }
                        startAngle = startAngle   segmentAngle
                    }
                }
            }
        }
    }
}

and an example controller - each tap anywhere animates the next step:

class ViewController: UIViewController {
    
    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        sampleProgress()
    }
    
    var p: Int = 0
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        animateLayer(isAnimate: true, stepsToAnimate: p)
        p  = 1
    }
    
}
  • Related