Reputation: 30336
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
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