CAKeyframeAnimation of "transform" is not animated alongside "opacity" in a CAAnimationGroup - ios

What would be the reason for CAKeyframeAnimation objects to not be able to animate simultaneously in a CAAnimationGroup, but it does if being added individually to a CALayer?
A code snippet to describe this:
// The following is a part of CALayer extension function
let quadAnimation = CAKeyframeAnimation(keyPath: "transform")
quadAnimation.duration = 1.0
quadAnimation.repeatCount = 1
quadAnimation.removedOnCompletion = false
quadAnimation.fillMode = kCAFillModeForwards
quadAnimation.beginTime = CACurrentMediaTime()
quadAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.743, 0, 0.833, 0.833)
// Note:
// AGK-based methods come from a geometry manipulation library https://github.com/agens-no/AGGeometryKit
//
var values: [NSValue] = []
let numberOfFrames = 30
for frame in 0..<numberOfFrames {
let p = AGKEaseOutWithBezier(CGFloat(frame) / CGFloat(numberOfFrames))
let quad = AGKQuadInterpolate(quad1, quad2, CGFloat(p))
let innerQuad = AGKQuadMove(quad, -self.position.x, -self.position.y)
let transform = CATransform3DWithAGKQuadFromBounds(innerQuad, CGRect(origin: CGPoint.zero, size: self.boundsSize))
let value = NSValue(CATransform3D: transform)
values += [value]
}
quadAnimation.values = values
let opacityAnimation = CAKeyframeAnimation(keyPath: "opacity")
opacityAnimation.keyTimes = [0, 1]
opacityAnimation.values = [0, 1]
opacityAnimation.timingFunction = CAMediaTimingFunction(controlPoints: 0.167, 0.167, 0.833, 0.833)
opacityAnimation.duration = 0.5
opacityAnimation.beginTime = CACurrentMediaTime() + 0.5
opacityAnimation.fillMode = kCAFillModeForwards
let animationBlockDelegate = AGKCALayerAnimationBlockDelegate()
animationBlockDelegate.onStart = {
self.quadrilateral = quad2
}
let animationGroup = CAAnimationGroup()
animationGroup.animations = [quadAnimation, opacityAnimation]
animationGroup.duration = quadAnimation.duration + opacityAnimation.duration + 0.5
animationGroup.delegate = animationBlockDelegate
CATransaction.begin()
CATransaction.setCompletionBlock {
// Completion block handler
}
self.addAnimation(animationGroup, forKey: "transformOpacityAnimationKey")
CATransaction.commit()
The opacity will be animated given the right timingFunction and beginTime, but the CA3DTransform values are not animated at all.
All of the above works fine if I added the animation individually to the layer:
self.addAnimation(quadAnimation, forKey:"quadAnimation")
self.addAnimation(opacityAnimation, forKey: "opacityAnimation")
With each animation quadAnimation and opacityAnimation process delegated to AGKCALayerAnimationBlockDelegate()

Related

Animate shadow properties

I want to animate shadow properties for a view. Initially I tried:
func animateIn(delay: TimeInterval = 0) {
UIView.animate(withDuration: 0.2, delay: delay, options: .allowUserInteraction, animations: {
self.shadowView.layer.shadowColor = UIColor.black.cgColor
self.shadowView.layer.shadowOffset = CGSize(width: 15, height: 15)
self.shadowView.layer.shadowRadius = 20
self.shadowView.layer.shadowOpacity = 0.2
self.layoutIfNeeded()
}, completion: nil)
}
which doesnt work given that UIView.animate doesnt work with layers.
I tested the following post (How to animate layer shadowOpacity?), but it only specifies how to animate the shadow Opacity. Is there any way to animate all my shadow properties like I would with an animation blocl?
You are very close, you just create multiple CABasicAnimations for all the property changes and then you can use CAAnimationGroup
So let say you have 2 CABasicAnimation animations like:
let shadownOpacityAnimation = CABasicAnimation(keyPath: "shadowOpacity")
...
let shadowRadiusAnimation = CABasicAnimation(keyPath: "shadowRadius")
...
Then you can add them using CAAnimationGroup
let group = CAAnimationGroup()
group.duration = 0.2
group.repeatCount = 1
group.autoreverses = false
group.timingFunction = CAMediaTimingFunction(name: .easeInEaseOut)
group.animations = [shadownOpacityAnimation, shadowRadiusAnimation]
layer.add(group, forKey: "allAnimations")
You can read more about it in this answer:
How can I create an CABasicAnimation for multiple properties?

