12

I want to create an animation with several key frames. I want my Layer (a button in this case) to scale up to 1.5 then down to 0.5 then up to 1.2 then down to 0.8 then 1.0.

I also want to EaseIn and EaseOut of each keyframe.

As you can imagine, this will create a Springy/Bounce effect on the spot.

In other parts of my app I have been using CAKeyframeAnimation like this (see below code). This creates a similar springy animation but for x and y position.

Can I adapt the below code to affect scale instead of position?

Thank you in advance!

- (CAAnimation*)monInAnimation {
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path,NULL,113,320);
CGPathAddLineToPoint(path, NULL, 113.5, 283);
CGPathAddLineToPoint(path, NULL, 113.5, 179);
CGPathAddLineToPoint(path, NULL, 113.5, 207);  
CGPathAddLineToPoint(path, NULL, 113.5, 187);
CGPathAddLineToPoint(path, NULL, 113.5, 199);
CGPathAddLineToPoint(path, NULL, 113.5, 193);
CGPathAddLineToPoint(path, NULL, 113.5, 195);
CGPathAddLineToPoint(path, NULL, 113.5, 194);

CAKeyframeAnimation *
animation = [CAKeyframeAnimation 
             animationWithKeyPath:@"position"];

[animation setPath:path];
[animation setDuration:1.5];
[animation setCalculationMode:kCAAnimationLinear];
NSArray *arr = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0], 
 [NSNumber numberWithFloat:0.12], 
 [NSNumber numberWithFloat:0.24], 
 [NSNumber numberWithFloat:0.36], 
 [NSNumber numberWithFloat:0.48],
 [NSNumber numberWithFloat:0.60],
 [NSNumber numberWithFloat:0.72],
 [NSNumber numberWithFloat:0.84],
 [NSNumber numberWithFloat:1.0],nil];

 [animation setKeyTimes:arr];
 [animation setTimingFunctions:[NSArray arrayWithObjects:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                                [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut], nil]];
 //[animation setAutoreverses:YES];
CFRelease(path);
return animation;

}

- (void)monBtnIn {

[monButton.layer setPosition:CGPointMake(113.5,194)];
[monButton.layer addAnimation:[self monInAnimation] 
                    forKey:@"position"];

}

Corey Floyd
  • 25,929
  • 31
  • 126
  • 154
Jonathan
  • 621
  • 1
  • 11
  • 27

4 Answers4

29

Two alternative solutions for you:

First, You can also animate the transform property.

Using Brad's code, but using @"transform" for the keypath. The primary advantage being that you do not have to calculate the actual frame, but instead provide a simple scaling factor:

Objective-C:

CAKeyframeAnimation *boundsOvershootAnimation = [CAKeyframeAnimation animationWithKeyPath:@"transform"];

CATransform3D startingScale = CATransform3DScale (layer.transform, 0, 0, 0);
CATransform3D overshootScale = CATransform3DScale (layer.transform, 1.2, 1.2, 1.0);
CATransform3D undershootScale = CATransform3DScale (layer.transform, 0.9, 0.9, 1.0);
CATransform3D endingScale = layer.transform; 

NSArray *boundsValues = [NSArray arrayWithObjects:[NSValue valueWithCATransform3D:startingScale],
                                                  [NSValue valueWithCATransform3D:overshootScale],
                                                  [NSValue valueWithCATransform3D:undershootScale],
                                                  [NSValue valueWithCATransform3D:endingScale], nil];
[boundsOvershootAnimation setValues:boundsValues];

NSArray *times = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0f],
                  [NSNumber numberWithFloat:0.5f],
                  [NSNumber numberWithFloat:0.9f],
                  [NSNumber numberWithFloat:1.0f], nil];    
[boundsOvershootAnimation setKeyTimes:times];


NSArray *timingFunctions = [NSArray arrayWithObjects:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], 
                            [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                            [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                            nil];
[boundsOvershootAnimation setTimingFunctions:timingFunctions];
boundsOvershootAnimation.fillMode = kCAFillModeForwards;
boundsOvershootAnimation.removedOnCompletion = NO;

Swift 4:

let boundsOvershootAnimation = CAKeyframeAnimation(keyPath: "transform")

let startingScale = CATransform3DScale(layer.transform, 0, 0, 0)
let overshootScale = CATransform3DScale(layer.transform, 1.2, 1.2, 1.0)
let undershootScale = CATransform3DScale(layer.transform, 0.9, 0.9, 1.0)
let endingScale = layer.transform

boundsOvershootAnimation.values = [startingScale, overshootScale, undershootScale, endingScale]

boundsOvershootAnimation.keyTimes = [0.0, 0.5, 0.9, 1.0].map { NSNumber(value: $0) }

boundsOvershootAnimation.timingFunctions = [
    CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseOut),
    CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseInEaseOut),
    CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseInEaseOut)
]

