CAShapeLayer 和 CABasicAnimation for circle - loader style - blinking

CAShapeLayer and CABasicAnimation for circle - loader style - blinking

我和 CAShapeLayer 在 playground 项目上玩得很开心。

我想创建一个以时钟方式为 CALayer 的笔触着色的动画,然后,一个移除笔触颜色的动画,以相同的方向。所以它给出了放置和删除笔划的效果。

到目前为止,它看起来很像想要的东西,只是在第二次调用 fullAnimate 之前,笔画像 "full" 一样闪烁,然后消失,然后是动画再次播放。

我承认我的代码目前还不是最好的,因为我只是在玩它,但我一直在寻找解释,但没有找到任何有用的答案。

有人可以向我解释一下那里发生了什么吗?以及如何避免呢?

这是我的 playground 文件

import UIKit
import PlaygroundSupport

enum CircleProgressionViewAnimationState {
    case start, firstAnimation, secondAnimation, progress, stop
}

class CircleProgressionView : UIView {
    static let offset: CGFloat = 10.0
    private var path : UIBezierPath? {
        didSet {
            circleLayer.path = path?.cgPath
        }
    }

    private var state : CircleProgressionViewAnimationState {
        didSet {
            observe(change: state)
        }
    }
    private var progressionPath : UIBezierPath?
    var circleLayerContainer = CALayer()

    var circleLayer : CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineWidth = 4
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = #colorLiteral(red: 0.6666666865, green: 0.6666666865, blue: 0.6666666865, alpha: 1)

        return shapeLayer
    }()

    var circleProgressLayer : CAShapeLayer = {
        let shapeLayer = CAShapeLayer()
        shapeLayer.lineWidth = 15
        shapeLayer.cornerRadius = 2
        shapeLayer.fillColor = UIColor.clear.cgColor
        shapeLayer.strokeColor = #colorLiteral(red: 0.2392156869, green: 0.6745098233, blue: 0.9686274529, alpha: 1)
        shapeLayer.lineCap = CAShapeLayerLineCap.round
        return shapeLayer
    }()

    override init(frame: CGRect) {
        state = .stop
        super.init(frame:frame)
        initLayerValues()
    }

    required init?(coder aDecoder: NSCoder) {
        state = .stop
        super.init(coder:aDecoder)
        initLayerValues()
    }



    private func initLayerValues() {
        let side = min(frame.width, frame.height)
        circleLayerContainer.frame = CGRect(x: 0, y: 0, width: side, height: side)
        let offset = CircleProgressionView.offset
        let bezierSide = side - (offset * 2)
        let bezierRect = CGRect(x:offset,
                                y:offset,
                                width: bezierSide,
                                height:bezierSide)
        path = UIBezierPath(roundedRect: bezierRect,
                            cornerRadius: CGFloat(bezierSide / 2))
        circleLayerContainer.addSublayer(circleLayer)
        layer.addSublayer(circleLayerContainer)
        layer.addSublayer(circleProgressLayer)
    }

    func setProgressionPath(_ progressionInPercent: CGFloat) {
        let progression = progressionInPercent / 100 * (360)
        let rad = (progression + 270) * CGFloat.pi / 180
        let start = 270 *  CGFloat.pi / 180
        let offset = CircleProgressionView.offset

        progressionPath = UIBezierPath(arcCenter: CGPoint(x: self.frame.size.width/2, y: self.frame.size.height/2),
                radius: (self.frame.size.height - offset - circleProgressLayer.lineWidth / 2) / 2,
                startAngle: start,
                endAngle: rad,
                clockwise: true)
    }

    func observe(change: CircleProgressionViewAnimationState) {
        print(change)
        switch change {
        case .firstAnimation:
             fullAnimate { self.state = .secondAnimation }
            break
       case .secondAnimation:
             emptyAnimate { self.state = .firstAnimation }
            break
        case .start, .progress, .stop: break
        }
    }

    func animate(loop: Bool) {

        var completion : ()->Void = {}
        if loop {
            state = .start
            completion = { self.state = .secondAnimation }
        }
        fullAnimate(completion: completion)
    }



    func fullAnimate(completion: @escaping ()->Void) {
        self.state = .progress
        circleProgressLayer.removeAllAnimations()
        circleProgressLayer.path
        CATransaction.begin()
         circleProgressLayer.path = progressionPath?.cgPath
        CATransaction.setCompletionBlock{ completion() }
        let animation : CABasicAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))

        animation.fromValue = 0.0
        animation.toValue = 1.0

        animation.duration = 2

       animation.timingFunction =  CAMediaTimingFunction(name:
            CAMediaTimingFunctionName.easeInEaseOut)

        circleProgressLayer.add(animation, forKey: #keyPath(CAShapeLayer.strokeEnd))
        CATransaction.commit()


    }

    func emptyAnimate(completion: @escaping ()->Void) {
        self.state = .progress
        circleProgressLayer.removeAllAnimations()
        CATransaction.begin()

        circleProgressLayer.path = progressionPath?.reversing().cgPath
        CATransaction.setCompletionBlock{ completion() }
        let animation : CABasicAnimation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.strokeEnd))

        animation.fromValue = 1.0
        animation.toValue = 0.0

        animation.duration = 2
        animation.timingFunction =  CAMediaTimingFunction(name:
            CAMediaTimingFunctionName.easeInEaseOut)

        circleProgressLayer.add(animation, forKey: #keyPath(CAShapeLayer.strokeEnd))
        CATransaction.commit()
    }
}

var container : UIView = {
    let frame = CGRect(x: 0, y: 0, width: 300, height: 300)
    let view = UIView(frame: frame)
    view.backgroundColor = #colorLiteral(red: 1, green: 1, blue: 1, alpha: 1)
    return view
}()

let circle = CircleProgressionView(frame: container.frame)
PlaygroundPage.current.liveView = container
circle.setProgressionPath(100)
container.addSubview(circle)
circle.animate(loop: true)

在空的动画块中添加下面两行,你就可以很容易地找出原因。

   func emptyAnimate(completion: @escaping ()->Void) {
    self.state = .progress
    circleProgressLayer.removeAllAnimations()
    CATransaction.begin()

  . .....
    animation.fillMode = .forwards
   animation.isRemovedOnCompletion = false

 ......

  }