How to animate the string of a CATextLayer?

I am currently overlaying some text in a video using the AVMutableComposition, I would like to change the string repeatedly based on some time interval. I am trying to use the Core Animation to achieve this; however, the string property does not seem to be animatable. Is there any other way to achieve the goal? Thanks.
Code (not working):
func getSubtitlesAnimation(withFrames frames: [String], duration: CFTimeInterval)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"string")
animation.calculationMode = kCAAnimationDiscrete
animation.duration = duration
animation.values = frames
animation.keyTimes = [0,0.5,1]
animation.repeatCount = Float(frames.count)
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeForwards
animation.beginTime = AVCoreAnimationBeginTimeAtZero
return animation
}
String is not an animatable path on a CATextLayer. Here is the list of keyPaths that you can use on any CALayer.
Some other important things about using Core Animation with AVFoundation.
All animations have to have removedCompletion of false.
You can only apply one key path animation to a layer once. Meaning if you want to add an opacity of fade in and fade out it needs to be combined into a single keyframe animation. In core animation you could use beginTime and apply two different opacity animations but in my experience with AVFoundation and Core Animation this does not work. Because of this you will have to figure out the key times and values for what you would want to occur if you wanted to use two different (ex opacity) animations on the same keypath for the CALayer.
Your discrete curve will still work but you will have to work out the keytimes and values.
Because string is not available as an animatable property the next best thing would be to use multiple CATextLayers and fade them in one after the other. Here is an example. Replace CACurrentMediaTime() with AVCoreAnimationBeginTimeAtZero for use with AVFoundation. This was just an example so you could visualize what you want.
Example1Using-Discrete
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//center the frame
let textFrame = CGRect(x: (self.view.bounds.width - 200)/2, y: (self.view.bounds.height - 100)/2, width: 200, height: 50)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
//animation spacing could be a negative value of half the animation to appear to fade between strings
self.animateText(subtitles: ["Hello","Good Morning","Good Afternoon","Good Evening","Goodnight","Goodbye"], duration: 2, animationSpacing: 0, frame: textFrame, targetLayer: self.view.layer)
}
}
func animateText(subtitles:[String],duration:Double,animationSpacing:Double,frame:CGRect,targetLayer:CALayer){
var currentTime : Double = 0
for x in 0..<subtitles.count{
let string = subtitles[x]
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = string
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 20.0
textLayer.alignmentMode = kCAAlignmentCenter
let anim = getSubtitlesAnimation(duration: duration, startTime: currentTime)
targetLayer.addSublayer(textLayer)
textLayer.add(anim, forKey: "opacityLayer\(x)")
currentTime += duration + animationSpacing
}
}
func getSubtitlesAnimation(duration: CFTimeInterval,startTime:Double)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"opacity")
animation.duration = duration
animation.calculationMode = kCAAnimationDiscrete
//have to fade in and out with a single animation because AVFoundation
//won't allow you to animate the same propery on the same layer with
//two different animations
animation.values = [0,1,1,0,0]
animation.keyTimes = [0,0.001,0.99,0.999,1]
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeBoth
//Replace with AVCoreAnimationBeginTimeAtZero for AVFoundation
animation.beginTime = CACurrentMediaTime() + startTime
return animation
}
}
EXAMPLE2-Using a long fade-Gif Attached
import UIKit
class ViewController: UIViewController {
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
//center the frame
let textFrame = CGRect(x: (self.view.bounds.width - 200)/2, y: (self.view.bounds.height - 100)/2, width: 200, height: 50)
DispatchQueue.main.asyncAfter(deadline: DispatchTime.now()) {
//animation spacing could be a negative value of half the animation to appear to fade between strings
self.animateText(subtitles: ["Hello","Good Morning","Good Afternoon","Good Evening","Goodnight","Goodbye"], duration: 4, animationSpacing: -2, frame: textFrame, targetLayer: self.view.layer)
}
}
func animateText(subtitles:[String],duration:Double,animationSpacing:Double,frame:CGRect,targetLayer:CALayer){
var currentTime : Double = 0
for x in 0..<subtitles.count{
let string = subtitles[x]
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = string
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 20.0
textLayer.alignmentMode = kCAAlignmentCenter
let anim = getSubtitlesAnimation(duration: duration, startTime: currentTime)
targetLayer.addSublayer(textLayer)
textLayer.add(anim, forKey: "opacityLayer\(x)")
currentTime += duration + animationSpacing
}
}
func getSubtitlesAnimation(duration: CFTimeInterval,startTime:Double)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"opacity")
animation.duration = duration
//have to fade in and out with a single animation because AVFoundation
//won't allow you to animate the same propery on the same layer with
//two different animations
animation.values = [0,0.5,1,0.5,0]
animation.keyTimes = [0,0.25,0.5,0.75,1]
animation.isRemovedOnCompletion = false
animation.fillMode = kCAFillModeBoth
//Replace with AVCoreAnimationBeginTimeAtZero for AVFoundation
animation.beginTime = CACurrentMediaTime() + startTime
return animation
}
}
RESULT:
Duration of 2 seconds in and 2 seconds out. Could be immediate.
Solution for text animation using CATextLayer and CAKeyframeAnimation. Right now the string property is animate-able. Idk how was it in the past.
func addTextLayer(to layer: CALayer) {
let myAnimation = CAKeyframeAnimation(keyPath: "string");
myAnimation.beginTime = 0;
myAnimation.duration = 1.0;
myAnimation.values = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"]
myAnimation.fillMode = CAMediaTimingFillMode.forwards;
myAnimation.isRemovedOnCompletion = false;
myAnimation.repeatCount = 1;
let textLayer = CATextLayer();
textLayer.frame = CGRect(x: 200, y: 300, width: 100, height: 100);
textLayer.string = "0";
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 40.0
textLayer.alignmentMode = .center
textLayer.add(myAnimation, forKey: nil);
layer.addSublayer(textLayer);
}
Solution for Swift 5:
like the answer from #agibson007
optimized for a duration of 0.25, faster wasn´t possible. (problems with fading / flicker)
func animateText(subtitles:[String],duration:Double,animationSpacing:Double,frame:CGRect,targetLayer:CALayer){
var currentTime : Double = 0
for x in 0..<subtitles.count{
let textLayer = CATextLayer()
textLayer.frame = frame
textLayer.string = subtitles[x]
textLayer.font = UIFont.systemFont(ofSize: 20)
textLayer.foregroundColor = UIColor.black.cgColor
textLayer.fontSize = 40.0
textLayer.alignmentMode = .center
let anim = getSubtitlesAnimation(duration: duration, startTime: currentTime)
textLayer.add(anim, forKey: "opacityLayer\(x)")
targetLayer.addSublayer(textLayer)
currentTime += duration + animationSpacing
}
}
func getSubtitlesAnimation(duration: CFTimeInterval,startTime:Double)->CAKeyframeAnimation {
let animation = CAKeyframeAnimation(keyPath:"opacity")
animation.duration = duration
animation.calculationMode = .discrete
animation.values = [0,1,1,0,0]
animation.keyTimes = [0,0.00000001,0.999,0.999995,1]
animation.isRemovedOnCompletion = false
animation.fillMode = .both
animation.beginTime = AVCoreAnimationBeginTimeAtZero + startTime // CACurrentMediaTime() <- NO AV Foundation
return animation
}
let steps = 0.25
let duration = 8.0
let textFrame = CGRect( x: (videoSize.width / 2) - (90 / 2) , y: videoSize.height * 0.2, width: 90, height: 50)
var subtitles:[String] = []
for i in 0...Int(duration / steps) {
let time = i > 0 ? steps * Double(i) : Double(i)
subtitles.append(String(format: "%0.1f", time) )
}
animateText(subtitles: subtitles, duration: steps, animationSpacing: 0, frame: textFrame, targetLayer: layer)
Try to think how subtitles are added in a movie?
A subtitle file contains time stamp values like, 00:31:21 : "Some text". So when the seekbar of a movie is at 00:31:21, you see "Some text" as the subtitle.
Similarly fo your requirement, you would need multiple CATextLayers which have animations corresponding to them (same or different for each CATExtLayer). When one CATextLayer animation ends, you can fade in your second CATextLayer and Fadeout the first one.
Last time I did this I remember, animations can be grouped and you can actually specify start point of an animation. Check beginTime property of CABasicAnimation

