Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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];
}
like image 466
Kevin Sylvestre Avatar asked Jan 25 '11 01:01

Kevin Sylvestre


2 Answers

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.

like image 167
Nathan Eror Avatar answered Sep 21 '22 02:09

Nathan Eror


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.

like image 41
Fabio Avatar answered Sep 19 '22 02:09

Fabio