boundsOvershootAnimation.fillMode = kCAFillModeForwards
boundsOvershootAnimation.isRemovedOnCompletion = false

Second, and likely easier, is using FTUtils, an open source wrapper for core animation. It includes a stock "springy" animation.

You can get it at: http://github.com/neror/ftutils

Gordon Tucker
  • 6,653
  • 3
  • 27
  • 41
Corey Floyd
  • 25,929
  • 31
  • 126
  • 154
24

transform.scale, anyone?

let anim = CAKeyframeAnimation(keyPath: "transform.scale")
anim.values = [0, 1, 0, 1, 0, 1, 0]
anim.duration = 4.0

smallView.layer.addAnimation(anim, forKey: "what")

Totally unrelated, if you are gonna use floats in the values array, you must add them as NSNumbers, otherwise they'd just end up as 0s!

anim.values = [0.0, 1.2, 0.9, 1.0].map { NSNumber(double: $0) }
Mazyod
  • 22,319
  • 10
  • 92
  • 157
  • I was using "transform", but that caused problems with `rotationMode = kCAAnimationRotateAuto`. Using "transform.scale" made the values declaration simpler as well. Many thanks. – Henrik Feb 15 '16 at 20:35
  • The "totally unrelated" part is the most relevant part for me :D Thanks. – Starsky Jul 22 '23 at 13:46
8

Rather than setting the path of your CAKeyframeAnimation, you'll want to set the keyframes themselves. I've created a "pop-in" effect before by animating the size of the bounds of a layer:

CAKeyframeAnimation *boundsOvershootAnimation = [CAKeyframeAnimation animationWithKeyPath:@"bounds.size"];
CGSize startingSize = CGSizeZero;
CGSize overshootSize = CGSizeMake(targetSize.width * (1.0f + POPINOVERSHOOTPERCENTAGE), targetSize.height * (1.0f + POPINOVERSHOOTPERCENTAGE));
CGSize undershootSize = CGSizeMake(targetSize.width * (1.0f - POPINOVERSHOOTPERCENTAGE), targetSize.height * (1.0f - POPINOVERSHOOTPERCENTAGE));
NSArray *boundsValues = [NSArray arrayWithObjects:[NSValue valueWithCGSize:startingSize],
                         [NSValue valueWithCGSize:overshootSize],
                         [NSValue valueWithCGSize:undershootSize],
                         [NSValue valueWithCGSize:targetSize], nil];
[boundsOvershootAnimation setValues:boundsValues];

NSArray *times = [NSArray arrayWithObjects:[NSNumber numberWithFloat:0.0f],
                  [NSNumber numberWithFloat:0.5f],
                  [NSNumber numberWithFloat:0.9f],
                  [NSNumber numberWithFloat:1.0f], nil];    
[boundsOvershootAnimation setKeyTimes:times];


NSArray *timingFunctions = [NSArray arrayWithObjects:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut], 
                            [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                            [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut],
                            nil];
[boundsOvershootAnimation setTimingFunctions:timingFunctions];
boundsOvershootAnimation.fillMode = kCAFillModeForwards;
boundsOvershootAnimation.removedOnCompletion = NO;

where POPINOVERSHOOTPERCENTAGE is the fraction by which I wanted to overshoot the target size of the layer.

Brad Larson
  • 170,088
  • 45
  • 397
  • 571
6

I don't know if you can use a CAKeyframeAnimation for animating the scale of a UIView, but you can do it with a CABasicAnimation and setting the fromValue and toValue properties, and using that to animate the transform property:

- (CAAnimation*)monInAnimation 
{
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform"];
    animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.0, 1.0, 1.0)];
    animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(3.0, 3.0, 3.0)];
    [animation setDuration:1.5];
    [animation setAutoreverses:YES];
    return animation;
}

- (IBAction)monBtnIn 
{
    [monButton.layer addAnimation:[self monInAnimation] forKey:@"transform"];
}
Adrian Kosmaczewski
  • 7,956
  • 4
  • 33
  • 59
  • 1
    Thank you akosma! I have tried that way of scaling but I cant figure out how do add more than 2 keyframes. With 'setAutoreverses' it goes from A to B then back to A. I want to go from A to B to C to D. do you know if you can add more 'fromValues' and 'ToValues'? Thanks – Jonathan Jan 04 '10 at 11:29
  • Take a look at the CAAnimationGroup class. It inherits from CAAnimation, and with it you can "chain" individual CABasicAnimations, each with its own fromValue and toValue. Might this be what you are looking for? – Adrian Kosmaczewski Jan 04 '10 at 14:49
  • Scratch that, I've checked the docs and it appears that animations in a CAAnimationGroup are run concurrently, not one after the other. Bummer. You should set delegates for your animations, and then start a new animation when the previous one has finished. – Adrian Kosmaczewski Jan 04 '10 at 14:57