@interface AQBlog : NSBlog @end

Tutorials, musings on programming and ePublishing

AQGlassButton Internals

Permalink

The AQGlassButton class is implemented using two CoreGraphics objects: a CGMutablePathRef and a CGGradientRef. The gradient defines the actual gloss appearance, while the path defines the shape of the button, and is used for both drawing its outline and for clipping the gradient when that is rendered.

The Path

The path itself is a rounded rectangle. We create it in the -setup function, which is called any time the view’s frame is set. The path is drawn by creating a new CGMutablePathRef object and drawing lines and arcs, starting at the lower-left corner just above the arc. Note that cornerRadius is defined as 20% of the view’s current height, but this is just arbitrary based on my own personal tastes.

_path = CGPathCreateMutable();
CGPathMoveToPoint( _path, NULL, CGRectGetMinX(bounds),
CGRectGetMinY(bounds) + cornerRadius );

We then draw the lines and the arcs which make up the shape of the button. First we draw a line up to height - cornerRadius, then we draw the arc. The arc is a bézier curve with control points at the top-left corner of the bounds rectangle.

CGPathAddLineToPoint( _path, NULL, CGRectGetMinX(bounds),
CGRectGetMaxY(bounds) - cornerRadius );
CGPathAddArcToPoint( _path, NULL, CGRectGetMinX(bounds), CGRectGetMaxY(bounds),
CGRectGetMinX(bounds) + cornerRadius,
CGRectGetMaxY(bounds), cornerRadius );

This repeats in similar fashion to draw the lines and arcs for top+right, right+bottom, and bottom+left. At the end we close the subpath; this might not be entirely necessary since we have ended at the same point at which we started, but I prefer to close the path explicitly for completeness.

CGPathCloseSubpath( _path );

The Gradient

The gradient is implemented using CGGradientRef. There are other ways (there are some nice third-party Objective-C gradient classes available, for example) but for simplicity I went with CoreGraphics’ version. Essentially the glass effect is created by using a gradient from the top to the bottom of the button with five colours defined at five points. The given tint colour (the default is {0.6, 0.6, 0.6}) is used as the basis of each colour, with its alpha channel pre-multiplied with our desired transparency values. This isn’t ideal, but it was the quickest way to get a form of tinting in place. Probably I should just full the button with the tint colour and overlay the gradient in the default grey; I’ll try that shortly.

To create the gradient we first need a colour space and an array of colours defined either as CGColorRef or individual CGFloat components. To save on unnecessary allocations I chose to use component colours. Since we’re basing this on the tint colour, we need to pull the existing components out of the supplied colour, and we will do the same with the colour space (this saves allocation and memory-management).

CGColorRef color = self.tintColor.CGColor;
CGColorSpaceRef space = CGColorGetColorSpace( color );

While fetching the components, we’ll fetch their number (RGB or HSB colors will have 4 components, Grayscale will have 2, and CYMK will have 5). We’ll also grab the color’s alpha value directly so we can pre-multiply our gloss transparency values.

size_t numComponents = CGColorGetNumberOfComponents( color );
const CGFloat * srcComponents = CGColorGetComponents( color );
CGFloat tintAlpha = CGColorGetAlpha( color );

To setup the gloss, we have to allocate the appropriate amount of memory to hold all our colour component values; we’ll need five entries of numComponents components, and we want some custom alpha channels.

CGFloat * components = (CGFloat *) NSZoneMalloc( [self zone], numComponents * 5 * sizeof(CGFloat) );
int i, j;
CGFloat alphas[5] = {
0.60 * tintAlpha,
0.40 * tintAlpha,
0.20 * tintAlpha,
0.23 * tintAlpha,
0.30 * tintAlpha
};
for ( i = 0; i < 5; i++ )
{
for ( j = 0; j < numComponents-1; j++ )
components[i*numComponents+j] = srcComponents[j];
components[i*numComponents+j] = alphas[i];
}

The locations of each color are also specified in a plain C array, although this one doesn’t need to be dynamically allocated since there are a constant number of locations:

CGFloat locations[5] = {
1.0, 0.5, 0.499, 0.1, 0.0
};

The locations are specified as a multiplicative function of the height of the rectangle in which the gradient is drawn; therefore 0.0 is where it starts and 1.0 is where it ends. Here we have it grading smoothly for half of the button’s height, then jumping to another color to smoothly grade up to the fourth, then to the fifth color in the last 10% of the gradient. Note that we are supplying the locations in ‘reverse’ since this will actually draw the gradient bottom-up.

Lastly we create the gradient by handing all this information into the creation function:

_gradient = CGGradientCreateWithColorComponents( space, components, locations, 5 );
NSZoneFree( [self zone], components );

Drawing the button

The button itself is drawn in (where else?) the -drawRect: method. Because we’ve done all the hard work elsewhere, saving our path and our gradient and doing all the appropriate math in advance, this is really quite simple.

First of all we set the stroke colour to a dark grey, and we clear the supplied rect:

[[UIColor darkGrayColor] setStroke];
[[UIColor clearColor] setFill];
UIRectFill( theRect );

We indicate a highlighted state by setting the fill colour to a very-nearly-transparent white:

if ( self.highlighted )
[[UIColor colorWithWhite: 1.0 alpha: 0.1] setFill];

Now it’s down to the fiddly stuff. The path is used twice, for drawing the outline (with an optional fill) and for setting up the clipping path ready for the gradient to be drawn. Next we draw the gradient into the button’s bounds, with the clipping path causing it to fill our rounded-rectangle area only. The steps go like this:

  1. Get the current context.
  2. Begin a path in that context and add our saved path object.
  3. Draw it using the current stroke and fill colours (this clears the context’s current path in the process).
  4. Begin a new path in the context.
  5. Add our saved path object again.
  6. Clip the context to the current path (this also clears the current path).
  7. Draw the gradient in our bounds rectangle.

To draw the path with both a stroke and a fill, we use the kCGPathFillStroke constant as the options parameter to CGContextDrawPath(). Setting the clip rectangle to the current path involves a simple call to CGContextClip(). Drawing the gradient just requires passing a pair of points– the top and bottom of our bounds rectangle. The gradient simply takes this vector as its route, and it will fill the context’s clipping region across the perpendicular vector (i.e. since we supply a vertical vector, it will fill horizontally).

The code, which is substantially shorter than everything else, looks like this:

CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth( ctx, 1.0 );
CGContextBeginPath( ctx );
CGContextAddPath( ctx, _path );
CGContextDrawPath( ctx, kCGPathFillStroke );
CGContextBeginPath( ctx );
CGContextAddPath( ctx, _path );
CGContextClip( ctx );
CGRect bounds = self.bounds;
CGContextDrawLinearGradient( ctx, _gradient, CGPointMake(bounds.origin.x, CGRectGetMaxY(bounds)), bounds.origin, 0 );

And there you have it! A simple-enough way to draw your own buttons in code, replete with teh shiny, all without needing to think about images once.

Comments