Animare il disegno di un cerchio

Sto cercando un modo per animare il disegno di un cerchio. Sono stato in grado di creare il cerchio, ma lo disegna tutto insieme.

Ecco la mia class CircleView :

 import UIKit class CircleView: UIView { override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.clearColor() } required init(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } override func drawRect(rect: CGRect) { // Get the Graphics Context var context = UIGraphicsGetCurrentContext(); // Set the circle outerline-width CGContextSetLineWidth(context, 5.0); // Set the circle outerline-colour UIColor.redColor().set() // Create Circle CGContextAddArc(context, (frame.size.width)/2, frame.size.height/2, (frame.size.width - 10)/2, 0.0, CGFloat(M_PI * 2.0), 1) // Draw CGContextStrokePath(context); } } 

Ed ecco come aggiungo alla gerarchia della vista nel mio controller di visualizzazione:

 func addCircleView() { let diceRoll = CGFloat(Int(arc4random_uniform(7))*50) var circleWidth = CGFloat(200) var circleHeight = circleWidth // Create a new CircleView var circleView = CircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight)) view.addSubview(circleView) } 

C’è un modo per animare il disegno del cerchio per 1 secondo?

Esempio, in parte attraverso l’animazione assomiglierebbe alla linea blu in questa immagine:

animazione parziale

    Il modo più semplice per farlo è usare la potenza dell’animazione principale per fare la maggior parte del lavoro per te. Per fare ciò, dovremo spostare il codice del disegno circolare dalla funzione drawRect a un CAShapeLayer . Quindi, possiamo usare CABasicAnimation per animare la proprietà strokeEnd da 0.0 a 1.0 . strokeEnd è una grande parte della magia qui; dai documenti:

    In combinazione con la proprietà strokeStart, questa proprietà definisce la sottoregione del percorso da tracciare. Il valore in questa proprietà indica il punto relativo lungo il percorso in cui terminare il tratto mentre la proprietà strokeStart definisce il punto iniziale. Un valore di 0.0 rappresenta l’inizio del percorso mentre un valore di 1.0 rappresenta la fine del percorso. I valori intermedi sono interpretati linearmente lungo la lunghezza del percorso.

    Se si imposta strokeEnd su 0.0 , non disegnerà nulla. Se lo impostiamo su 1.0 , disegnerà un cerchio completo. Se lo impostiamo a 0.5 , disegnerà un semicerchio. eccetera.

    Quindi, per iniziare, creiamo un CAShapeLayer nella funzione init CircleView e aggiungiamo quel livello ai sublayers della vista (assicurati anche di rimuovere la funzione drawRect poiché il livello ora disegnerà il cerchio):

     let circleLayer: CAShapeLayer! override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.clearColor() // Use UIBezierPath as an easy way to create the CGPath for the layer. // The path should be the entire circle. let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true) // Setup the CAShapeLayer with the path, colors, and line width circleLayer = CAShapeLayer() circleLayer.path = circlePath.CGPath circleLayer.fillColor = UIColor.clearColor().CGColor circleLayer.strokeColor = UIColor.redColor().CGColor circleLayer.lineWidth = 5.0; // Don't draw the circle initially circleLayer.strokeEnd = 0.0 // Add the circleLayer to the view's layer's sublayers layer.addSublayer(circleLayer) } 

    Nota: stiamo impostando circleLayer.strokeEnd = 0.0 modo che il cerchio non venga disegnato immediatamente.

    Ora, aggiungiamo una funzione che possiamo chiamare per triggersre l’animazione del cerchio:

     func animateCircle(duration: NSTimeInterval) { // We want to animate the strokeEnd property of the circleLayer let animation = CABasicAnimation(keyPath: "strokeEnd") // Set the animation duration appropriately animation.duration = duration // Animate from 0 (no circle) to 1 (full circle) animation.fromValue = 0 animation.toValue = 1 // Do a linear animation (ie the speed of the animation stays the same) animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) // Set the circleLayer's strokeEnd property to 1.0 now so that it's the // right value when the animation ends. circleLayer.strokeEnd = 1.0 // Do the actual animation circleLayer.addAnimation(animation, forKey: "animateCircle") } 

    Quindi, tutto ciò che dobbiamo fare è modificare la funzione addCircleView modo che addCircleView l’animazione quando aggiungi CircleView alla sua superview :

     func addCircleView() { let diceRoll = CGFloat(Int(arc4random_uniform(7))*50) var circleWidth = CGFloat(200) var circleHeight = circleWidth // Create a new CircleView var circleView = CircleView(frame: CGRectMake(diceRoll, 0, circleWidth, circleHeight)) view.addSubview(circleView) // Animate the drawing of the circle over the course of 1 second circleView.animateCircle(1.0) } 

    Tutto ciò che viene messo insieme dovrebbe assomigliare a questo:

    animazione del cerchio

    Nota: non si ripeterà così, rimarrà un cerchio completo dopo l’animazione.

    Risposta di Mikes aggiornata per Swift 3.0

     var circleLayer: CAShapeLayer! override init(frame: CGRect) { super.init(frame: frame) self.backgroundColor = UIColor.clear // Use UIBezierPath as an easy way to create the CGPath for the layer. // The path should be the entire circle. let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true) // Setup the CAShapeLayer with the path, colors, and line width circleLayer = CAShapeLayer() circleLayer.path = circlePath.cgPath circleLayer.fillColor = UIColor.clear.cgColor circleLayer.strokeColor = UIColor.red.cgColor circleLayer.lineWidth = 5.0; // Don't draw the circle initially circleLayer.strokeEnd = 0.0 // Add the circleLayer to the view's layer's sublayers layer.addSublayer(circleLayer) } required init?(coder aDecoder: NSCoder) { fatalError("init(coder:) has not been implemented") } func animateCircle(duration: TimeInterval) { // We want to animate the strokeEnd property of the circleLayer let animation = CABasicAnimation(keyPath: "strokeEnd") // Set the animation duration appropriately animation.duration = duration // Animate from 0 (no circle) to 1 (full circle) animation.fromValue = 0 animation.toValue = 1 // Do a linear animation (ie The speed of the animation stays the same) animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear) // Set the circleLayer's strokeEnd property to 1.0 now so that it's the // Right value when the animation ends circleLayer.strokeEnd = 1.0 // Do the actual animation circleLayer.add(animation, forKey: "animateCircle") } 

    Per chiamare la funzione:

     func addCircleView() { let diceRoll = CGFloat(Int(arc4random_uniform(7))*50) var circleWidth = CGFloat(200) var circleHeight = circleWidth // Create a new CircleView let circleView = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight)) //let test = CircleView(frame: CGRect(x: diceRoll, y: 0, width: circleWidth, height: circleHeight)) view.addSubview(circleView) // Animate the drawing of the circle over the course of 1 second circleView.animateCircle(duration: 1.0) } 

    La risposta di Mike è fantastica! Un altro modo semplice e carino è usare drawRect in combinazione con setNeedsDisplay (). Sembra lento, ma non è così 🙂 inserisci la descrizione dell'immagine qui

    Vogliamo disegnare un cerchio partendo dall’alto, che è -90 ° e termina a 270 °. Il centro del cerchio è (centroX, centroY), con un raggio dato. CurrentAngle è l’angolo corrente del punto finale del cerchio, che va da minAngle (-90) a maxAngle (270).

     // MARK: Properties let centerX:CGFloat = 55 let centerY:CGFloat = 55 let radius:CGFloat = 50 var currentAngle:Float = -90 let minAngle:Float = -90 let maxAngle:Float = 270 

    In drawRect, specifichiamo come deve essere visualizzato il cerchio:

     override func drawRect(rect: CGRect) { let context = UIGraphicsGetCurrentContext() let path = CGPathCreateMutable() CGPathAddArc(path, nil, centerX, centerY, radius, CGFloat(GLKMathDegreesToRadians(minAngle)), CGFloat(GLKMathDegreesToRadians(currentAngle)), false) CGContextAddPath(context, path) CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor) CGContextSetLineWidth(context, 3) CGContextStrokePath(context) } 

    Il problema è che adesso, dato che currentAngle non sta cambiando, il cerchio è statico e non mostra nemmeno, come currentAngle = minAngle.

    Creiamo quindi un timer e ogni volta che il timer scatta, aumentiamo currentAngle. Nella parte superiore della tua class, aggiungi il tempo tra due fuochi:

     let timeBetweenDraw:CFTimeInterval = 0.01 

    Nel tuo init, aggiungi il timer:

     NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) 

    Possiamo aggiungere la funzione che verrà chiamata quando scatta il timer:

     func updateTimer() { if currentAngle < maxAngle { currentAngle += 1 } } 

    Purtroppo, quando si esegue l'app, non viene visualizzato nulla perché non è stato specificato il sistema che dovrebbe disegnare di nuovo. Questo viene fatto chiamando setNeedsDisplay (). Ecco la funzione del timer aggiornata:

     func updateTimer() { if currentAngle < maxAngle { currentAngle += 1 setNeedsDisplay() } } 

    _ _ _

    Tutto il codice che ti serve è riassunto qui:

     import UIKit import GLKit class CircleClosing: UIView { // MARK: Properties let centerX:CGFloat = 55 let centerY:CGFloat = 55 let radius:CGFloat = 50 var currentAngle:Float = -90 let timeBetweenDraw:CFTimeInterval = 0.01 // MARK: Init required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) setup() } override init(frame: CGRect) { super.init(frame: frame) setup() } func setup() { self.backgroundColor = UIColor.clearColor() NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true) } // MARK: Drawing func updateTimer() { if currentAngle < 270 { currentAngle += 1 setNeedsDisplay() } } override func drawRect(rect: CGRect) { let context = UIGraphicsGetCurrentContext() let path = CGPathCreateMutable() CGPathAddArc(path, nil, centerX, centerY, radius, -CGFloat(M_PI/2), CGFloat(GLKMathDegreesToRadians(currentAngle)), false) CGContextAddPath(context, path) CGContextSetStrokeColorWithColor(context, UIColor.blueColor().CGColor) CGContextSetLineWidth(context, 3) CGContextStrokePath(context) } } 

    Se si desidera modificare la velocità, è sufficiente modificare la funzione updateTimer o la velocità con cui viene chiamata questa funzione. Inoltre, potresti voler invalidare il timer una volta completato il cerchio, cosa che ho dimenticato di fare 🙂

    NB: per aggiungere il cerchio nello storyboard, aggiungi una vista, selezionala, vai a Identity Inspector e, come class , specifica CircleClosing .

    Saluti! BRO

    Se si desidera un gestore di completamento, questa è un’altra soluzione simile a quella di Mike S, eseguita in Swift 3.0

     func animateCircleFull(duration: TimeInterval) { CATransaction.begin() let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration animation.fromValue = 0 animation.toValue = 1 animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) circleLayer.strokeEnd = 1.0 CATransaction.setCompletionBlock { print("animation complete") } // Do the actual animation circleLayer.add(animation, forKey: "animateCircle") CATransaction.commit() } 

    Con il gestore di completamento, è ansible eseguire nuovamente l’animazione chiamando ricorsivamente la stessa funzione per ripetere l’animazione (che non apparirà molto piacevole), oppure è ansible avere una funzione inversa che continuerà a concatenarsi finché non viene soddisfatta una condizione , per esempio:

     func animate(duration: TimeInterval){ self.isAnimating = true self.animateCircleFull(duration: 1) } func endAnimate(){ self.isAnimating = false } func animateCircleFull(duration: TimeInterval) { if self.isAnimating{ CATransaction.begin() let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration animation.fromValue = 0 animation.toValue = 1 animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) circleLayer.strokeEnd = 1.0 CATransaction.setCompletionBlock { self.animateCircleEmpty(duration: duration) } // Do the actual animation circleLayer.add(animation, forKey: "animateCircle") CATransaction.commit() } } func animateCircleEmpty(duration: TimeInterval){ if self.isAnimating{ CATransaction.begin() let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration animation.fromValue = 1 animation.toValue = 0 animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) circleLayer.strokeEnd = 0 CATransaction.setCompletionBlock { self.animateCircleFull(duration: duration) } // Do the actual animation circleLayer.add(animation, forKey: "animateCircle") CATransaction.commit() } } 

    Per renderlo ancora più elaborato, puoi cambiare la direzione dell’animazione in questo modo:

      func setCircleClockwise(){ let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: true) self.circleLayer.removeFromSuperlayer() self.circleLayer = formatCirle(circlePath: circlePath) self.layer.addSublayer(self.circleLayer) } func setCircleCounterClockwise(){ let circlePath = UIBezierPath(arcCenter: CGPoint(x: frame.size.width / 2.0, y: frame.size.height / 2.0), radius: (frame.size.width - 10)/2, startAngle: 0.0, endAngle: CGFloat(M_PI * 2.0), clockwise: false) self.circleLayer.removeFromSuperlayer() self.circleLayer = formatCirle(circlePath: circlePath) self.layer.addSublayer(self.circleLayer) } func formatCirle(circlePath: UIBezierPath) -> CAShapeLayer{ let circleShape = CAShapeLayer() circleShape.path = circlePath.cgPath circleShape.fillColor = UIColor.clear.cgColor circleShape.strokeColor = UIColor.red.cgColor circleShape.lineWidth = 10.0; circleShape.strokeEnd = 0.0 return circleShape } func animate(duration: TimeInterval){ self.isAnimating = true self.animateCircleFull(duration: 1) } func endAnimate(){ self.isAnimating = false } func animateCircleFull(duration: TimeInterval) { if self.isAnimating{ CATransaction.begin() let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration animation.fromValue = 0 animation.toValue = 1 animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) circleLayer.strokeEnd = 1.0 CATransaction.setCompletionBlock { self.setCircleCounterClockwise() self.animateCircleEmpty(duration: duration) } // Do the actual animation circleLayer.add(animation, forKey: "animateCircle") CATransaction.commit() } } func animateCircleEmpty(duration: TimeInterval){ if self.isAnimating{ CATransaction.begin() let animation = CABasicAnimation(keyPath: "strokeEnd") animation.duration = duration animation.fromValue = 1 animation.toValue = 0 animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseInEaseOut) circleLayer.strokeEnd = 0 CATransaction.setCompletionBlock { self.setCircleClockwise() self.animateCircleFull(duration: duration) } // Do the actual animation circleLayer.add(animation, forKey: "animateCircle") CATransaction.commit() } }