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 #

I bought the book, PDF & example movies.
I learned a lot; but am working with the iPhone OS.

I'm hung up about iPhone's missing layout manager.

Essentially, I want to replicate what you've done on the iPhone (e.g., "Cover Flow"),
where initially I need to display subLayers in a linear fashion.

I know there must be a simple way of doing this.

I've read that the use of 'Struts & Springs' is an option via the layer's autoresizingMask. But I don't see ANY such mask in the CALayer class.

So, what is the standard way to positioning subLayers in an orderly pattern within a parent Layer?

- Ric.

Posted by Ric on September 28, 2008 at 08:52 PM MDT #

hi Ric,

Sorry but with NDA still in place I can't really comment on how to make things work on the iPhone. As soon as the NDA lifts look for all these examples to be reworked for the iPhone.

Posted by Bill Dudney on September 28, 2008 at 08:53 PM MDT #

Hi Bill,

Many thanks for this wonderful tutorial. Just started learning Xcode/Cocoa and was looking for something like this for sometimes - came across this page, then your book (ended up with buying it) - really enjoyed learning.

I've another question though: How can I do the same using a background image as "navigation/scroll bar" in stead of creating one (like yours and possibly FR) on fly?

Thanks,
Santanu

Posted by Santanu on January 05, 2009 at 11:05 AM MST #

Hi Santanu,

I don't fully understand your question, however I'll answer what I think your question is.

You can place an image into the background layer as its contents and it will show up behind all the sublayers.

If that does not answer your question feel free to post again.

Good luck and thanks for the kind words!

Posted by Bill Dudney on January 05, 2009 at 11:17 AM MST #

Hi Bill,

Thanks for your quick reply and sorry for made you confused. I wasn't very clear indeed, I probably should have said "highlight-bar" in stead. I already have an image in the BG layer and here comes another problem due to "layer.backgroundColor = [self black];" in the (NSArray *)menuItemsFromNames:(NSArray *)itemNames, which puts a black bar behind every menu text. Commenting out that line fixes that issue but makes the shadow (i.e. layer.shadowColor = [self red];) to be disappeared.

So, what I'm trying achieve is: an image in the background, no black (or whatever color) patch behind the texts yet a shadowed highlight bar, like this one.

Cheers,
Santanu

Posted by Santanu on January 05, 2009 at 08:40 PM MST #

Ah,

I think I see now, problem is that transparent stuff wont cast a shadow.

But you can achieve a similar effect by creating an image in photoshop that has the desired shadow effect and then place that in another layer behind the text.

The app looks great BTW!

Good luck.

Posted by Bill Dudney on January 06, 2009 at 04:39 AM MST #

Hi Bill,

That's exactly what I thought as you suggested. But, as I'm still learning and don't really very familiar with cocoa stuff yet, I was wondering it would be so kind of you if you can help me with some sample code.

BTW, I see another problem here: I also have this:


//Here we update the current time
updateTimer = [[NSTimerscheduledTimerWithTimeInterval:1.00
target:self
selector:@selector(refreshTimeData:)
userInfo:nil
repeats:YES] retain];

in the "(void)drawRect:(NSRect)rect" for the clock to update the current time. And what happens here: If the keyboard is not used before the clock starts or within just a couple of seconds after the clock starts, keyboard navigation doesn't work anymore. Any idea how to fix that?

-Santanu

Posted by Santanu on January 06, 2009 at 08:11 AM MST #

I just noticed a typo in my previous posting, which may have confused you already. The "updateTimer" stuff in the "awakeFromNib", *NOT* in the "drawRect:(NSRect)rect" in deed. Sorry about that.

Cheers,
Santanu

Posted by Santanu on January 07, 2009 at 02:10 PM MST #

is it possible to get the Code?
or could i download it from enywhere?

thanx

Posted by jason on February 02, 2009 at 10:28 AM MST #

Reviving this topic, I hope. Way back in September Ric asked about CALayer layout on the iPhone. You said that you couldn't comment due to the NDA at the time, but that you would be reworking your examples for iPhone.

I've just finished reading your book and I'm trying to figure out the best way to manage layout on the iPhone. As Ric pointed out, the docs say that the iPhone uses "Struts & Springs", but this doesn't appear to be the case. Any comments?

In my case, I have a super-layer that resizes its bounds via animation and I want to do a relayout of the contained sub-layers. I've subclassed CALayer to try to figure out how I could catch the property change as it progressed through the animation and tell the sub-layers to adjust themselves. But no luck so far.

Posted by David on April 03, 2009 at 01:05 AM MDT #

When I run this project, the menu 'collapses upwards' (all items move to the top of the screen and partly on top of each other). Also the selected item is not highlighted but a faint blue sheen is seen.

I tried building on both 10.5 and 10.6.

Any idea what might be wrong? Did something got deprecated (there are no warnings during building...).

Thanks

Posted by Sebastian on November 04, 2009 at 07:43 PM MST #

Post a Comment:
  • HTML Syntax: Allowed