Stars at Various Scales and filled at 25, 50, 75, 100 %

Stars at Various Scales and filled at 25, 50, 75, 100 %

Earlier we posted about implementing a star ratings slider using two images.  Unfortunately this method does not work for iPhone OS 3.0 as Apple decided to stretch the two images instead of using a mask like they had done in OS 2.2.1.  The new implementation uses the Quartz 2D system to draw the stars and fill them in.  In the following article we will demonstrate how to do this using Quartz 2D and will provide code examples to get you going with your own custom implementations.  In part one we will talk about creating a custom star that can be filled to any arbitrary percentage.  Part 2 will explain how to use the custom star to create the slider.

Quartz 2D

The underlying drawing API on the iPhone (and on OS X Leopard) is Quartz.  Quartz allows you to draw complex 2D images using paths to draw arbitrary shapes (including Bézier curves) and stroke and fill them.  Quartz also provides you with powerful Affine Transform methods that allow you to scale, rotate, and translate your drawings.  Quartz uses a painters model, so each subsequent draw operation will paint over top of your previous image.  When you are building a complex image, the order in which you issue the draw commands matters.  We will take advantage of this when drawing our custom fill-able star.

Drawing and Filling a Star

The first thing we want to do in order to create a star slider is to be able draw and fill a star.  For this purpose we created the BAFillableStar class.  This class will allow you to draw and fill a star to any arbitrary percentage (0-100%) starting from the left side.

@interface BAFillableStar : UIView {

CGPoint points[10];
UIColor * fillColor;
UIColor * backgroundColor;
UIColor * strokeColor;

CGFloat lineWidth;

float fillPercent;

}

@property (nonatomic, retain) UIColor * fillColor, * backgroundColor, * strokeColor;
@property (nonatomic) CGFloat lineWidth;
@property (nonatomic) float fillPercent;

@end

Before we get to the drawing, lets talk about the coordinates of the star.  We want to have normalized coordinates for the 10 points that make up the star in order to be able to draw the star to any scale.  In the init method of the class we want to load these into a CGPoint array.  CGPoint array is used because it makes it easier to draw the path for the star.

– (id)initWithFrame:(CGRect)frame {

if (self = [super initWithFrame:frame]) {

// a normalized star points array
points[0] = CGPointMake(0.5,0.025);
points[1] = CGPointMake(0.654,0.338);
points[2] = CGPointMake(1,0.388);
points[3] = CGPointMake(0.75,0.631);
points[4] = CGPointMake(0.809,0.975);
points[5] = CGPointMake(0.5,0.813);
points[6] = CGPointMake(0.191,0.975);
points[7] = CGPointMake(0.25,0.631);
points[8] = CGPointMake(0,0.388);
points[9] = CGPointMake(0.346,0.338);

}
lineWidth = 2.0;  //default line width

self.strokeColor = [UIColor blackColor];

//default colors
self.fillColor = [UIColor yellowColor];
self.backgroundColor = [UIColor whiteColor];
//scale our normalized points to the dimensions of the rectangle
for (int i=0; i<10; i++) {
points[i].x = points[i].x * frame.size.width;
points[i].y = points[i].y * frame.size.height;
}
return self;

}

Remember that Quartz uses the painters method so the first thing we want to draw is the background.  This is simple as all we want to do is fill the rectangle that defines the size of the view with the background colour.

-(void) fillBackgroundOfContext:(CGContextRef)context withRect:(CGRect)rect;
{

CGContextSetFillColorWithColor(context, [backgroundColor CGColor]);
CGContextFillRect(context, rect);

}

Then we want to draw the filled star.  Remember that we are going to fill it up from the left side to an arbitrary percentage.  The simplest way to do this is to define a path that draws the star and restrict drawing to the area defined by this path.  This is done by using the CGContextClip method.  Before we set the clipping area we want to save the graphic context as we don’t want any other subsequent drawing commands to get effected by our clipped area.  At the end we restore the context to its original state.

-(void) fillStarInContext:(CGContextRef)context withRect:(CGRect)rect
{

CGContextSaveGState(context);//create the path using our points array
CGContextBeginPath(context);
CGContextAddLines(context, points, 10);
CGContextClosePath(context);
CGContextClip(context);  //clip drawing to the area defined by this path

rect.size.width = rect.size.width * fillPercent;  //we want make the width of the rect
CGContextSetFillColorWithColor(context, [fillColor CGColor]);
CGContextFillRect(context, rect);

CGContextRestoreGState(context);

}

Finally we want to draw the outline of the star and stroke it with our stroke colour.

-(void) drawStarOutlineInContext:(CGContextRef)context withRect:(CGRect)rect
{

CGContextBeginPath(context);
CGContextAddLines(context, points, 10);  //create the path
CGContextClosePath(context);
//set the properties for the line
CGContextSetLineWidth(context, lineWidth);
CGContextSetStrokeColorWithColor(context, [strokeColor CGColor]);//stroke the path
CGContextStrokePath(context);

}

Putting it all together, we want to override the drawRect method of the UIView class so that we can draw our star.  First we have to grab the graphic context, then we want to create a layer from this context because drawing to a layer is faster than drawing to the graphic context directly.  The next step is to scale our normalized points to their correct locations using the width and height of the rectangle.  We then call our draw and fill method defined above in the right order using the layers context.  Finally we draw the layer to the actual graphic context and we are done.

– (void)drawRect:(CGRect)rect {

CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetShouldAntialias(context, true);CGLayerRef layer = CGLayerCreateWithContext(context, rect.size, NULL);
CGContextRef layerContext = CGLayerGetContext(layer);

[self fillBackgroundOfContext:layerContext withRect:rect];
[self fillStarInContext:layerContext withRect:rect];
[self drawStarOutlineInContext:layerContext withRect:rect];

CGContextDrawLayerInRect(context, rect, layer);  //draw the layer to the actual drawing context

CGLayerRelease(layer);  //release the layer

}

And that is it for Part 1, look for part 2 coming out soon.