Анимационный рисунок круга

Я ищу способ анимации рисования круга. Мне удалось создать круг, но он объединяет все это.

Вот мой класс 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);
  }
}

И вот как я добавляю его в иерархию представлений в контроллере представления:

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

Есть ли способ оживить рисование круга за 1 секунду?

Пример: частично через анимацию он будет выглядеть как синяя линия на этом изображении:

partial animation

Ответ 1

Самый простой способ сделать это - использовать силу основной анимации, чтобы выполнить большую часть работы для вас. Для этого нам нужно будет переместить код рисования круга из вашей функции drawRect в CAShapeLayer. Затем мы можем использовать CABasicAnimation для анимации CAShapeLayer strokeEnd свойства 0.0 до 1.0. strokeEnd - большая часть магии; из документов:

В сочетании с свойством strokeStart это свойство определяет субрегион пути к инсульту. Значение в этом свойстве указывает относительная точка вдоль пути, по которой можно закончить поглаживание Свойство strokeStart определяет начальную точку. Значение 0.0 представляет начало пути, в то время как значение 1.0 представляет конец пути. Значения между ними интерпретируются линейно вдоль длина пути.

Если мы установим strokeEnd в 0.0, он ничего не рисует. Если мы установим его на 1.0, он будет рисовать полный круг. Если мы установим его на 0.5, он нарисует половину круга. и др.

Итак, чтобы начать, создайте CAShapeLayer в вашей функции CircleView init и добавьте этот слой в представление sublayers (также обязательно удалите функцию drawRect, так как слой будет рисовать теперь круг):

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 layer sublayers
    layer.addSublayer(circleLayer)
}

Примечание: Мы устанавливаем circleLayer.strokeEnd = 0.0 так, чтобы круг сразу не рисовался.

Теперь добавим функцию, которую мы можем вызвать, чтобы вызвать анимацию круга:

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 strokeEnd property to 1.0 now so that it the
    // right value when the animation ends.
    circleLayer.strokeEnd = 1.0

    // Do the actual animation
    circleLayer.addAnimation(animation, forKey: "animateCircle")
}

Тогда все, что нам нужно сделать, это изменить вашу функцию addCircleView так, чтобы она запускала анимацию, когда вы добавляете CircleView в ее 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)
}

Все, что собралось, должно выглядеть примерно так:

circle animation

Примечание: Он не будет повторяться так, он будет оставаться полным кругом после его оживления.

Ответ 2

Ответы на майки обновлены для 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 layer 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 strokeEnd property to 1.0 now so that it the
    // Right value when the animation ends
    circleLayer.strokeEnd = 1.0

    // Do the actual animation
    circleLayer.add(animation, forKey: "animateCircle")
}

Чтобы вызвать функцию:

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

Ответ 3

Майк ответ велик! Другой приятный и простой способ сделать это - использовать drawRect в сочетании с setNeedsDisplay(). Кажется, лагги, но его нет:-) введите описание изображения здесь

Мы хотим нарисовать круг, начиная с вершины, который составляет -90 ° и заканчивается на 270 °. Центр окружности (центр X, центр Y) с заданным радиусом. CurrentAngle - текущий угол конечной точки круга, идущий от 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

В drawRect мы указываем, как должен отображаться круг:

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

Проблема в том, что прямо сейчас, поскольку currentAngle не изменяется, круг является статическим и даже не показывает, как currentAngle = minAngle.

Затем мы создаем таймер, и всякий раз, когда срабатывает этот таймер, мы увеличиваем currentAngle. В верхней части вашего класса добавьте время между двумя огнями:

let timeBetweenDraw:CFTimeInterval = 0.01

В вашем init добавьте таймер:

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

Мы можем добавить функцию, которая будет вызываться при срабатывании таймера:

func updateTimer() {

    if currentAngle < maxAngle {
        currentAngle += 1
    }
}

К сожалению, при запуске приложения ничего не отображается, потому что мы не указали систему, которую он должен нарисовать снова. Это делается путем вызова setNeedsDisplay(). Вот обновленная функция таймера:

func updateTimer() {

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

_ _ _

Весь код, который вам нужен, суммируется здесь:

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

Если вы хотите изменить скорость, просто измените функцию updateTimer или скорость, с которой эта функция вызывается. Кроме того, вам может потребоваться аннулировать таймер после завершения круга, который я забыл сделать: -)

Примечание. Чтобы добавить круг в раскадровку, просто добавьте представление, выберите его, перейдите в его Идентификационный инспектор и как Класс, укажите CircleClosing.

Ура! BRO

Ответ 4

Если вы хотите обработчик завершения, это еще одно решение, аналогичное тому, которое Mike S выполнил в 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()
}

С обработчиком завершения вы можете снова запустить анимацию либо путем рекурсивного вызова одной и той же функции для повторной анимации (что не будет выглядеть очень красиво), либо вы можете иметь обратную функцию, которая будет непрерывно связываться до тех пор, пока условие выполняется, например:

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

Чтобы сделать его еще более привлекательным, вы можете изменить направление анимации следующим образом:

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