Fun With Core Animation: Shutter Transition

It’s all over the Twitter now that I forgot about my first #idevblogaday post. So, in the interest of hitting my deadline (and keeping my slot), this post will be quick. I’ll also keep the subject matter squarely in my wheelhouse: Core Animation.

While preparing the material for my half-day workshop at iPhone/iPad DevCon at the end of the month, I added some more examples to my stable of Core Animation examples. One of the new examples is based on an idea that has been rattling around in my head for a while. I call it the shutter transition. Rather than explain what I mean by "shutter transition", I’ll let this quick screencast do the talking for me:

While this is a fairly simple animation, it was a fun problem to solve. It was so fun, in fact, that I solved it two different ways. I’ll briefly cover the most universally applicable solution today, and we’ll revisit this code in a later post to cover the other solution. This is going to go fast, so hold on tight. Also, please forgive the directness. I’m on a deadline here. :)

The shutter animation can be broken into the following steps:

  • Flatten the layer to be animated into a single bitmap.
  • Create multiple CALayer objects of equal size to be the shutters.
  • Loop over all of the shutter layers and:
    • Set the flattened bitmap to the contents of each shutter.
    • Line up all of the shutter layers horizontally.
    • Set the contentsRect of each shutter to be offset slightly based on its position in the list.
  • Hide the original layer, and show the shutters.
  • Animate the shutters offscreen, alternating the direction.
  • Once the shutters are offscreen, remove all of them from their superlayer.

Got it? Good.

Let’s take a look at the code that actually does this. If you are so inclined, all of the code for the shutter transition is on github. Feel free to play with this code and use it however you please in any project. You can also use it to follow along with this post. For those of you following along in Xcode, all of the code I’m about to quote is in the doShutterTransition method.

Let’s get started.

First, We need to flatten our layer into a single bitmap. The renderInContext: method on CALayer makes this very easy for us:

CGSize layerSize = mainLayer_.bounds.size;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGContextRef context = CGBitmapContextCreate(NULL, (int)layerSize.width, (int)layerSize.height, 8, (int)layerSize.width * 4, colorSpace, kCGImageAlphaPremultipliedLast);

[mainLayer_ renderInContext:context];
UIImage *layerImage = [UIImage imageWithCGImage:CGBitmapContextCreateImage(context)];

See? I told you it was easy.

You’ll notice that I create the two animations next. There is a good reason for this. When an animation is added to a layer, the layer makes a copy of the CAAnimation object for its own purposes. Since we will be adding the animations to layers inside of a loop, we can take advantage of this copy behavior and only create the animations once. That way, we won’t incur the overhead of instantiating and throwing away a new CAAnimation object on every loop iteration.

The animation code is very straightforward:

CABasicAnimation *slideUp = [CABasicAnimation animationWithKeyPath:@"position.y"];
slideUp.toValue = [NSNumber numberWithFloat:-(mainLayer_.frame.size.height / 2.f)];
slideUp.duration = 1.f;
slideUp.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
slideUp.fillMode = kCAFillModeForwards;

CABasicAnimation *slideDown = [CABasicAnimation animationWithKeyPath:@"position.y"];
slideDown.toValue = [NSNumber numberWithFloat:(mainLayer_.frame.size.height / 2.f) + [UIScreen mainScreen].bounds.size.height];
slideDown.duration = 1.f;
slideDown.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
slideDown.fillMode = kCAFillModeForwards;

Now we need to remove our original layer from the hierarchy and start creating and adding our "shutters". We also wrap this whole loop in a CATransaction block so that the animations are all in sync.

Finally, we set a completionBlock to remove our shutter layers from the superview once they are offscreen. Because we’re using blocks here, this will only work in iOS 4+. You could implement an animation delegate if you have to target anything earlier than iOS 4. You poor, poor soul.

NSMutableArray *bands = [[NSMutableArray alloc] initWithCapacity:BAND_COUNT];
[mainLayer_ removeFromSuperlayer];

[CATransaction begin];
[CATransaction setCompletionBlock:^(void) {
  [bands enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {
    [obj setDelegate:nil];
    [obj removeFromSuperlayer];

CGFloat bandWidth = layerSize.width / (CGFloat)BAND_COUNT;
for(int i = 0; i < BAND_COUNT; i++) {
  CALayer *band = [[CALayer alloc] init];
  band.masksToBounds = YES;

  CGFloat xOffset = 1.f / (CGFloat)BAND_COUNT;
  band.bounds = CGRectMake(0.f, 0.f, bandWidth, layerSize.height);
  band.contents = (id)[layerImage CGImage];
  band.contentsGravity = kCAGravityCenter;
  band.contentsRect = CGRectMake(xOffset * i , 0.f, xOffset, 1.f);
  CGPoint bandOrigin = mainLayer_.frame.origin;
  bandOrigin.x = bandOrigin.x + (bandWidth * i);
  [band setValue:[NSValue valueWithCGPoint:bandOrigin] forKeyPath:@"frame.origin"];

  [self.view.layer addSublayer:band];

  [band addAnimation:(i % 2) ? slideUp : slideDown forKey:nil];
  [bands addObject:band];
  [band release];
[CATransaction commit];
[bands release];

That’s all there is to it. The real key here is the contentsRect attribute of CALayer. This example was actually created to illustrate its usage (among other things). contentsRect defines what portion to show of the image in the layer’s contents attribute. You’ll notice that the elements of the contentsRect are normalized meaning that each element is in the range 0 - 1. Think of the origin and size of the contentsRect as percentages.

I know we blew through that example really quickly. I’ll be more gentle next time. For now, I need to get this published to meet @mysterycoconut‘s deadline. :)

Until next week…

No comments yet.

Leave a Reply