PrEV
Thoughts from a NeXTStep Guy on Cocoa Development

Front Row like Menus with Core Animation Layers

Jan 11, 2008 by Bill Dudney

A couple of weeks ago someone on the Cocoa-Dev list was asking about making a menu item like the one in Front Row. There were several suggestions and many of them good but none that I thought 'oh that is it'. So I figured I'd post mine here on my blog.

While its not perfect it does show some of the tricks necessary to mold Core Animation to your will.

Before we get started lets do a quick list of steps.

  • Create a 'menu' layer - assumed and not discussed
  • Put the 'menu' layer into a scrolling layer - assumed and not discussed
  • Add 'menu item' layers for each menu item - assumed and not discussed
  • For the selected item:
    • Add a 'bloom' filter to the arrow character
    • Add a blue shadow
    • Add the 'reflection' highlight to the top
  • Live Happily Ever After.

We are going to skip over the scrolling layer and the menu layer in this post. I'll post again about scrolling layers later.

Here is the code snipit that does all the 'highlight' steps in one place. We will go into the detail in the remainder of this post.

  [scrollLayer setValue:[NSNumber numberWithInteger:value] forKey:@"selectedItem"];

  // get the newly selected item

  CALayer *itemLayer = [[menuLayer sublayers] objectAtIndex:value];

  // make sure its scrolled to visible

  [itemLayer scrollRectToVisible:itemLayer.bounds];

  // -- apply the selection features --

  // apply the bloom effect

  CALayer *arrowLayer = [[itemLayer sublayers] objectAtIndex:1];

  [arrowLayer setFilters:[NSArray arrayWithObject:[self bloomArrowEffect]]];

  // set the shadow to visible

  itemLayer.shadowOpacity = 0.85f;

  // add the highlight layer

  [itemLayer insertSublayer:[self highlightLayer] atIndex:0];

Add Bloom Filter

We create the bloom effect with a Core Image bloom filter like this;

- (CIFilter *)bloomArrowEffect {

  if(nil == bloomArrowEffect) {

    bloomArrowEffect = 

    [CIFilter filterWithName:@"CIBloom" 

               keysAndValues:kCIInputRadiusKey, [NSNumber numberWithFloat:5.0f], 

     kCIInputIntensityKey, [NSNumber numberWithFloat:1.0f], nil];

  }

  return bloomArrowEffect;

}

Then the filter is added to the 'arrowLayer' like this;

  CALayer *arrowLayer = [[itemLayer sublayers] objectAtIndex:1];

  [arrowLayer setFilters:[NSArray arrayWithObject:[self bloomArrowEffect]]];

Adding CI effect is one of the coolest eye-candy things that we can do with Core Animation, we could even animate the bloom (i'm not here but you could if you wanted to, if there is enough interest I'd be glad to add it to this example).

Now we have our arrow layer being highlighted its time to move on to the shadow.

Add Blue Shadow

During the initialization of the menu item layers we add the shadow like this;

    layer.cornerRadius = 2.0f;

    layer.shadowRadius = 15.0f;

    layer.shadowOffset = CGSizeMake(0.0f, 0.0f);

    layer.shadowColor = [self blue];

    layer.backgroundColor = [self black];

We specifically set the offset to (0,0) because we want the shadow to come out from under the whole layer, if we set the offset to something it would not be the effect we are looking for, when you are playing with the code make sure to change this to see how it looks when you set the offset.

We set the background color to get the layer to cast a shadow. Remember that the shadow stuff in CA works a lot like the Transparency Layer stuff does in quartz. The drawing done in your layer has the shadow applied to it. If there is no drawing there is now shadow. Setting the background ensures that there is drawing. If you leave out the background color you get shadow only under the text layers, which looks cool too but is not what Front Row looks like. Since we are shooting for Front Row I've done things this way.

Now as the selection changes all we do is set the shadow's transparency to 0.85 (we set it to zero to make it invisible, check out the code for unselectCurrentSelection in the project download).

  itemLayer.shadowOpacity = 0.85f;

Now lets look at the highlight layer.

Add Reflection Highlight

Setting the highlight layer is easy, all we do is place it in the zeroth spot of our sublayers array, like this;

  [itemLayer insertSublayer:[self highlightLayer] atIndex:0];

The real trick is in setting up the highlight layer. Lets look at that code now. While it looks like a lot the code is really simple.

- (CALayer *)highlightLayer {

  if(nil == highlightLayer) {

    highlightLayer = [CALayer layer];

    highlightLayer.masksToBounds = YES;

    highlightLayer.layoutManager = [CAConstraintLayoutManager layoutManager];

    [highlightLayer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintWidth

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintWidth]];

    [highlightLayer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintHeight

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintHeight]];

    [highlightLayer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintMinX

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintMinX]];

    [highlightLayer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintMinY

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintMinY]];

    // highlight at the top of the selection layer

    CALayer *layer = [CALayer layer];

    layer.backgroundColor = [self white];

    layer.opacity = 0.25f;

    layer.cornerRadius = 6.0f;

    [layer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintWidth

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintWidth]];

    [layer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintHeight

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintHeight]];

    [layer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintMinX

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintMinX]];

    [layer addConstraint:

     [CAConstraint constraintWithAttribute:kCAConstraintMinY

                                relativeTo:@"superlayer" 

                                 attribute:kCAConstraintMidY offset:self.offset/2.0f]];

    [highlightLayer addSublayer:layer];

  }

  return highlightLayer;

}

The big picture is that we are setting up two layers, one as a child one as a parent. The highlightLayer has a single child with a backgroundColor of white and an opacity of 0.25. The child also uses rounded corners (radius set to 6). The highlightLayer is set to clip its sublayers (via the masksToBounds property being set to YES, remember that unlike views layers do not clip their sublayers by default). The rest of the code is just setting up constraints as to the way the layers sit in their superlayers.

And finally here are some diagrams of the layer layout. The first one shows the menu layer's layout. The second shows the selected layer and its parts.

Step 1
Step 1

And now the whole UI showing the 'Podcasts' item highlighted.

Front Row Menu

As I said earlier this is far from perfect but I like it better than the alternatives I saw posted. Any feed back is as always greatly appreciated. I have posted the code here for thoes that would like to play around with it. And there is much more about this example (and lots of others) in the Core Animation Book which will be entering Beta in the next week or two.



Comments:

Great article!
Waiting for the book with eagerness.

Greetings from Italy
Sam

Posted by Samuele Schiavi on January 16, 2008 at 01:23 AM MST #

Thanks for the kind words Sam.

Posted by Bill Dudney on January 16, 2008 at 06:12 AM MST #

Hi,

Thanks for the topical article. I tried to download the sample code but the link appears to broken.

Thanks,
Dave.

Posted by Dave on January 18, 2008 at 01:47 AM MST #

Sorry Dave,

Looks like I forgot to set the url...

It should be working now, let me know and thanks for reading!

-bd

Posted by Bill Dudney on January 19, 2008 at 07:05 AM MST #

Post a Comment:
  • HTML Syntax: Allowed