aheze
aheze

Reputation: 30336

CALayer path animation flickers when stopping and starting again

I'm trying to stop a CABasicAnimation and then start it again. To stop the animation at its current value, I followed this answer which said to

get the presentationLayer for the animating layer, read the current value of the animated property, set that value to the animating layer, and only then remove the animation.

However, when I start the animation again, I get a weird flicker effect, like this:

Here's my code:

class ViewController: UIViewController {
    
    let shapeView = UIView(frame: CGRect(x: 50, y: 50, width: 100, height: 100))
    var pathLayer: CAShapeLayer?
    
    let startButton = UIButton(type: .system)
    let stopButton = UIButton(type: .system)
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        startButton.frame = CGRect(x: 50, y: 150, width: 80, height: 30)
        startButton.setTitle("Start", for: .normal)
        startButton.setTitleColor(UIColor.blue, for: .normal)
        view.addSubview(startButton)
        startButton.addTarget(self, action: #selector(start(_:)), for: .touchUpInside)
        
        stopButton.frame = CGRect(x: 200, y: 150, width: 80, height: 30)
        stopButton.setTitle("Stop", for: .normal)
        stopButton.setTitleColor(UIColor.blue, for: .normal)
        view.addSubview(stopButton)
        stopButton.addTarget(self, action: #selector(stop(_:)), for: .touchUpInside)
        
        view.addSubview(shapeView)
        shapeView.backgroundColor = UIColor.blue
        
        let pathLayer = CAShapeLayer()
        pathLayer.path = getPentagonPath()
        shapeView.layer.mask = pathLayer
        self.pathLayer = pathLayer
        
    }
    
    @objc func start(_ sender: UIButton!) {
        guard let pathLayer = pathLayer else { return }
        let newPath = getConcavePentagonPath()
        let animation = CABasicAnimation(keyPath: #keyPath(CAShapeLayer.path))
        animation.fromValue = pathLayer.path
        animation.toValue = newPath
        animation.duration = 3
        pathLayer.path = newPath
        pathLayer.add(animation, forKey: "path")
    }
    @objc func stop(_ sender: UIButton!) {
        guard let pathLayer = pathLayer else { return }
        if let currentValue = pathLayer.presentation()?.value(forKeyPath: #keyPath(CAShapeLayer.path)) { /// get the presentationLayer for the animating layer
            let currentPath = currentValue as! CGPath /// read the current value of the animated property
            pathLayer.path = currentPath /// set that value to the animating layer
            pathLayer.removeAllAnimations() /// remove the animation
        }
    }
    
    func getPentagonPath() -> CGPath {
        let pentagonPath = UIBezierPath()
        pentagonPath.move(to: CGPoint(x: 50, y: 0))
        pentagonPath.addLine(to: CGPoint(x: 97.55, y: 34.55))
        pentagonPath.addLine(to: CGPoint(x: 79.39, y: 90.45))
        pentagonPath.addLine(to: CGPoint(x: 20.61, y: 90.45))
        pentagonPath.addLine(to: CGPoint(x: 2.45, y: 34.55))
        pentagonPath.close()
        return pentagonPath.cgPath
    }
    func getConcavePentagonPath() -> CGPath {
        let pentagonPath = UIBezierPath()
        pentagonPath.move(to: CGPoint(x: 50, y: 50))
        pentagonPath.addLine(to: CGPoint(x: 97.55, y: 34.55))
        pentagonPath.addLine(to: CGPoint(x: 79.39, y: 90.45))
        pentagonPath.addLine(to: CGPoint(x: 20.61, y: 90.45))
        pentagonPath.addLine(to: CGPoint(x: 2.45, y: 34.55))
        pentagonPath.close()
        return pentagonPath.cgPath
    }
}

Upvotes: 0

Views: 576

Answers (1)

Nick Lockwood
Nick Lockwood

Reputation: 40995

While I'm not exactly sure why, setting animation.fillMode = .both or .backwards seems to solve the flicker problem. Per the documentation for this value:

The receiver clamps values before zero to zero when the animation is completed.

So I suspect what's happening is that when it is first attached, the animation is being shown at a time offset < 0 and by clamping it you ensure it shows a valid state at all times.

Upvotes: 1

Related Questions