This is the code I’m using to animate my CAShapeLayer:
_progressBarLayer.strokeEnd = CGFloat(_progressToDrawForProgress(progress))
let progressAnimation = CABasicAnimation(keyPath: "strokeEnd")
progressAnimation.duration = CFTimeInterval(1.0)
progressAnimation.fromValue = CGFloat(self.progress)
progressAnimation.toValue = _progressBarLayer.strokeEnd
progressAnimation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseIn)
_progressBarLayer.addAnimation(progressAnimation, forKey: "progressAnimation")
I’ve tested using the delegate to see if the animation plays, and it does. Logging start and stop in the right place.
This code is in a setProgress(progress: CGFloat, animated: Bool) function and runs if animated is true.
Is there anything glaringly obvious here?
Long answer: It turns out that the animation wasn’t playing because of something being drawn above the CAShapeLayer using quartz, so what I thought was the CAShapeLayer (that should be animated) was actually a Quartz drawing of that same layer.
Short answer: Don’t draw to the graphics context with Quartz
Related
I have a circular CAShapeLayer where I'd like to on tap, change the stroke and fill colors. To do so, I added a CABasicAnimation, but to my confusion, the animation doesn't end to the set toValue. Instead, the animation, resets back to the original color.
I tried using the animationDidStop delegate method in setting the stroke and fill colors to the desired end color, which worked, but a flashing glitch of the original colors appeared.
Any guidance would be appreciated.
func createCircleShapeLayer() {
let layer = CAShapeLayer()
let circularPath = UIBezierPath(arcCenter: view.center, radius: 50, startAngle: 0, endAngle: 2 * CGFloat.pi, clockwise: true)
layer.path = circularPath.cgPath
layer.strokeColor = UIColor.red.cgColor
layer.lineWidth = 10
layer.fillColor = UIColor.clear.cgColor
layer.lineCap = CAShapeLayerLineCap.round
layer.frame = view.bounds
view.layer.addSublayer(layer)
}
func animateLayerOnTap() {
let strokeAnim = CABasicAnimation(keyPath: "strokeColor")
strokeAnim.toValue = UIColor.red.cgColor
strokeAnim.duration = 0.8
strokeAnim.repeatCount = 0
strokeAnim.autoreverses = false
let fillAnim = CABasicAnimation(keyPath: "fillColor")
fillAnim.toValue = UIColor.lightGray.cgColor
fillAnim.duration = 0.8
fillAnim.repeatCount = 0
fillAnim.autoreverses = false
layer.add(fillAnim, forKey: "fillColor")
layer.add(strokeAnim, forKey: "strokeColor")
}
UIView animation is pretty clean and simple to use. When you submit a block-based UIView animation, the system creates an animation that animates your view's properties to their end values, and they stay there. (Under the covers UIView animation adds one or more CAAnimations to the view's layer, and manages updating the view's properties to the final values once the animation is complete, but you don't need to care about those details.)
None of that is true with Core Animation. It's confusing, non-intuitive, and poorly documented. With Core animation, the animation adds a temporary "presentation layer" on top of your layer that creates the illusion that your properties are animating from their start to the their end state, but it is only an illusion.
By default, the presentation layer is removed once the animation is complete, and your layer seems to snap back to it's pre-animation state, as you've found. There are settings you can apply that cause the animation layer to stick around in it's final state once the animation is complete, but resist the urge to do that.
Instead, what you want to do is to set your layer's properties to their end state explicitly right after the animation starts. Making that more complicated, though, is the fact that a fair number of layer properties are "implicitly animated", meaning that if you change them, the system creates an animation that makes that change for you. (In that case, the change "sticks" once the implicit animation is complete.)
You are animating strokeColor and FillColor, which are both animatable. It would be easier to take advantage of the implicit animation of those layer properties. The tricky part there is that if you want to change any of the default values (like duration) for implicit animations, you have to enclose your changes to animatable properties between a call to CATransaction.begin() and CATransaction.commit(), and use calls to CATransaction to change those values.
Here is what your code to animate your layer's strokeColor and fillColor might look like using implicit animation and a CATransaction:
CATransaction.begin()
CATransaction.setAnimationDuration(0.8)
layer.strokeColor = UIColor.red.cgColor // Your strokeColor was already red?
layer.fillColor = UIColor.lightGray.cgColor
CATransaction.commit()
I would like to animate lineCap property of a CAShapeLayer.
Here is my code:
func animate() {
let animation = CABasicAnimation(keyPath: "lineCap")
animation.toValue = CAShapeLayerLineCap.round
animation.duration = 0.3
//var progressLayer: CAShapeLayer?
progressLayer?.add(animation, forKey: "AnimationKey")
}
Nothing happens. Probably the error is in keyPath, but a can't find proper value
Line cap is not animatable according to the documentation.
https://developer.apple.com/documentation/quartzcore/cashapelayer
Take a look at the documentation:
https://developer.apple.com/documentation/quartzcore/cashapelayer/1521905-linecap
Search for the word "animatable". You won't find it (at least, not with respect to this property). So your expectation that you can animate this property is wrong.
I want to make rotation of CAShapeLayer with spring effect (like in UIView.animateWithDuration(_:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:)) but on layer not on a view.
When the button is tapped its sublayer of main layer should rotate to 3*PI/4 and spring should bounce to 2*PI/3. Then, when button is tapped again, layer rotation should be done in reversed order than before: first bounce to 2*PI/3, then rotation to the initial position (before first rotation).
How I could do that? I cannot achieve it by UIView.animateWithDuration(_:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:) because layer's transform property is animatable by default.
I've tried changing CATransaction but it rotates only by one angle (without taking into consideration other rotation):
let rotation1 = CGAffineTransformRotate(CGAffineTransformIdentity, angle1)
let rotation2 = CGAffineTransformRotate(CGAffineTransformIdentity, angle2)
let transform = CGAffineTransformConcat(rotation1, rotation2)
CATransaction.begin()
CATransaction.setAnimationDuration(0.6)
self.plusLayer.setAffineTransform(transform)
CATransaction.commit()
Update
According to Duncan C post I try to use CASpringAnimation and I achive animation in one direction:
myLayer.setAffineTransform(CGAffineTransformMakeRotation(angle))
let spring = CASpringAnimation(keyPath: "transform.rotation")
spring.damping = 12.0
spring.fromValue = 2.0 * CGFloat(M_PI)
spring.toValue = 3.0 * CGFloat(M_PI_4)
spring.duration = 0.5
myLayer.addAnimation(spring, forKey: "rotation")
But how to reverse that animation on button tapped?
Thanks in advance for your help.
UIView block animation is for animating view properties. You could animate the button's transform (of type CGAffineTransform, a 2D transform) using UIView.animateWithDuration(_:delay:usingSpringWithDamping:initialSpringVelocity:options:animations:completion:
If you need to animate layer properties, though, you'll need to use Core Animation.
It seems Apple added (or made public) a spring CAAnimation in iOS 9. It doesn't seem to be in the Xcode docs however.
Check out this thread:
SpringWithDamping for CALayer animations?
I'm using a CAKeyFrameAnimation in a similar manner to how it's used on this page. I'm trying to have an action occur at the end of the animation, but I'm not sure how I can go about doing that. I looked through the CAKeyFrameAnimation docs and didn't see anything about a completionHandler or anything, and the only thing I can think to do is to set a timer for the animation length and handle everything after that. I figure there must be some better way to get notified that the animation has completed, but I haven't been able to find a better solution.
Swift
Use CATransaction.setCompletionBlock as below.
CATransaction.begin()
CATransaction.setCompletionBlock({
view.isHidden = true
})
let animation = CAKeyframeAnimation(keyPath: "position")
animation.path = path
view.layer.add(animation, forKey: "moveIn")
CATransaction.commit()
You didn't look up the inheritance chain. CAAnimation has a delegate property, and a delegate method, animationDidStop:finished:, that you can use to detect the end of an animation.
Before this code, my movie pic alpha is set to 0,
CABasicAnimation* fadein= [CABasicAnimation animationWithKeyPath:#"alpha"];
[fadein setToValue:[NSNumber numberWithFloat:1.0]];
[fadein setDuration:0.5];
[[moviepic layer]addAnimation:fadein forKey:#"alpha"];
Nothing happened, if I set alpha to 0.5 beforehand instead, the alpha remains at 0.5 and not animating to 1.
I've seen a code using UIView beginAnimations: around, but I'm teaching core animation so I wondered why CABasicAnimation can't do simple task like this?
[CABasicAnimation animationWithKeyPath:#"opacity"];
UIView exposes this as alpha where as CALayer exposes this as opacity.
For Swift:
let opacity = CABasicAnimation(keyPath: "opacity")
opacity.fromValue = fromValue
opacity.toValue = toValue
opacity.duration = duration
opacity.beginTime = CACurrentMediaTime() + beginTime //If a delay is needed
view.layer.add(opacity, forKey: nil)
If you want to keep the final alpha value, you have to set the current view controller as the delegate of the opacity animation:
opacity.delegate = self
And, in the delegate function animationDidStop, you should do:
extension ViewController: CAAnimationDelegate {
func animationDidStop(_ anim: CAAnimation, finished flag: Bool) {
view.alpha = toValue
}
}
#ohho answers the posted question. Mine will be a bit more generic. For a list what can and how be animated with CABasicAnimation please refer to Apple's documentation