Custom Star Ratings Slider Using Quartz2D Part 2

In Part 1 we talked about creating a custom UIView class that draws fillable stars. We’ve edited the code in Part 1 to fix some errors that were discovered when implementing the slider, mainly that the scaling of the coordinates should only happen once, (upon initialization) and not in the drawRect method.

In this part we will talk about using this fillable star to create a custom UISlider. Here is the interface definition for the BAUIStarSlider.

typedef enum
{
ApproximationModeNone,
ApproximationModeWhole,
ApproximationModeHalf,
ApproximationModeQuarter

} SliderApproximationMode;

@interface BAUIStarSlider : UIControl {

NSMutableArray * starArray;
int numStars;
SliderApproximationMode approxMode;
float value;

}
@property(nonatomic) SliderApproximationMode approxMode;
@property(nonatomic) float value;

-(id)initWithFrame:(CGRect)frame andStars:(int)inNumStars;

@end

As you can see the BAUIStarSlider is a subclass of UIControl which will allow us to make use of the event dispatching methods found in UIControl. The implementation of the custom slider is pretty straight forward. There are only a few methods that need to be implemented, the first being the initialization method:

-(id)initWithFrame:(CGRect)frame andStars:(int)inNumStars
{
if (self = [super initWithFrame:frame])
{
numStars = inNumStars;
starArray = [[NSMutableArray alloc] initWithCapacity:numStars];
approxMode = ApproximationModeWhole; //default approximation mode

float width = frame.size.width/numStars;
float height = frame.size.height;

for(int i=0; i < numStars; i++)
{

BAFillableStar * star = [[BAFillableStar alloc] initWithFrame:CGRectMake(0+(i*width), 0, width, height)];
[starArray addObject:star];
[self addSubview:star];
[star release];

}

}

return self;

}

The next two methods deal with setting the value on the slider using touch. When a user touches a point on the star, we want to fill the stars of the slider up to that point. We also want to allow the user to slide their finger along the slider and when they do this we want to fill or clear the stars along the slider appropriately.

- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
UITouch* touch = [touches anyObject];
CGPoint touchPoint = [touch locationInView:self];

//make sure the points are within our bounds
if(touchPoint.x < 0 )
{

touchPoint.x = 0;

}
if(touchPoint.x > self.frame.size.width)
{

touchPoint.x = self.frame.size.width;

}

value = touchPoint.x / (self.frame.size.width/numStars);
[self fillStars];

}
- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event
{

//just call the touchesBegan method
[self touchesBegan:touches withEvent:event];

}

The final two methods to implement do the actual filling of the stars based on the value that was set using the touch methods. The implementation of setFillPercent in the BAFillableStar class calls [self setNeedsDisplay] so that when the fill percent is set, the star will automatically redraw itself. We want to implement our own setValue method so that when the value of the slider is initially set the correct number of stars get filled properly. The getApproximatedPart method uses the approximation mode to round the part value, so if the approximation mode is none, no rounding happens, if the mode is half, then it will round it to the nearest half value, etc…

-(void)fillStars{
float whole = floor(value);
float part = value – whole;
part = [self getApproximatedPart:part];

//dispatch a value changed event
value = whole + part; //since we approximated
[self sendActionsForControlEvents:UIControlEventValueChanged];

int i = 0;
for(BAFillableStar * star in starArray)
{

if( i < whole)
{
star.fillPercent = 1;

}
else if ( part > 0 )
{

star.fillPercent = part;
part = 0;

}
else
{

star.fillPercent = 0;

}
i++;

}

}
-(void)setValue:(float)inValue
{

value = inValue;
[self fillStars];

}

That’s it for this tutorial. Hope you found it informative. If you have any questions ask in the comments section and we will try to answer them as best we can.

Tags: , ,

Custom Star Ratings Slider Using Quartz2D Part 1

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.

Tags: , , , , ,