Animating Views with Core Animation Layer - objective-c

I have a NSWindow containing a NSView with 'Wants Core Animation Layer' enabled. The view then contains many NSImageView that use are initially animated into position. When I run the animation, it is extremely sluggish and drops most of the frames. However, if I disable 'Wants Core Animation Layer' the animation works perfectly. I'm going to need the core animation layer but can't figure out how to get it to perform adequately.
Can I do anything to fix the performance issues?
Here is the code:
// AppDelegate
NSRect origin = ...;
NSTimeInterval d = 0.0;
for (id view in views)
{
[view performSelector:#selector(animateFrom:) withObject:origin afterDelay:d];
d += 0.05f;
}
// NSImageView+Animations
- (void)animateFrom:(NSRect)origin
{
NSRect original = self.frame;
[self setFrame:origin];
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:0.20f];
[[self animator] setFrame:original];
[NSAnimationContext endGrouping];
}

It's possible that the NSTimer is killing your performance. Core Animation has rich support for controlling the timing of animations through the CAMediaTiming protocol, and you should take advantage of that in your app. Instead of using the animator proxy and NSAnimationContext, try using Core Animation directly. If you create a CABasicAnimation for each image and set its beginTime, it will delay the start of the animation. Also, for the delay to work the way you want, you must wrap each animation in a CAAnimationGroup with its duration set to the total time of the entire animation.
Using the frame property could also be contributing to the slowdown. I really like to take advantage of the transform property on CALayer in situations like this where you're doing an "opening" animation. You can lay out your images in IB (or in code) at their final positions, and right before the window becomes visible, modify their transforms to the animation's starting position. Then, you just reset all of the transforms to CATransform3DIdentity to get the interface into its normal state.
I have an example in my <plug type="shameless"> upcoming Core Animation book </plug> that's very similar to what you're trying to do. It animates 30 NSImageViews simultaneously with no dropped frames. I modified the example for you and put it up on github. These are the most relevant bits of code with the extraneous UI stuff stripped out:
Transform the layers to their start position
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
// ... SNIP ... //
//Start with all of the images at the origin
[CATransaction begin];
[CATransaction setDisableActions:YES];
for (CALayer *imageLayer in [[[self imageContainer] layer] sublayers]) {
CGPoint layerPosition = [layer position];
CATransform3D originTransform = CATransform3DMakeTranslation(20.f - layerPosition.x, -layerPosition.y, 0.f);
[imageLayer setTransform:originTransform];
}
[CATransaction commit];
}
Animate the transform back to the identity
- (IBAction)runAnimation:(id)sender {
CALayer *containerLayer = [[self imageContainer] layer];
NSTimeInterval delay = 0.f;
NSTimeInterval delayStep = .05f;
NSTimeInterval singleDuration = [[self durationStepper] doubleValue];
NSTimeInterval fullDuration = singleDuration + (delayStep * [[containerLayer sublayers] count]);
for (CALayer *imageLayer in [containerLayer sublayers]) {
CATransform3D currentTransform = [[imageLayer presentationLayer] transform];
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform"];
anim.beginTime = delay;
anim.fromValue = [NSValue valueWithCATransform3D:currentTransform];
anim.toValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
anim.fillMode = kCAFillModeBackwards;
anim.duration = singleDuration;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.animations = [NSArray arrayWithObject:anim];
group.duration = fullDuration;
[imageLayer setTransform:CATransform3DIdentity];
[imageLayer addAnimation:group forKey:#"transform"];
delay += delayStep;
}
}
I also have a video on YouTube of the example in action if you want to check it out.

Did you try to batch everything in a CATransaction?
[CATransaction begin];
for {...}
[CATransaction commit];
CATransaction is the Core Animation mechanism for batching multiple layer-tree operations into atomic updates to the render tree.

Related

Creating animated layer stroke

I want to create something like this, just consider single loop and how it completes a circle and reverse it on completion:
This piece of code does half of what I want:
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
drawAnimation.duration = 1;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circleLayer addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
I tried it to reverse but does not work:
[CATransaction begin];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
drawAnimation.duration = 1;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
//[circleLayer addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction setCompletionBlock:^{
CABasicAnimation *animation2 = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation2.duration = 1;
animation2.fromValue = [NSNumber numberWithFloat:0.0f];
animation2.toValue = [NSNumber numberWithFloat:1.0f];
animation2.removedOnCompletion = NO;
animation2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circleLayer addAnimation:animation2 forKey:#"circelBack"];
}];
[circleLayer addAnimation:drawAnimation forKey:#"circleFront"];
[CATransaction commit];
The problem is I cannot reverse the animation.
The Problem
First of all, I suspect you've got the key paths of your animations round the wrong way. You should first be animating the stroke end from 0 to 1, and then the stroke start from 0 to 1.
Secondly, you're never updating the model layer with your new values - so when the animation is complete, the layer will 'snap back' to its original state. For you, this means that when the first animation is done - the strokeStart will snap back to 0.0 - therefore the reverse animation will look weird.
The Solution
To update the model layer values, you can simply set disableActions to YES in your CATransaction block to prevent an implicit animations from being generated on layer property changes (won't affect explicit animations). You'll then want to update the model layer's property after you add the animation to the layer.
Also, you can re-use CAAnimations – as they are copied when added to a layer. Therefore you can define the same animation for both the forward and reverse animation, just changing the key path.
If you're repeating the animation, you'll probably want to define your animation as an ivar - and simply update the key path before adding it.
For example, in your viewDidLoad:
#implementation ViewController {
CABasicAnimation* drawAnimation;
}
- (void)viewDidLoad {
[super viewDidLoad];
// define your animation
drawAnimation = [CABasicAnimation animation];
drawAnimation.duration = 1;
// use an NSNumber literal to make your code easier to read
drawAnimation.fromValue = #(0.0f);
drawAnimation.toValue = #(1.0f);
// your timing function
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// kick off the animation loop
[self animate];
}
You'll then want to create an animate method in order to perform one iteration of the animation. Unfortunately, since Core Animation doesn't support repeating multiple animations in a sequence, you'll have to use recursion to achieve your effect.
For example:
-(void) animate {
if (self.circleLayer.superlayer) { // check circle layer is the layer heirachy before attempting to animate
// begin your transaction
[CATransaction begin];
// prevent implicit animations from being generated
[CATransaction setDisableActions:YES];
// reset values
self.circleLayer.strokeEnd = 0.0;
self.circleLayer.strokeStart = 0.0;
// update key path of animation
drawAnimation.keyPath = #"strokeEnd";
// set your completion block of forward animation
[CATransaction setCompletionBlock:^{
// weak link to self to prevent a retain cycle
__weak typeof(self) weakSelf = self;
// begin new transaction
[CATransaction begin];
// prevent implicit animations from being generated
[CATransaction setDisableActions:YES];
// set completion block of backward animation to call animate (recursive)
[CATransaction setCompletionBlock:^{
[weakSelf animate];
}];
// re-use your drawAnimation, just changing the key path
drawAnimation.keyPath = #"strokeStart";
// add backward animation
[weakSelf.circleLayer addAnimation:drawAnimation forKey:#"circleBack"];
// update your layer to new stroke start value
weakSelf.circleLayer.strokeStart = 1.0;
// end transaction
[CATransaction commit];
}];
// add forward animation
[self.circleLayer addAnimation:drawAnimation forKey:#"circleFront"];
// update layer to new stroke end value
self.circleLayer.strokeEnd = 1.0;
[CATransaction commit];
}
}
To stop the animation, you can remove the layer from the superlayer - or implement your own boolean check for whether the animation should continue.
Full Project: https://github.com/hamishknight/Circle-Pie-Animation

Rotating NSView around center

I'm trying to animate the rotation of an NSView around its center, but it keeps shifting side to side during rotation. What is causing this?
-(void)startRefreshAnimation {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:1.0];
[[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]];
[NSAnimationContext currentContext].completionHandler = ^{ [self startRefreshAnimation]; };
[[view animator] setFrameCenterRotation:previousRotation - 90.0];
previousRotation += -90.0;
[NSAnimationContext endGrouping];
}
Shift up during rotation:
Shift down during rotation:
here is the dummy project and the answer that I found. (For Cocoa Applicaiton)
Rotate NSImageView
github project (download)
From the documentation:
If the application has altered the layer’s anchorPoint property, the behavior is undefined. Sending this message to a view that is not managing a Core Animation layer causes an exception.
https://developer.apple.com/library/mac/#documentation/Cocoa/Reference/ApplicationKit/Classes/NSView_Class/Reference/NSView.html
Is your view managing a CALayer with an unmodified anchor point?
EDIT
I setup similar code at got the exact same results. No adjust of origin or anchor point could resolve this problem. My theory is that this particular method contains bugs (does for autolayout), or works in a way which we don't anticipate. I achieved the correct effect using CABasicAnimation.
/* setup */
....
_view.layer.anchorPoint = CGPointMake(0.5f, 0.5f);
_view.layer.position = ...
[self startRefreshAnimation];
}
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
[self startRefreshAnimation];
}
-(void)startRefreshAnimation {
CABasicAnimation *anim2 = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
anim2.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
anim2.fromValue = [NSNumber numberWithFloat:previousRotation * (M_PI / 180.0f)];
anim2.toValue = [NSNumber numberWithFloat:(previousRotation + 90.0f) * (M_PI / 180.0f)];
previousRotation = previousRotation + 90.0f;
anim2.duration = 1.0f;
anim2.delegate = self;
[_view.layer addAnimation:anim forKey:#"transform"];
}

CABasicAnimation move frame left by 300px

I need help with CABasicAnimation. I am trying to move a NSView left by 300 pixels. I found this SO thread: How to animate the frame of an layer with CABasicAnimation?
Turns out animating the frame is not possible and one of the answer points to a link to QA on Apple's website but it takes me a to a generic page:
http://developer.apple.com/library/mac/#qa/qa1620/_index.html
So, how can I do something as simple as translation of my NSView/CALyer?
Thanks!
NSView has a protocol called NSAnimatablePropertyContainer which allows you to create basic animations for views:
The NSAnimatablePropertyContainer protocol defines a way to add
animation to an existing class with a minimum of API impact ...
Sending of key-value-coding compliant "set" messages to the proxy will
trigger animation for automatically animated properties of its target
object.
The NSAnimatablePropertyContainer protocol can be found here
I recently used this technique to change the origin of a frame:
-(void)setOrigin:(NSPoint)aPoint {
[[self animator] setFrameOrigin:aPoint];
}
Instead of calling the [view setFrameOrigin:], I created another method called setOrigin: which then applies the setFrameOrigin: call to the view's animator.
If you need to change the duration of the animation, you can do so like this (similar to CATransactions):
-(void)setOrigin:(NSPoint)aPoint {
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setCompletionHandler:^{
...Completion Callback Code goes here...
}];
[[NSAnimationContext currentContext] setDuration:1.0];
[[self animator] setFrameOrigin:aPoint];
[NSAnimationContext endGrouping];
}
The NSAnimationContext is described here
You can animate center property instead. Eg:
//assuming view is your NSView
CGPoint newCenter = CGPointMake(view.center.x - 300, view.center.y);
CABasicAnimation *animation = [CABasicAnimation animation];
//setup your animation eg. duration/other options
animation.fromValue = [NSValue valueWithCGPoint:v.center];
animation.toValue = [NSValue valueWithCGPoint:newCenter];
[view.layer addAnimation:animation forKey:#"key"];

Animating a gaussian blur using core animation?

I'm trying to animate something where it's initially blurry then it comes into focus. I guess it works OK, but when the animation is done it's still a little blurry. Am I doing this wrong?
CABasicAnimation* blurAnimation = [CABasicAnimation animation];
CIFilter *blurFilter = [CIFilter filterWithName:#"CIGaussianBlur"];
[blurFilter setDefaults];
[blurFilter setValue:[NSNumber numberWithFloat:0.0] forKey:#"inputRadius"];
[blurFilter setName:#"blur"];
[[self layer] setFilters:[NSArray arrayWithObject:blurFilter]];
blurAnimation.keyPath = #"filters.blur.inputRadius";
blurAnimation.fromValue = [NSNumber numberWithFloat:10.0f];
blurAnimation.toValue = [NSNumber numberWithFloat:1.0];
blurAnimation.duration = 1.2;
[self.layer addAnimation:blurAnimation forKey:#"blurAnimation"];
Your problem is that the animation stops and is automatically removed, but the filter lingers with the tiniest of blur applied.
What you want to do is to remove the blur filter when the animation completes. You need to add a delegate to the CABasicAnimation instance and implement the -[id<CAAnimationDelegate> animationDidStop:finished:] method.
If you let self be the delegate in this case it should be fairly simple, add this line before adding the animation to your layer:
blurAnimation.delegate = self;
And the callback is equally simple:
- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag {
[[self layer] setFilters:nil];
}
If you're looking for an optimized way to animate a blur then I recommend creating a single blurred image of your view and then fading the blurred image from alpha 0 to 1 over the top of your original view. Seems nice and fast in tests.

NSView CALayer Opacity Madness

It seems as though the layer's properties of my NSView are sometimes not editable/wrong. In the code below, the animation works perfectly, and all appears normal. The output from the NSlogs are always :
anim over opacity = 1.00000
first opacity = 0.50000
current opacity = 0.00000
updated opacity = 0.00000
The first two logs look right, so even at animation did stop, the layer seems to operate normally. However, some time later, when I check the opacity it magically turned to 0. Further wrong, when I set the layer's opacity to 1, and check it immediately after, it still is 0. How is that possible?
I goofed around with setneedsdisplay in the layer and setneedsdisplay:YES in nsview and that
didn't help. Any suggestions are greatly appreciated.
- (void) someSetupAnimationMethod {
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
[aLayer setOpacity:0.0];
CABasicAnimation *opacity = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacity.byValue = [NSNumber numberWithFloat:1.0];
opacity.duration = 0.3;
opacity.delegate = self;
opacity.fillMode = kCAFillModeForwards;
opacity.removedOnCompletion = NO;
[opacity setValue:#"opacity done" forKey:#"animation"];
[aLayer addAnimation:opacity forKey:nil];
}
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
if([[anim valueForKey:#"animation"] isEqualToString:#"opacity done"]) {
NSLog(#"anim over opacity = %f", aLayer.opacity);
aLayer.opacity = 0.5;
[aLyaer removeAllAnimations];
NSLog(#"first opacity = %f", aLayer.opacity);
}
}
- (void) someLaterMethod {
NSLog(#"current opacity = %f", aLayer.opacity);
aLayer.opacity = 1.0;
NSLog(#"updated opacity = %f", aLayer.opacity);
}
You're breaking a fundamental CALayer/NSView rule by creating a layer-backed view and then trying to manipulate the layer directly.
aLayer = [CALayer layer];
[theView setWantsLayer:YES];
[theView setLayer:aLayer];
When you tell the view to use a layer before calling setLayer:, the view becomes "layer-backed" -- it is simply using the layer as a buffer and all drawing that you want to do should be done through the usual drawRect: and related methods. You are not allowed to touch the layer directly. If you change the order of those calls:
[theView setLayer:aLayer];
[theView setWantsLayer:YES];
You now have a "layer-hosting" view, which means that the view just provides a space on the screen for the layer to be drawn into. In this case, you do your drawing directly into the layer. Since the contents of the layer become the contents of the view, you cannot add subviews or use the view's drawing mechanisms.
This is mentioned in the -[NSView setWantsLayer:] documentation; I also found an old cocoa-dev thread that explains it pretty well.
I'm not certain that this will fix your problem, but as far as I can see you're doing everything else correctly, such as setting the fill mode and updating the value after the animation finishes.