Using NSViewAnimation with NSAnimationEaseOut weird behaviour - objective-c

In one of my views that can be dragged, if the user drops it at an inappropriate place, it simply drops back to the original frame that was recorded when the mouse went down. This has been working fine for a few days with no animation, but I've decided to introduce animation:
-(void)dropToFrameOrigin:(NSPoint)newFrameOrigin animated:(BOOL)animated {
if (animated) {
NSRect newFrameRect = [self frame];
newFrameRect.origin = newFrameOrigin;
NSDictionary *animationInfo = [NSDictionary dictionaryWithObjectsAndKeys:self, NSViewAnimationTargetKey,
[NSValue valueWithRect:[self frame]], NSViewAnimationStartFrameKey,
[NSValue valueWithRect:newFrameRect], NSViewAnimationEndFrameKey,
nil];
NSViewAnimation *animation = [[NSViewAnimation alloc] initWithViewAnimations:[NSArray arrayWithObject:animationInfo]];
[animation setDuration:0.2];
[animation setAnimationCurve:NSAnimationLinear];
[animation startAnimation];
[animation release];
} else {
[self setFrameOrigin:newFrameOrigin];
}
}
This is working with NSAnimationLinear, and the default NSAnimationEaseInOut, but what I actually want is merely the sense of deceleration, which I assume NSAnimationEaseOut is supposed to do. For some reason, when I use that however, the view snaps immediately to the frame it should end at, then animates sliding back to where the cursor dropped it (the start frame), then snaps back to (what should be) the end frame.
All the other modes (NSAnimationLinear, NSAnimationEaseIn, NSAnimationEaseInOut) seem to work just fine. Am I misunderstanding the purpose of NSAnimationEaseOut?

For some reason you have to flip the start and end frames to get NSAnimationEastOut to work. Plus, you need to set an animation delegate so you can be notified when the animation ends, where you must then manually set the view's frame to the original end frame.

Related

Move an NSView from outside view with autolayout using NSAnimation

I am using auto-layout in my project and have a 'detailed view' slide over the 'tile view' upon a button press.
The 'tile view' is aligned to the top, bottom, left and right.
The 'detailed view' is aligned to centre x,y and equal width/height.
I got the animations to work however I had to use two different methods:
1) This is my preferred method of animation and works perfectly fine on the 'tile view'
CAKeyframeAnimation* tileKeyAnimation = [CAKeyframeAnimation animationWithKeyPath:#"transform"];
tileKeyAnimation.fillMode = kCAFillModeForwards;
tileKeyAnimation.removedOnCompletion = NO;
tileKeyAnimation.keyTimes = #[#0,#0.45];
tileKeyAnimation.values = #[[NSValue valueWithCATransform3D:CATransform3DIdentity],
[NSValue valueWithCATransform3D:self.tileTransform]];
tileKeyAnimation.duration = 0.5;
[_tileView.layer addAnimation:tileKeyAnimation forKey:#"transform"];
2) Since the above animation was not working for the 'detailed view', I resorted to using a different type of animation for it. (I created an outlet for the centreX constraint named _detailedCenterXConstraint)
_detailedCenterXConstraint.constant = _tileView.bounds.size.width;
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setTimingFunction:[[CAMediaTimingFunction alloc] initWithControlPoints:0 :0 :0 :0.45]];
[context setDuration:[self pushAnimationDuration]];
[_detailedCenterXConstraint.animator setConstant:_detailedView.bounds.size.width];
} completionHandler:^{
}];
The problem with this approach is that the animations do not match with each other. I have tried using a custom CAMediaTimingFunction in (2) to match the animation timing in (1) but it seems that I'm not using this correctly.
Is there a reason why a CAKeyframeAnimation (1) should not work on my 'detailed view'?
Alternatively is there a way how to match the animation timing between the two approaches?
I have managed to get an identical animation with the following code:
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
[context setTimingFunction:0.45 :1.0 :0.45 :1.0];
[context setDuration:[self pushAnimationDuration]/2];
[_detailedCenterXConstraint.animator setConstant:_detailedView.bounds.size.width];
} completionHandler:^{
}];

animationDidStop method called immediately

I've never had this issue come up before. my animationDidStop method is being called before the animation actually completes. animationDidStart gets called first, but then animationDidStop is called immediately after. I tried to handle this using an animation completion block, but it called the animation completed immediately still. Anybody run across this before? I really could use a bit of help. THANK YOU.
-James
CODE:
-(void) runAnimation {
//Create an animation that rotates the tile
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
[animation setDuration:6];
[animation setFromValue:[NSNumber numberWithFloat:0]];
[animation setToValue:[NSNumber numberWithFloat:0.5*M_PI]];
[animation setDelegate:self];
animation.fillMode = kCAFillModeForwards;
animation.removedOnCompletion = NO;
[[self.view viewWithTag:100].layer addAnimation:animation forKey:#"solutionRotate"];
}
-(void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
if (theAnimation == [[self.view viewWithTag:100].layer animationForKey:#"solutionRotate"]){
//test
NSLog (#"test");
}
}
If the layer is not part of any layer tree then the animation will end immediately since there is nothing to actually animate on screen. Make sure the animated view is added to a visible view hierarchy.

background flashes during image swap cross disolve, CATransition

Currently I have a CATransistion that should swap out the _backgroundImageView.image with a new one. It works great... And works quickly, however the previous image will glitch during this transition randomly... Aka flash the full image for one frame somewhere in the duration of the transition and then finish the transition.
Here is the code:
[CATransaction begin];
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
transition.duration = 0.3;
transition.delegate = self;
[transition setValue:#"swipe" forKey:#"tag"];
[_backgroundImageView.layer addAnimation:transition forKey:nil];
_backgroundImageView.image = [_backgroundImages objectAtIndex:pageControl.currentPage];
[CATransaction commit];
If I add this before the code then the glitch doesnt happen but other UI stuff gets choppy because the image comparison seems to block the main thread (which it has to as the animation is a main thread thing too)
if ([UIImagePNGRepresentation(_backgroundImageView.image) isEqualToData:UIImagePNGRepresentation([_backgroundImages objectAtIndex:pageControl.currentPage])]) {
return;
}
BTW flickering doesn't happen on the device.... at least so far I can see... so maybe this question is moot... I have seen crashes during transitions... but that might be another issue.

Animating Views with Core Animation Layer

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.

How do I trigger a callback when a NSAnimationContext ends?

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)