Spring animation for rotation that ends where it began

I'd like to create a CASpringAnimation for rotation that ends where it began. Here's some working code that doesn't end where it began (but otherwise is what I'm looking for):
func spring(view: UIView) -> CASpringAnimation{
let animation = CASpringAnimation(keyPath: "transform")
animation.damping = 0.5
animation.initialVelocity = 70
animation.mass = 0.04
animation.stiffness = 12
animation.duration = 1.2
animation.toValue = CATransform3DRotate(CATransform3DMakeAffineTransform(view.transform),
CGFloat(M_PI)/32, // Must be non-zero 😕
0, 0, 1)
return animation
}
And here's how you'd use it:
let animation = spring(view: viewToAnimate)
viewToAnimate.layer.add(animation, forKey: nil)
When I set the angle (beside the comment in CATransform3DRotate) to 0, the animation doesn't work! Any ideas on how to get a spring rotation with the same start and end angles? Thanks for reading.
By adding a CABasicAnimation that animates to 0° over the same time duration, and also by tweaking the spring animation parameters, I've got something that looks pretty good (tested for iOS 10):
#IBAction func springButtonTapped(_ sender: UIButton) { // Attach to the view you'd like animated
let springAnim = spring(view:sender)
sender.layer.add(springAnim, forKey: nil)
let correctionAnim = correct(view:sender)
sender.layer.add(correctionAnim, forKey: nil)
}
func correct(view: UIButton) -> CABasicAnimation{
let animation = CABasicAnimation(keyPath: "transform")
animation.duration = 0.3
animation.toValue = CATransform3DRotate(CATransform3DMakeAffineTransform(view.transform),
0,
0, 0, 1)
return animation
}
func spring(view: UIView) -> CASpringAnimation{
let animation = CASpringAnimation(keyPath: "transform")
animation.damping = 0.0
animation.initialVelocity = 80
animation.mass = 0.04
animation.stiffness = 50
animation.duration = 0.3
animation.toValue = CATransform3DRotate(CATransform3DMakeAffineTransform(view.transform),
CGFloat(M_PI)/64,
0, 0, 1)
return animation
}

