I've made entire working package that will mimic this progress view you have posted. However, to make it more customizable, I have not used any images, but used CoreGraphics to draw it. The package can be found at lightdesign/LDProgressView. I'll also probably make it into a CocoaPod if you know what that is.
How To Draw A KAYAK-like Progress View
All of the inner workings of the progress view and therefore how to mimic the KAYAK progress view can be found in this file. I've added some comments here in the code blocks for easier understanding. Here's the drawRect
method:
- (void)drawRect:(CGRect)rect {
[self setAnimateIfNotSet];
CGContextRef context = UIGraphicsGetCurrentContext();
[self drawProgressBackground:context inRect:rect];
if (self.progress > 0) {
[self drawProgress:context withFrame:rect];
}
}
This is pretty self-explanatory. I set the animate property if it isn't set already and I draw the background. Then, if the progress is greater 0, I'll draw the progress within the total frame. Let's move on to the drawProgressBackground:inRect:
method:
- (void)drawProgressBackground:(CGContextRef)context inRect:(CGRect)rect {
CGContextSaveGState(context);
// Draw the background with a gray color within a rounded rectangle
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:10];
CGContextSetFillColorWithColor(context, [UIColor colorWithRed:0.51f green:0.51f blue:0.51f alpha:1.00f].CGColor);
[roundedRect fill];
// Create the inner shadow path
UIBezierPath *roundedRectangleNegativePath = [UIBezierPath bezierPathWithRect:CGRectMake(-10, -10, rect.size.width+10, rect.size.height+10)];
[roundedRectangleNegativePath appendPath:roundedRect];
roundedRectangleNegativePath.usesEvenOddFillRule = YES;
CGSize shadowOffset = CGSizeMake(0.5, 1);
CGContextSaveGState(context);
CGFloat xOffset = shadowOffset.width + round(rect.size.width);
CGFloat yOffset = shadowOffset.height;
CGContextSetShadowWithColor(context,
CGSizeMake(xOffset + copysign(0.1, xOffset), yOffset + copysign(0.1, yOffset)), 5, [[UIColor blackColor] colorWithAlphaComponent:0.7].CGColor);
// Draw the inner shadow
[roundedRect addClip];
CGAffineTransform transform = CGAffineTransformMakeTranslation(-round(rect.size.width), 0);
[roundedRectangleNegativePath applyTransform:transform];
[[UIColor grayColor] setFill];
[roundedRectangleNegativePath fill];
CGContextRestoreGState(context);
}
Here, I create a rounded rectangle within the view with a radius of 10
(which I may later allow to be customizable) and fill it. Then the rest of the code is drawing the inner shadow, which I don't really need to go into detail about. Now, here's the code for drawing the progress in the method drawProgress:withFrame:
:
- (void)drawProgress:(CGContextRef)context withFrame:(CGRect)frame {
CGRect rectToDrawIn = CGRectMake(0, 0, frame.size.width * self.progress, frame.size.height);
CGRect insetRect = CGRectInset(rectToDrawIn, 0.5, 0.5);
UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:insetRect cornerRadius:10];
if ([self.flat boolValue]) {
CGContextSetFillColorWithColor(context, self.color.CGColor);
[roundedRect fill];
} else {
CGContextSaveGState(context);
[roundedRect addClip];
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGFloat locations[] = {0.0, 1.0};
NSArray *colors = @[(__bridge id)[self.color lighterColor].CGColor, (__bridge id)[self.color darkerColor].CGColor];
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
CGContextDrawLinearGradient(context, gradient, CGPointMake(insetRect.size.width / 2, 0), CGPointMake(insetRect.size.width / 2, insetRect.size.height), 0);
CGContextRestoreGState(context);
CGGradientRelease(gradient);
CGColorSpaceRelease(colorSpace);
}
CGContextSetStrokeColorWithColor(context, [[self.color darkerColor] darkerColor].CGColor);
[self drawStripes:context inRect:insetRect];
[roundedRect stroke];
[self drawRightAlignedLabelInRect:insetRect];
}
There are 4 primary parts to this method. First, I do the calculation of the frame that the progress will take up based on the self.progress
property. Second, I draw either a solid color if the flat
property is set, or I draw a calculated gradient (methods lighterColor
and darkerColor
are in a UIColor
category). Third, I draw stripes and finally draw the percentage label. Let's cover those 2 methods quickly. Here's the drawStripes:inRect:
method:
- (void)drawStripes:(CGContextRef)context inRect:(CGRect)rect {
CGContextSaveGState(context);
[[UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:10] addClip];
CGContextSetFillColorWithColor(context, [[UIColor whiteColor] colorWithAlphaComponent:0.2].CGColor);
CGFloat xStart = self.offset, height = rect.size.height, width = STRIPE_WIDTH;
while (xStart < rect.size.width) {
CGContextSaveGState(context);
CGContextMoveToPoint(context, xStart, height);
CGContextAddLineToPoint(context, xStart + width * 0.25, 0);
CGContextAddLineToPoint(context, xStart + width * 0.75, 0);
CGContextAddLineToPoint(context, xStart + width * 0.50, height);
CGContextClosePath(context);
CGContextFillPath(context);
CGContextRestoreGState(context);
xStart += width;
}
CGContextRestoreGState(context);
}
This is where the animation "magic" happens. Essentially I draw these stripes based off of self.offset
which is somewhere between -STRIPE_WIDTH
and 0
as incremented by a timer. Then, I create a simple loop so that I only create enough stripes to completely fill the progress portion of the view. I also leave 25% of the STRIPE_WIDTH
blank so that the stripes aren't bunched up against each other. Here's the final drawing method drawRightAlignedLabelInRect:
:
- (void)drawRightAlignedLabelInRect:(CGRect)rect {
UILabel *label = [[UILabel alloc] initWithFrame:rect];
label.backgroundColor = [UIColor clearColor];
label.textAlignment = NSTextAlignmentRight;
label.text = [NSString stringWithFormat:@"%.0f%%", self.progress*100];
label.font = [UIFont boldSystemFontOfSize:17];
UIColor *baseLabelColor = [self.color isLighterColor] ? [UIColor blackColor] : [UIColor whiteColor];
label.textColor = [baseLabelColor colorWithAlphaComponent:0.6];
[label drawTextInRect:CGRectOffset(rect, -6, 0)];
}
In this method I create a label with text that is convert from a float (between 0.0
and 1.0
) to a percentage (from 0%
to 100%
). I then either set the color to be dark or light depending on the darkness of the chosen progress color and draw the label in the CGContext
.
Customizability
There are three properties that can be set either directly on an instance of LDProgressView
or beforehand in a UIAppearance
method.
The color will obviously set the general look of the picker. The gradients, stripes, and/or outline colors are determined off of this. The UIAppearance
method would be something like this:
[[LDProgressView appearance] setColor:[UIColor colorWithRed:0.87f green:0.55f blue:0.09f alpha:1.00f]];
This will determine whether the background of the progress view will be a gradient or just the color
property. This UIAppearance
method would look something like this:
[[LDProgressView appearance] setFlat:@NO];
Finally, this will determine whether the stripes will be animated. This UIAppearance
method can also be generically set for all instances of LDProgressView
and looks like this:
[[LDProgressView appearance] setAnimate:@YES];
Conclusion
Whew! That was a long answer. I hope I didn't bore you guys too much. If you just skipped down to here, here's is the gist for drawing in code rather than with images. I think CoreGraphics is a superior way of drawing on iOS if you have the time/experience since it allows for more customization and I believe tends to be faster.
Here's a picture of the final, working product: