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"];
Related
I have a NSWindowController containing several NSViewController. The windowController displays the view of the first viewController as a subView of the main window.
On a button click the windowController adds the next viewControllers view as subView, adds some layout constraints and animates them so that the first view moves out and the next view moves in. After the animation the first view is removed from its superView.
[nextController setLeftLayoutConstraint:nextLeft];
// ^^^^^^^^^^^^^^^^^^^^^^^ custom category'd property
[containerView addSubview:nextView];
[containerView addConstraints:#[nextWidth, nextHeight, nextTop, nextLeft]];
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
[context setDuration:0.5];
[[nextLeft animator] setConstant:[self leftOffset]];
[[[[self currentViewController] view] animator] setAlphaValue:-0.25]; // negative alpha to shift the timing
[[[[self currentViewController] leftLayoutConstraint] animator] setConstant:-NSWidth(containerView.frame)];
// ^^^^^^^^^^^^^^^^^^^^ custom category'd property
} completionHandler:^{
[[[self currentViewController] view] setAlphaValue:1.0];
[[[self currentViewController] view] removeFromSuperview];
[[self currentViewController] removeFromParentViewController];
_currentViewController = nextController;
}];
Now the second view is much taller than the first so it changes the windows hight as well.
Unfortunately this window frame change is not animated and the window pops ugly in the right size.
I tried getting the next views hight first to then animate all constraints or something like this. Unfortunately the views are not in the correct size before the animation is done.
Is there any way to animate the window change as well?
I am working on adapting my app to Mavericks since I have been developing it for 10.8. So far I have found various issues which I can't seem to be able to solve. One of them has to do with NSPopover-like animations.
I have a window which I animate in this way:
_zoomWindow.alphaValue = 0;
[_zoomWindow orderFront: self];
// Configure bouncing animation
CAKeyframeAnimation* frameAnim = [CAKeyframeAnimation animation];
[frameAnim setTimingFunction: [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut]];
[frameAnim setValues: #[[NSValue valueWithRect: startFrame], [NSValue valueWithRect: overshootFrame], [NSValue valueWithRect: endFrame]]];
[frameAnim setDuration: duration];
[frameAnim setDelegate: self];
[_zoomWindow setAnimations: #{#"frame": frameAnim}];
// Configure alpha animation
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration: duration];
[[_zoomWindow animator] setAlphaValue: 1.0];
[[_zoomWindow animator] setFrame: endFrame display: YES];
[NSAnimationContext endGrouping];
This works beautifully on 10.8! But it just doesn't do anything on 10.9. Am I missing something here?
I figured it out. The result was that it didn't show anything while suposedly animating, so at first I thought it wasn't animating at all. As it turns out, the problem was in another part of my code completely: the window that I was animating had the content view subclassed by me, and that view was doing some custom drawing. The real problem was that the view wasn't drawing anything on 10.9, even though it was drawing on 10.8... Once I fixed the drawing issue, the animation worked perfectly.
I'm trying to figure out how to animate switching (replacing) subviews in a vertically-configured, 2-view NSSplitView. I've got it semi-working using the following methods in my NSSplitView subclass:
To set up the animation:
- (void)awakeFromNib {
// set delegate
[self setWantsLayer:YES];
CATransition *transition = [CATransition animation];
[transition setType:kCATransitionPush];
[transition setSubtype:kCATransitionFromBottom];
[transition setDuration:1.0];
[self setAnimations:[NSDictionary dictionaryWithObject:transition
forKey:#"subviews"]];
}
And to perform it:
- (void)replaceRightView:(NSView *)newView animated:(BOOL)animate {
NSRect currentSize = [[[self subviews] objectAtIndex:1] frame];
[newView setFrame:currentSize];
if (animate) {
[[self animator] replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
} else {
[self replaceSubview:[[self subviews] objectAtIndex:1]
with:newView];
}
}
However, this code has the effect of pushing the entire NSSplitView off, rather than just the subview on the right side of the split.
Is there a way to animate just the subview transition? Perhaps I'm using the wrong animation key ("subviews")? Other animation methods would be fine too.
Thanks!
It's probably not the cleanest way, but I ended up using an NSView 'container' subclass with custom addSubview: and replaceSubview:withView: methods that modify the new subview's frame to match the container view's frame, which is then structured into the NSSplitView subview I wanted to animate. I then setup the CATransition on the container view, and everything worked as I wanted.
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.
I have an animation which moves some views around. When this animation completes I want the window to recalculate the keyview loop. My code is simmilar to the follow mock code:
[NSAnimationContext beginGrouping];
[newView setAlpha: 0.0]; //hide newView
[self addSubView:newView];
//position the views
[[oldView animator] setFrame: newFrame1];
[[newView animator] setFrame: newFrame2];
[[newView animator] setAlpha: 1.0]; //fade-in newView
[NSAnimationContext endGrouping];
[[self window] recalculateKeyViewLoop];
The problem with this code is that recalculateKeyViewLoop is called before the views are in their new positions which means that the keyviewloop is wrong.
How do I fix this?
My first though is to call recalculateKeyViewLoop in a callback from when the animation ends but I can't figure out how to do this.
Something that's not so obvious, or at least wasn't to me, is that there are two animations going on when you do a setFrame:, with keys frameSize and frameOrigin.
Depending on what your original and final frames are you may need to register yourself as a delegate for one or both of them.
I'd also recommend that you make a copy of the animation you get back from -animationForKey: and store your modified copy in the animations dictionary of your object. This way your delegate will only be called at the conclusion of that particular objects' animator duration, versus all objects animating that key.
eg.
CAAnimation *animation = [[view animationForKey:#"frameOrigin"] copy];
animation.delegate = self;
[view setAnimations:[NSDictionary dictionaryWithObject:animation forKey:#"frameOrigin"]];
In this way your animation object will supersede the default animation object for that view. Then implement whichever delegate methods you're interested in.
You should be able to send -animationForKey: to your views to get a CAAnimation instance, then set yourself as its delegate and implement the method that Adam mentioned.
if you use CAAnimation this has an animationDidStop:finished: delegate method..
http://developer.apple.com/documentation/GraphicsImaging/Reference/CAAnimation_class/Introduction/Introduction.html#//apple_ref/occ/cl/CAAnimation
Hope this helps
You can use a completion handler like this:
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context){
// Start some animations.
[[myView animator] setFrameSize:newViewSize];
[[myWindow animator] setFrame:newWindowFrame display:YES];
} completionHandler:^{
// This block will be invoked when all of the animations started above have completed or been cancelled.
NSLog(#"All done!");
}];
Check out my post here: How would I do this iOS animation on OSX?
I wrote a class that handles this for you, using blocks. Hopefully your target allows blocks! :)
If you are animating an NSWindow (as opposed to an NSView), the other answers will not work and you should do this instead:
CAAnimation *animation = [CABasicAnimation animation];
animation.delegate = self;
self.window.animations = #{#"frame": animation};
[[self.window animator] setFrame:NSMakeRect(0, 0, 400, 200) display:YES];
which will then call the animationDidStop:finished: method on your delegate (which in the above code would be self)