Animate drawing of full circle continuously withou

2019-08-02 04:11发布

问题:

I am trying to have an animation in a view controller in which the circle rotates with animation. The circle should rotate until a process completed like a gif below. I have implemented the circle animation but couldn't reach to the point what I want to achieve.

import UIKit

class ViewController: UIViewController {

    var circle : Circle?;

    override func viewDidLoad() {
        super.viewDidLoad();

        view.backgroundColor = UIColor.white;

        setupViews();
    }

    func setupViews(){
        circle = Circle(frame: self.view.frame);

        view.addSubview(circle!);

        circle?.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true;
        circle?.topAnchor.constraint(equalTo: view.topAnchor).isActive = true;
        circle?.heightAnchor.constraint(equalTo: view.heightAnchor).isActive = true;
        circle?.widthAnchor.constraint(equalTo: view.widthAnchor).isActive = true;

    }

}

class Circle : UIView{


    override init(frame: CGRect) {
        super.init(frame: frame);

        self.backgroundColor = .blue;
        self.translatesAutoresizingMaskIntoConstraints = false;

        setupCircle();
    }

    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    let gradientLayer = CAGradientLayer();


    func setupCircle(){

        layer.addSublayer(shapeLayer);

        let circlePath = UIBezierPath(arcCenter: CGPoint(x: self.frame.width / 2 - 50, y: self.frame.height / 2 - 50), radius: 50, startAngle: CGFloat(Double.pi * (0 / 4)), endAngle: CGFloat(Double.pi * 2), clockwise: true);

        shapeLayer.path = circlePath.cgPath;

        let group = CAAnimationGroup()
        group.animations = [animateStrokeEnd, animateOpacity]
        group.duration = 0.8

        group.repeatCount = HUGE // repeat forver
        shapeLayer.add(group, forKey: nil)
    }

    let shapeLayer: CAShapeLayer = {
        let layer = CAShapeLayer();
        layer.strokeColor = UIColor.white.cgColor;
        layer.lineWidth = 5;
        layer.fillColor = UIColor.clear.cgColor;
        layer.strokeStart = 0
        layer.strokeEnd = 1;
        return layer;
    }();

    let animateOpacity : CABasicAnimation = {
        let animation = CABasicAnimation(keyPath: "opacity");
        animation.fromValue = 0;
        animation.toValue = 0.8;
        animation.byValue = 0.01;
        animation.repeatCount = Float.infinity;
        return animation
    }();

    let animateStrokeEnd: CABasicAnimation = {
        let animation = CABasicAnimation(keyPath: "strokeEnd");
        animation.fromValue  = 0;
        animation.repeatCount = Float.infinity;
        animation.toValue = 1;
        return animation;
    }();

}

I am using strokeEnd animation to implement the animation. And opacity to animate the color. But when the circle reaches 360 degree, its makes a lag before starting a new circle.

Does anybody know how to remove this effect and get smooth animation?

The above code produces this animation

But i want to achieve this animation

Also the stroke colour is different from the original animation. Can we achieve this animation using the CABasicAnimation?

回答1:

Rather than trying to animate the actual drawing, just draw the view once and then animate it.

Here is a custom PadlockView and a custom CircleView which mimic the animation you showed. To use it, add the code below to your project. Add a UIView to your Storyboard, change its class to PadlockView, and make an @IBOutlet to it (called padlock perhaps). When you want the view to animate, set padlock.circle.isAnimating = true. To stop animating, set padlock.circle.isAnimating = false.


CircleView.swift

// This UIView extension was borrowed from @keval's answer:
// https://stackoverflow.com/a/41160100/1630618
extension UIView {
    func rotate360Degrees(duration: CFTimeInterval = 3) {
        let rotateAnimation = CABasicAnimation(keyPath: "transform.rotation")
        rotateAnimation.fromValue = 0.0
        rotateAnimation.toValue = CGFloat.pi * 2
        rotateAnimation.isRemovedOnCompletion = false
        rotateAnimation.duration = duration
        rotateAnimation.repeatCount = Float.infinity
        self.layer.add(rotateAnimation, forKey: nil)
    }
}

class CircleView: UIView {

    var foregroundColor = UIColor.white
    var lineWidth: CGFloat = 3.0

    var isAnimating = false {
        didSet {
            if isAnimating {
                self.isHidden = false
                self.rotate360Degrees(duration: 1.0)
            } else {
                self.isHidden = true
                self.layer.removeAllAnimations()
            }
        }
    }

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    func setup() {
        self.isHidden = true
        self.backgroundColor = .clear
    }

    override func draw(_ rect: CGRect) {
        let width = bounds.width
        let height = bounds.height
        let radius = (min(width, height) - lineWidth) / 2.0

        var currentPoint = CGPoint(x: width / 2.0 + radius, y: height / 2.0)
        var priorAngle = CGFloat(360)

        for angle in stride(from: CGFloat(360), through: 0, by: -2) {
            let path = UIBezierPath()
            path.lineWidth = lineWidth

            path.move(to: currentPoint)
            currentPoint = CGPoint(x: width / 2.0 + cos(angle * .pi / 180.0) * radius, y: height / 2.0 + sin(angle * .pi / 180.0) * radius)
            path.addArc(withCenter: CGPoint(x: width / 2.0, y: height / 2.0), radius: radius, startAngle: priorAngle * .pi / 180.0 , endAngle: angle * .pi / 180.0, clockwise: false)
            priorAngle = angle

            foregroundColor.withAlphaComponent(angle/360.0).setStroke()
            path.stroke()
        }
    }

}

PadlockView.swift

class PadlockView: UIView {

    var circle: CircleView!

    override init(frame: CGRect) {
        super.init(frame: frame)
        setup()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        setup()
    }

    func setup() {
        self.backgroundColor = .clear

        circle = CircleView()
        circle.translatesAutoresizingMaskIntoConstraints = false
        self.addSubview(circle)
        circle.centerXAnchor.constraint(equalTo: self.centerXAnchor).isActive = true
        circle.centerYAnchor.constraint(equalTo: self.centerYAnchor).isActive = true
        circle.widthAnchor.constraint(equalTo: self.widthAnchor).isActive = true
        circle.heightAnchor.constraint(equalTo: self.heightAnchor).isActive = true
    }

    override func draw(_ rect: CGRect) {
        let width = bounds.width
        let height = bounds.height

        let lockwidth = width / 3
        let lockheight = height / 4

        let boltwidth = lockwidth * 2 / 3

        UIColor.white.setStroke()

        let path = UIBezierPath()
        path.move(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2))
        path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2))
        path.addLine(to: CGPoint(x: (width + lockwidth) / 2, y: height / 2 + lockheight))
        path.addLine(to: CGPoint(x: (width - lockwidth) / 2, y: height / 2 + lockheight))
        path.close()
        path.move(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2))
        path.addLine(to: CGPoint(x: (width - boltwidth) / 2, y: height / 2 - boltwidth / 4))
        path.addArc(withCenter: CGPoint(x: width/2, y: height / 2 - boltwidth / 4), radius: boltwidth / 2, startAngle: .pi, endAngle: 0, clockwise: true)
        path.lineWidth = 2.0
        path.stroke()
    }

}

Note: Continuous animation code courtesy of this answer.


Here is a demo that I setup with the following code in my ViewController:

@IBOutlet weak var padlock: PadlockView!

@IBAction func startStop(_ sender: UIButton) {
    if sender.currentTitle == "Start" {
        sender.setTitle("Stop", for: .normal)
        padlock.circle.isAnimating = true
    } else {
        sender.setTitle("Start", for: .normal)
        padlock.circle.isAnimating = false
    }
}