Resolution Independence

Saturday, 5 April 2008

I, like most developers, love shiny new user interfaces. Rounded corners, gradients, drop shadows, custom drawing — all of these things make me smile. What I don’t love, though, is an application that can’t draw with resolution independence and whose Resources folder weighs in at around 100MB. It seems that almost every new app (with a few notable exceptions) handles its custom UI needs by filling the Resources folder with bitmaps of every part of their interface in every possible state. To draw their controls, they tape these bitmaps back together on the screen and call it a day.

The problem with this is that it doesn’t give applications much flexibility. By drawing using bitmaps, you’re limited to only one look at only one resolution. Drawing with code, however, gives you not only the option to draw scaled versions of your controls, but also controls with variations in their appearance.

In order to remedy this, I’ve decided to write a tutorial or two on creating good looking custom controls without a whole mess of bitmaps. I’ll try to gear the workflow towards both the designer and the developer, so that hopefully anybody working on an app can benefit from what I have to offer. The first tutorial will be on the toolbar button style from iMove ‘08:

Part 1 - The Mockup

The first step, for me, in creating a new control is to mock it up in Photoshop. It’s not an absolute requirement, but it makes it easier to get started in the coding phase. Zoomed in, we can see that the make-up of the control is pretty simple. The center of the control is a big gradient, and the outline is a simple 1 pixel border, with a 1 pixel drop shadow.

Starting with a blank document with a grey background, I create the basic shape for the control. In this case, a rounded rectangle with a radius of 3.5 pixels.

Given a base shape to work with, Layer Effects are your best friend. If you’re not familiar with them, they’re like basic filters that you can continuously tweak until your layer looks right. The rest of the mockup phase is done entirely using Layer Effects on our basic shape.

To start, we’ll apply a greyscale gradient overlay with a starting brightness of 67% and an ending brightness of 100% (pure white) with an angle of 90 degrees. When you’re replicating an existing control, you can use the color-picker in Photoshop to find the values you need, but in this tutorial I’ll be providing them for you. The result should look something like this:

On top of this, we’ll apply a layer stroke to give the outside of the shape more definition. Setting the size to 1 pixel, the position to ‘Inside’, and the color to a grey with a brightness of 26%, we get something that looks like this:

The last thing needed for this state of the control is a simple drop shadow to give it more depth. Set the color to a grey with a brightness of 86%, and the rest of the settings as follows:

With that, the mockup of the first state of the control is finished! The pressed state of the control is only slightly more complicated, and is simple enough to mock up. Notable differences are that the gradient is darker and in the other direction, and there is an inner shadow for extra depth.

To get started, go ahead and make a copy of the shape layer to serve as the starting point. Changing the gradient overlay’s starting color brightness to 51% and the ending color’s brightness to 40%, we get something that looks like this:

This state of control actually has two inner shadows, so we’ll need to add another layer in a second. The first inner shadow is solid opaque black, with a distance of 0 pixels and a size of 3 pixels. The effect is subtle, but will make a big difference in the final appearance.

For the second, more obvious inner shadow, go ahead and create a copy of the current layer and remove all of its effects except for the inner shadow. So that you can see through the layer, be sure to set its Fill to 0%. Set the new layer’s inner shadow opacity to 50%, the distance to 2 pixels, the size to 8 pixels, and the angle to 90 degrees. The final product should look like this:

Now with all of the Photoshop work finished, we can move on to the fun part.

Part 2 - The Code

None of this mockup work will do your project any good if you can’t translate it into code, so let’s turn it into something useful. To make life easier, I’ve created an NSBezierPath category with a few helper methods for drawing inner shadows and blurs, along with a method for drawing a stroke inside of a path taken from Matt Gemmell. I’ve also created a category for NSShadow that adds a helpful constructor for more concise code.

As this aims to be a tutorial on drawing, and not subclassing in general, I’m going to dive right in without walking you through the setup. There are plenty of other resources on the internet for creating NSControl/NSCell subclasses, and it’s really not all that difficult once you get past a few hiccups.

