Animación del dibujo de un círculo

Estoy buscando una manera de animar el dibujo de un círculo. Pude crear el círculo, pero lo dibuja todo junto.

Aquí está mi clase 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); } } 

Y aquí es cómo lo agrego a la jerarquía de vista en mi controlador de vista:

 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) } 

¿Hay alguna manera de animar el dibujo del círculo durante más de 1 segundo?

Por ejemplo, en el medio de la animación se vería algo así como la línea azul en esta imagen:

animación parcial

La forma más fácil de hacerlo es utilizar el poder de la animación central para hacer la mayor parte del trabajo por usted. Para hacer eso, tendremos que mover el código del círculo de su función drawRect a un CAShapeLayer . Entonces, podemos usar una CABasicAnimation para animar la propiedad strokeEnd de 0.0 a 1.0 . strokeEnd es una gran parte de la magia aquí; de los documentos:

Combinado con la propiedad strokeStart, esta propiedad define la subregión de la ruta al trazo. El valor en esta propiedad indica el punto relativo a lo largo de la ruta en la que finalizar la ejecución, mientras que la propiedad strokeStart define el punto de inicio. Un valor de 0.0 representa el comienzo de la ruta, mientras que un valor de 1.0 representa el final de la ruta. Los valores intermedios se interpretan linealmente a lo largo de la ruta.

Si establecemos strokeEnd en 0.0 , no dibujará nada. Si lo configuramos en 1.0 , dibujará un círculo completo. Si lo configuramos en 0.5 , dibujará un medio círculo. etc.

Entonces, para empezar, CAShapeLayer un CAShapeLayer en la función init su CircleView y agregue esa capa a las sublayers la vista (también asegúrese de eliminar la función drawRect ya que la capa dibujará el círculo ahora):

 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: Estamos configurando circleLayer.strokeEnd = 0.0 para que el círculo no se circleLayer.strokeEnd = 0.0 inmediato.

Ahora, agreguemos una función que podemos llamar para activar la animación circular:

 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") } 

Entonces, todo lo que tenemos que hacer es cambiar su función addCircleView para que addCircleView la animación cuando agregue CircleView a su 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) } 

Todo lo que se junte debería verse más o menos así:

animación circular

Nota: No se repetirá así, se mantendrá en un círculo completo después de que se active.

Respuesta de Mikes actualizada para 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") } 

Para llamar a la función:

 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 respuesta de Mike es genial! Otra manera agradable y simple de hacerlo es usar drawRect combinado con setNeedsDisplay (). Parece perezoso, pero no es así 🙂 enter image description here

Queremos dibujar un círculo comenzando desde la parte superior, que es de -90 ° y termina a 270 °. El centro del círculo es (centro X, centro Y), con un radio dado. CurrentAngle es el ángulo actual del punto final del círculo, que va desde 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 

En drawRect, especificamos cómo se supone que se mostrará el círculo:

 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) } 

El problema es que en este momento, como el ángulo actual no está cambiando, el círculo es estático, y ni siquiera se muestra, como currentAngle = minAngle.

Luego creamos un temporizador, y cada vez que ese temporizador se dispara, aumentamos el ángulo actual. En la parte superior de tu clase, agrega el tiempo entre dos incendios:

 let timeBetweenDraw:CFTimeInterval = 0.01 

En tu init, agrega el temporizador:

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

Podemos agregar la función que se llamará cuando el temporizador se dispare:

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

Lamentablemente, al ejecutar la aplicación, no se visualiza nada porque no especificamos el sistema que debería dibujar nuevamente. Esto se hace llamando a setNeedsDisplay (). Aquí está la función de temporizador actualizada:

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

_ _ _

Todo el código que necesita se resume aquí:

 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) } } 

Si desea cambiar la velocidad, simplemente modifique la función updateTimer o la velocidad a la que se llama a esta función. Además, es posible que desee invalidar el temporizador una vez que el círculo esté completo, lo cual olvidé hacer 🙂

NB: para agregar el círculo en su guión gráfico, simplemente agregue una vista, selecciónela, vaya a su Inspector de identidad y, como Clase , especifique Circulo cerrado .

¡Aclamaciones! bRo

Si desea un controlador de finalización, esta es otra solución similar a la de Mike S, hecho en 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 el controlador de finalización, puede ejecutar la animación de nuevo llamando recursivamente a la misma función para volver a hacer la animación (que no se verá muy bien), o puede tener una función invertida que se encadenará continuamente hasta que se cumpla una condición , por ejemplo:

 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() } } 

Para que sea aún más elegante, puedes cambiar la dirección de la animación de esta manera:

  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() } }