Explicit animation of NSView using core animation - cocoa

I'm trying to slide in a NSView using core animation. I think I need to use explicit animation rather than relying on something like [[view animator] setFrame:newFrame]. This is mainly because I need to set the animation delegate in order to take action after the animation is finished.
I have it working just fine using the animator, but as I said, I need to be notified when the animation finishes. My code currently looks like:
// Animate the controlView
NSRect viewRect = [controlView frame];
NSPoint startingPoint = viewRect.origin;
NSPoint endingPoint = startingPoint;
endingPoint.x += viewRect.size.width;
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
CABasicAnimation *controlPosAnim = [CABasicAnimation animationWithKeyPath:#"position"];
[controlPosAnim setFromValue:[NSValue valueWithPoint:startingPoint]];
[controlPosAnim setToValue:[NSValue valueWithPoint:endingPoint]];
[controlPosAnim setDelegate:self];
[[controlView layer] addAnimation:controlPosAnim forKey:#"controlViewPosition"];
This visually works (and I get notified at the end) but it looks like the actual controlView doesn't get moved. If I cause the window to refresh, the controlView disappears. I tried replacing
[[controlView layer] setPosition:NSPointToCGPoint(endingPoint)];
with
[controlView setFrame:newFrame];
and that does cause the view (and layer) to move, but it is corrupting something such that my app dies with a seg fault soon afterwards.
Most of the examples of explicit animation seem to only be moving a CALayer. There must be a way to moving the NSView and also being able to set a delegate. Any help would be appreciated.

Changes made to views take effect at the end of the current run loop. The same goes for any animations applied to layers.
If you animate a view's layer, the view itself is unaffected which is why the view appears to jump back to its original position when the animation completes.
With these two things in mind, you can get the effect you want by setting the view's frame to what you want it to be when the animation is done and then adding an explicit animation to the view's layer.
When the animation begins, it moves the view to the starting position, animates it to the end position and when the animation is done, the view has the frame you specified.
- (IBAction)animateTheView:(id)sender
{
// Calculate start and end points.
NSPoint startPoint = theView.frame.origin;
NSPoint endPoint = <Some other point>;
// We can set the frame here because the changes we make aren't actually
// visible until this pass through the run loop is done.
// Furthermore, this change to the view's frame won't be visible until
// after the animation below is finished.
NSRect frame = theView.frame;
frame.origin = endPoint;
theView.frame = frame;
// Add explicit animation from start point to end point.
// Again, the animation doesn't start immediately. It starts when this
// pass through the run loop is done.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithPoint:startPoint]];
[animation setToValue:[NSValue valueWithPoint:endPoint]];
// Set any other properties you want, such as the delegate.
[theView.layer addAnimation:animation forKey:#"position"];
}
Of course, for this code to work you need to make sure both your view and its superview have layers. If the superview doesn't have a layer, you'll get corrupted graphics.

I think you need to call the setPosition at the end (after setting the animation).
Also, I don't think you should animate explicitely the layer of the view, but instead the view itself by using animator and setting the animations. You can use delegates too with animator :)
// create controlPosAnim
[controlView setAnimations:[NSDictionary dictionaryWithObjectsAndKeys:controlPosAnim, #"frameOrigin", nil]];
[[controlView animator] setFrame:newFrame];

Related

Layer backed NSView problems in viewDidLoad

I set off to transform an NSImageView. My initially attempt was
self.imageView.wantsLayer = YES;
self.imageView.layer.transform = CATransform3DMakeRotation(1,1,1,1);
Unfortunately I noticed that the transform only happens sometimes (maybe once every 5 runs). Adding an NSLog between confirmed that on some runs self.imageView.layer is null. State of the whole project is shown on the image below.
It's an incredibly simple 200x200 NSImageView with an outlet to a generated NSViewController. Some experimentation showed settings wantsDisplay doesn't fix the problem, but putting the transform on an NSTimer makes it work every-time. I'd love an explanation why this happens (I presume it's due to some race condition).
I'm using Xcode 8 on the macOS 10.12 but I doubt this is the cause of the problem.
Update
Removing wantsLayer and madly enabling Core Animation Layers in Interface Builder did not fix the problem.
Neither did attempts to animate it (I wasn't sure what I was hoping for)
// Sometimes works.. doesn't animate
[NSAnimationContext runAnimationGroup:^(NSAnimationContext *context) {
context.duration = 1;
self.imageView.animator.layer.transform = CATransform3DMakeRotation(1,1,1,1);
} completionHandler:^{
NSLog(#"Done");
}];
or
// Animates but only sometimes
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(1,1,1,1)];
animation.duration = 1;
[self.imageView.layer addAnimation:animation forKey:nil];
After experimenting with allowsImplicitAnimation I realised I might be trying to animate too early.
Moving the transform code into viewDidAppear made it work every time.
- (void)viewDidAppear {
[super viewDidAppear];
self.imageView.animator.layer.transform = CATransform3DMakeRotation(1,1,1,1);
}

El Capitan broke my NSView animations

I have a Mac app that uses a NSAnimationContext animation grouping to animate one NSView offscreen and another NSView onscreen. Prior to beginning the animation grouping I position the offscreen NSView in the position that I want it to originate from when it animates onscreen.
Under Yosemite and earlier versions this worked perfectly but under El Capitan it is as if the NSView never gets positioned in the start position that I specify so it animates onscreen from the wrong direction.
//Position offscreen view at correct starting point.
offscreenView.frame = STARTING_OFFSCREEN_RECT;
//Create animation grouping
[NSAnimationContext beginGrouping];
[[NSAnimationContext currentContext] setDuration:animationDuration];
[[NSAnimationContext currentContext] setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]];
[[NSAnimationContext currentContext] setCompletionHandler:^{
/*
Do cleanup stuff here
*/
}];
//Move the views
onscreenView.frame = ENDING_OFFSCREEN_RECT:
offscreenView.frame = ENDING_ONSCREEN_RECT;
//End Grouping
[NSAnimationContext endGrouping];
I've debugged this to the best of my ability and it appears to me that the setting of offscreenView's frame at the very beginning is not actually occurring.
Does anybody know what I'm doing wrong?
I had very similar problem - offscreenView sometimes starts from the wrong position.
The offscreenView.layer appears to be messed up.
I fixed it by adding the following to my clean-up code:
onscreenView.layer = nil;
so that the next time the offscreenView is animated it will start with clean layer.
Or maybe in your case reset the layer before starting the animation:
offscreenView.layer = nil;
//Position offscreen view at correct starting point.
offscreenView.frame = STARTING_OFFSCREEN_RECT;
//Create animation grouping
...
NOTE:
In my animation I add the offscreenView every time to the superView:
//Position offscreen view at correct starting point.
offscreenView.frame = STARTING_OFFSCREEN_RECT;
[superView addSubview:offscreenView];
//Create animation grouping
...
And in the clean-up code I remove the onscreenView as well:
[onscreenView removeFromSuperview];
onscreenView.layer = nil;

Animating an NSStatusItemView

Struggling with this one. I have a custom NSStatusItemView that I'm trying to animate. I've added the following code to my status item view to kick off the animation:
- (void)setAnimated
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"opacity"];
anim.duration = 1.0;
anim.repeatCount = HUGE_VALF;
anim.autoreverses = YES;
anim.fromValue=[NSNumber numberWithFloat:1.0];
anim.toValue=[NSNumber numberWithFloat:0.0];
[self.layer addAnimation: anim forKey: #"animateOpacity"];
[self setWantsLayer:YES];
[self setNeedsDisplay:YES];
}
When I call this method, nothing happens. Yet if I move this code to my drawRect method, then the view properly animates at launch. Not entirely sure what I need to do to be able to tell it to start animated after the fact but the above method is not doing it and I have no idea why! Any ideas?
Ok answer myself so the googles have record of the answer!
The problem was, kinda of, a lack of understanding of drawRect. When my setAnimated method calls setNeedsDisplay, it calls drawRect again, effectively undoing what is done in the setAnimated method.
There were two things I did to properly fix this. First, I modified the setAnimated method to accept a BOOL argument and set a isAnimated property on the view to that value. I then, in drawRect, check this BOOL value and do the animation if it is set to YES.
Secondly, it seems, you need to call [self setWantsLayer: YES] the first time the view is drawn. So I call this in drawRect the very first time it is run, so that later animation will work.

Resizing an NSView mid-animation

I have a normal NSView that is resizable by dragging the window edges.
If the view is resized during an [NSView animator] animation, it continues to animate to the final size of the original animation, but does not take into account the new window size.
Here is a simple example project. Double click to begin the animation, then resize the window before it finishes.
What is the best way to make the animation take account of the new frame size?
IMHO, the best way would be to stop the animation as soon as the resizing phase begins.
During the resize phase, the user is in control and sets the size of the window manually.
When the resizing phase ends, the window is already set to the desired size, so there is non need to do more.
This kind of problem is best solved with an NSTimer instead of the animator function:
Let the timer call a function repeatedly, until the animation is "complete".
Once complete, end the timer (invalidate).
The function to be called repeatedly in each loop grabs the actual framesize of the window and the actual framesize of your view and simply adds the third of the difference of the two to the frame of the view, like:
frame.size.height += diffHeight/3.0;
So, no matter what happens, the view grows or shrinks closer and closer to its destination.
Once the abs(of the difference) is less then e.g. 0.2 you set the view directly to the desired size and end the timer.
This is direct, uses only little code and you need not listen to any events while it performs pretty well. :-)
Here are the critical codes to initiallize the animation (timer must be an instance of your class):
if(timer)return;
timer = [NSTimer scheduledTimerWithTimeInterval:0.05 target:self selector:#selector(resizeView:) userInfo:[NSNumber numberWithBool:status] repeats:YES];
[timer setTolerance:0.02];
I use the word status instead of your word closed, the function to be repeatedly called might then look somewhat like:
- (void)resizeView:(id)userInfo;
{
BOOL status = [(NSNumber *)[userInfo userInfo] boolValue];
double startwid,stopwid;
NSRect newSizeRect = [[self window] frame];
stopwid = newSizeRect.size.width;
if(status){
stopwid -= 100.0;
}
NSRect cbgRect = [self frame];
startwid = cbgRect.size.width;
double diff = stopwid-startwid;
if(fabs(diff)<0.2){
diff = 0;
startwid = stopwid;
[timer invalidate];
timer = nil;
//NSLog(#"stop");
}
//NSLog(#"%f - %f = %f /10 = %f",stopwid,startwid,diff,diff/3.0);
cbgRect.size.width = startwid+diff/3.0;
[self setFrame:cbgRect];
}

Pausing a SceneKit animation

I'm trying to create a test app in which the user can pause an animation by clicking in the SceneView. The SceneView loads the animation from a .dae file created in a 3d app (Cinema 4D). The app successfully plays and loops the animation upon launch.
To pause the animation, I used Technical Q&A QA1673 as a reference. In the case of this .dae file, the animation actually comes in as a hierarchy of animations, so I have tried reaching down to each underlying CAKeyframeAnimation and setting its speed to zero. My code currently looks like this:
- (void)mouseDown:(NSEvent *)event {
SCNNode *cubeNode = [self.scene.rootNode childNodeWithName:#"C4D_Cube" recursively:YES];
CAAnimation *cubeAnimation = [cubeNode animationForKey:#"Cube_Anim_01-02-1"];
CAAnimationGroup *cubeAnimationGroup = (CAAnimationGroup *)cubeAnimation;
// cubeAnimationGroup contains 3 CAAnimationGroups, each of which contains a CAKeyframeAnimation.
// So I directly access each CAKeyframeAnimation and set its speed to zero.
for (CAAnimationGroup *subGroup in [cubeAnimationGroup animations]) {
CFTimeInterval pausedTime = CACurrentMediaTime();
[[subGroup animations] setValue:#0.0 forKey:#"speed"];
[[subGroup animations] setValue:[NSNumber numberWithFloat:pausedTime] forKey:#"timeOffset"];
}
}
When I set a breakpoint, I can see that the speed of the keyframe animations does change from 1 to 0, but the animation continues to play at its normal speed in the scene view. I originally tried just setting the speed on the top level CAAnimationGroup to zero, but this also had no effect. What's the correct way to pause an animation in progress?
The animations returned by "animationForKey:" are copies of the running animations.
The documentation says "Attempting to modify any properties of the returned object will result in undefined behavior."
So you could do something like this instead:
for(NSString *key in [myNode animationKeys]){
CAAnimation *animation = [myNode animationForKey:key];
[animation setSpeed:0]; //freeze
[animation setTimeOffset:CACurrentMediaTime() - [animation beginTime]]; //move back in time
[cube addAnimation:animation forKey:key]; //re-add the animation with the same key to replace
}
Note that if you just want to pause all the animations coming from a .DAE you might want to do:
[mySCNView setPlaying:NO]; //pause scene-time based animations
Or you could set paused to true.
In Swift:
mySCNView.scene?.paused = true
SCNScene has [isPaused][1] property which you can set. BTW, so does SKScene cause it is a SKNode.

Resources