To start, we’ll create a handful of static variables in our drawing method for colors/gradients/shadows that will be reused. Making these static variables in the method rather than instance variables for each object saves resources, but may not suit your needs. If the color of the control can be manipulated per object, for example, you’d want to save them as instance variables instead. Since our example is pretty straightforward, static variables will suffice.

static NSGradient *pressedGradient = nil;
static NSGradient *normalGradient = nil;
static NSColor *strokeColor = nil;
static NSShadow *dropShadow = nil;
static NSShadow *innerShadow1 = nil;
static NSShadow *innerShadow2 = nil;

if (pressedGradient == nil) {
   pressedGradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:.506 alpha:1.0]
                                                   endingColor:[NSColor colorWithCalibratedWhite:.376 alpha:1.0]];
   normalGradient = [[NSGradient alloc] initWithStartingColor:[NSColor colorWithCalibratedWhite:.67 alpha:1.0]
                                                 endingColor:[NSColor whiteColor]];

   dropShadow = [[NSShadow alloc] initWithColor:[NSColor colorWithCalibratedWhite:.863 alpha:.75]
                                         offset:NSMakeSize(0, -1.0) blurRadius:1.0];
   innerShadow1 = [[NSShadow alloc] initWithColor:[NSColor blackColor] offset:NSZeroSize blurRadius:3.0];
   innerShadow2 = [[NSShadow alloc] initWithColor:[NSColor colorWithCalibratedWhite:0.0 alpha:.52]
                                           offset:NSMakeSize(0.0, -2.0) blurRadius:8.0];

   strokeColor = [[NSColor colorWithCalibratedWhite:.26 alpha:1.0] retain];
}

All of these object take their values directly from their equivalent effects in the Photoshop mockup. This is where the benefit of mocking the control up first comes into play. While it’s possible to create the control by tweaking, compiling and running, it’s often much quicker to get the look you want in Photoshop first, and then replicate it in your code.

The next step is to calculate the drawing area for the given bounds, and to create the path object we’ll be working with.

NSRect rect = frame;
rect.size.height -= 1;
CGFloat radius = 3.5;

NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:rect xRadius:radius yRadius:radius];

We start with the rectangle given to us, frame, and alter its height by 1 pixel to account for the drop shadow. Note that in this situation we do not adjust the y-origin because the control we’re working on uses a flipped coordinate system, meaning the y-dimension increases positively down the screen from top to bottom. If we were not working in a flipped coordinate system, the y-origin would also have to be increased accordingly. The rounded rectangle path is given a radius of 3.5 points, the value we used for our vector shape in the mockup phase.

The order in which each part of the control is drawn is closely related to the order in which Photoshop composites the respective effect. In this case, we start with the drop shadow:

[NSGraphicsContext saveGraphicsState];
[dropShadow set];
[path fill];
[NSGraphicsContext restoreGraphicsState];

After that we draw the gradient:

NSGradient *gradient = self.isHighlighted ? pressedGradient : normalGradient;
[gradient drawInBezierPath:path angle:-90];

We do a simple check to see which gradient to draw, and then render it at -90 degrees. The reason for the negative angle is, again, because we are drawing in a flipped coordinate system. Next up is the inner stroke:

[strokeColor setStroke];
[path strokeInside];

For the normal state, that’s all there is to it, but for the pressed state we have to draw the inner shadows:

if (self.isHighlighted) {
   [path fillWithInnerShadow:innerShadow1];
   [path fillWithInnerShadow:innerShadow2];
}

That’s all there is to it! We’ve created a pretty decent looking custom control that uses zero bitmap resources, and will draw at any resolution.


Go ahead and download the sample code and the PSD for the mockup to check out the finer details. If you have any questions or comments, feel free to email me at comments@seanpatrickobrien.com. Hopefully this tutorial will be helpful, and if you have any ideas for new tutorials, don’t hesitate to send me your suggestions.