Turn rectangular UIImageView into a circle

My ViewController has an UIImageView (lets name it img) as atribute, and in the beginning, it shows img in full screen, that is, its size is a rectangle similar to the ViewController bounds.
I'd like to do an animation with img, making it become a little circle (not just a rectangle with circular corners) and then move it to some corner of the ViewController.
How can I achieve this? I prefer not to send any code cos Im pretty sure I did it the wrong way (I tried to turn it into a square and then set a cornerRadius (with radius width/2), then making scale and translating position at once, but its a mess and works wrongly, so lets forget it)
Your solution sounds fine to me, but what might be going wrong, is the fact that animating corner radius is not supported by UIView.animateWithDuration as seen in the View Programming Guide for iOS. Therefor, the results are sometimes not whats expected.
You can try out the following code.
func animate() {
//Some properties that needs to be set on UIImageView
self.imageView.clipsToBounds = true
self.imageView.contentMode = .ScaleAspectFill
//Create animations
let cornerRadiusAnim = changeCornerRadiusAnimation()
let squareAnim = makeSquareAnimation()
let boundsAnim = changeBoundsAnimation()
let positionAnim = changePositionAnimation(CGPointMake(300,480))
//Use group for sequenced execution of animations
let animationGroup = CAAnimationGroup()
animationGroup.animations = [cornerRadiusAnim, squareAnim, boundsAnim, positionAnim]
animationGroup.fillMode = kCAFillModeForwards
animationGroup.removedOnCompletion = false
animationGroup.duration = positionAnim.beginTime + positionAnim.duration
imageView.layer.addAnimation(animationGroup, forKey: nil)
}
func makeSquareAnimation() -> CABasicAnimation {
let center = imageView.center
let animation = CABasicAnimation(keyPath:"bounds")
animation.duration = 0.1;
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = NSValue(CGRect: imageView.frame)
animation.toValue = NSValue(CGRect: CGRectMake(center.x,center.y,imageView.frame.width,imageView.frame.width))
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
func changeBoundsAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath:"transform.scale")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = imageView.layer.mask?.valueForKeyPath("transform.scale")
animation.toValue = 0.1
animation.duration = 0.5
animation.beginTime = 0.1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
func changeCornerRadiusAnimation() -> CABasicAnimation {
let animation = CABasicAnimation(keyPath:"cornerRadius")
animation.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionLinear)
animation.fromValue = 0
animation.toValue = imageView.frame.size.width * 0.5
animation.duration = 0.1
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
func changePositionAnimation(newPosition: CGPoint) -> CABasicAnimation {
let animation = CABasicAnimation(keyPath: "position")
animation.fromValue = NSValue(CGPoint: imageView.layer.position)
animation.toValue = NSValue(CGPoint: newPosition)
animation.duration = 0.3
animation.beginTime = 0.6
animation.fillMode = kCAFillModeForwards
animation.removedOnCompletion = false
return animation
}
// try like this, i hope it will work for you.
UIView.animateWithDuration(3, delay: 0.0, options: UIViewAnimationOptions.CurveEaseIn, animations: { () -> Void in
img.layer.masksToBounds = true
img.layer.cornerRadius = img.frame.size.height / 2 // here img must be in square shape (height == width)
}) { (finished : Bool) -> Void in
}
option 1 call below method from code
// UIimageView Circle
class func roundImageView(imageView:UIImageView){
imageView.layer.borderWidth = 1.0
imageView.layer.masksToBounds = false
//imageView.layer.borderColor = UIColor.whiteColor().CGColor
imageView.layer.cornerRadius = imageView.frame.size.width/2
imageView.clipsToBounds = true
}
option 2 make user defined run time attributes from storyboard
view.layer.cornerRadius = 5.0f;
view.layer.masksToBounds = NO;

