Je cherche un moyen d'animer le dessin d'un cercle. J'ai pu créer le cercle, mais il rassemble tout.
Voici ma classe 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);
}
}
Et voici comment je l'ajoute à la hiérarchie des vues dans mon contrôleur de vue:
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)
}
Est-il possible d'animer le dessin du cercle en 1 seconde?
Exemple, à mi-chemin de l'animation, cela ressemblerait à la ligne bleue de cette image:
Pour ce faire, le moyen le plus simple consiste à utiliser la puissance de l'animation principale pour effectuer la majeure partie du travail à votre place. Pour ce faire, nous devrons déplacer votre code de dessin de cercle de votre fonction drawRect
vers un CAShapeLayer
. Ensuite, nous pouvons utiliser une CABasicAnimation
pour animer les propriétés de CAShapeLayer
strokeEnd
de 0.0
à 1.0
. strokeEnd
est une grande partie de la magie ici; des docs:
Combinée à la propriété strokeStart, cette propriété définit la sous-région du chemin d'accès au contour. La valeur de cette propriété indique le point relatif le long du chemin auquel terminer le tracé, tandis que la propriété strokeStart définit le point de départ. Une valeur de 0.0 représente le début du chemin, tandis qu'une valeur de 1.0 représente la fin du chemin. Les valeurs intermédiaires sont interprétées linéairement sur la longueur du trajet.
Si nous fixons strokeEnd
à 0.0
, ça ne tirera rien. Si on le met à 1.0
, ça va tracer un cercle complet. Si on le met à 0.5
, ça va tracer un demi cercle. etc.
Donc, pour commencer, créons un CAShapeLayer
dans la fonction CircleView
de votre init
et ajoutons cette couche à la vue sublayers
de la vue (veillez également à supprimer la fonction drawRect
puisque la couche dessinera le cercle maintenant):
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)
}
Remarque: Nous paramétrons circleLayer.strokeEnd = 0.0
afin que le cercle ne soit pas tracé immédiatement.
Ajoutons maintenant une fonction que nous pouvons appeler pour déclencher l'animation du cercle:
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 (i.e. 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")
}
Ensuite, tout ce que nous avons à faire est de changer votre fonction addCircleView
afin qu'elle déclenche l'animation lorsque vous ajoutez le CircleView
à sa 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)
}
Tout ce qui est mis ensemble devrait ressembler à ceci:
Note: Ça ne se répète pas comme ça, ça restera un cercle complet après l'animation.
La réponse de Mikes a été mise à jour pour 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 (i.e 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")
}
Pour appeler la fonction:
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 réponse de Mike est géniale! Un autre moyen simple et agréable de le faire consiste à utiliser drawRect en combinaison avec setNeedsDisplay (). Cela semble lent, mais ce n'est pas :-)
Nous voulons dessiner un cercle en partant du haut, qui est de -90 ° et se termine à 270 °. Le centre du cercle est (centreX, centreY), avec un rayon donné. CurrentAngle est l'angle actuel du point final du cercle, allant de minAngle (-90) à 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
Dans drawRect, nous précisons comment le cercle est censé afficher:
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)
}
Le problème est que pour le moment, currentAngle ne change pas, le cercle est statique et ne s'affiche même pas, comme currentAngle = minAngle.
Nous créons ensuite une minuterie et, chaque fois que cette minuterie se déclenche, nous augmentons currentAngle. Au sommet de votre classe, ajoutez le délai entre deux incendies:
let timeBetweenDraw:CFTimeInterval = 0.01
Dans votre init, ajoutez le timer:
NSTimer.scheduledTimerWithTimeInterval(timeBetweenDraw, target: self, selector: #selector(updateTimer), userInfo: nil, repeats: true)
Nous pouvons ajouter la fonction qui sera appelée lors du déclenchement de la minuterie:
func updateTimer() {
if currentAngle < maxAngle {
currentAngle += 1
}
}
Malheureusement, lors de l'exécution de l'application, rien ne s'affiche car nous n'avons pas spécifié le système sur lequel il devrait être dessiné à nouveau. Ceci est fait en appelant setNeedsDisplay (). Voici la fonction de minuterie mise à jour:
func updateTimer() {
if currentAngle < maxAngle {
currentAngle += 1
setNeedsDisplay()
}
}
_ _ _
Tout le code dont vous avez besoin est résumé ici:
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 vous souhaitez modifier la vitesse, modifiez simplement la fonction updateTimer ou la vitesse à laquelle cette fonction est appelée. Aussi, vous voudrez peut-être invalider le chronomètre une fois le cercle terminé, ce que j'ai oublié de faire :-)
NB: Pour ajouter le cercle dans votre storyboard, ajoutez simplement une vue, sélectionnez-le, accédez à son inspecteur d'identité et as Classe , spécifiez CircleClosing.
À votre santé! bRo
Si vous voulez un gestionnaire d’achèvement, c’est une autre solution similaire à celle de Mike S, réalisée dans 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()
}
Avec le gestionnaire d'achèvement, vous pouvez relancer l'animation en appelant récursivement la même fonction pour la refaire (ce qui ne semblera pas très agréable) ou vous pouvez avoir une fonction inversée qui sera enchaînée en continu jusqu'à ce qu'une condition soit remplie. , par exemple:
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()
}
}
Pour la rendre encore plus sophistiquée, vous pouvez changer le sens de l’animation comme ceci:
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()
}
}