Basic Core Animation - moving image

NO UIVIEW ANIMATIONS, I'M LEARNING CORE ANIMATION
When I run this code, the top image is not on the screen. Then 4 seconds later, it reappears and does as expected. Not sure why. What I want is for the top image to be on the screen when the app launches, and then 4 seconds later for the top image to move up and out of the screen.
#IBOutlet var top: UIImageView!
override func viewDidAppear(animated: Bool) {
super.viewDidAppear(animated)
let openTop = CABasicAnimation(keyPath: "position.y")
openTop.fromValue = self.top.frame.origin.y
openTop.toValue = -self.view.bounds.size.height
openTop.duration = 1.0
openTop.beginTime = CACurrentMediaTime() + 4
self.top.layer.addAnimation(openTop, forKey: nil)
self.top.layer.position.y = -self.view.bounds.size.height
}
Any thoughts?
Here's what I came up with:
let l = top.layer!
let startingPosition = l.position
l.position = CGPointMake( CGRectGetMidX( self.view.bounds ), -l.frame.height * 0.5 )
l.addAnimation({
let anim = CABasicAnimation( keyPath: "position" )
anim.fromValue = NSValue( CGPoint:startingPosition )
anim.beginTime = CACurrentMediaTime() + 4.0
anim.fillMode = kCAFillModeBoth
return anim
}(), forKey: nil)
so you want the image to be on-screen at the start, and then 4 seconds later to fly off the top? You can do this by grouping animations together
override func viewDidAppear(animated: Bool)
{
super.viewDidAppear(animated)
// keep a track of where we want it to be
let y = self.top.layer.position.y
// move it out of the way - this where it will end up after the animation
self.top.layer.position.y = -self.view.bounds.size.height
// do nothing, other than display the image for 4 seconds
let doNothing = CABasicAnimation(keyPath: "position.y")
doNothing.fromValue = y
doNothing.toValue = y
doNothing.duration = 4.0
// zip it away
let openTop = CABasicAnimation(keyPath: "position.y")
openTop.fromValue = y
openTop.toValue = -self.view.bounds.size.height
openTop.duration = 1.0
openTop.beginTime = doNothing.beginTime + doNothing.duration
// create the group
let group = CAAnimationGroup()
group.duration = 5.01
group.timingFunction = CAMediaTimingFunction(name: kCAMediaTimingFunctionEaseOut)
group.removedOnCompletion = false
group.fillMode = kCAFillModeForwards
group.animations = [doNothing, openTop]
self.top.layer.addAnimation(group, forKey: nil)
}

Resources