Question

I wonder how to make a progress view that looks like the one in KAYAK app (it's a travel app to search for flights and hotels), screenshot:

enter image description here

i dig into the resources of KAYAK app on a jailbroken iPhone and found the following 3 images that construct this progress view:

progressbar-background@2x.png

enter image description here

progressbar-gradient@2x.png

enter image description here

progressbar-overlay@2x.png

enter image description here

giving that the progress view has a moving overlay images that moves repeatedly along with the gradient image.

any ideas or sample code would be highly appreciated.

Was it helpful?

Solution

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.

  • Color

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]];
  • Flat

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];
  • Animate

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:

LDProgressView

OTHER TIPS

You could use NSTimer to update the "offset" at which you start rendering the overlay. You can find an example here. It's OS X control and it uses custom drawing, not images, but the principle is the same.

Adjust the color and thickness and you are good to go! https://www.cocoacontrols.com/controls/ylprogressbar

This one seems good too... but I have not use it yet. https://github.com/JonasGessner/JGProgressView

Good luck!

Here is some of example which you can just modify some of code and getting the progress view as you want : check Example1 and Example2

The Closest related Progress Bar in your case is ADVProgressBar.

Here, you need to do a little bit modification. Like, you have to change the Back ground image and you can add those Rounded corners. After those changes, the progress bar will look exactly like what you mentioned in your Question.


Screenshot :

enter image description here

You can use this custom cocoa control

ADV control

Try Cocoa Controls for the ProgressView....... as for the overlaying images, you need to use a NSTimer that relays in absolute sync with the progressview

When you set up your UIProgressView (self.progress) us the following set up to get the same look:

        UIImage *mynd =[[UIImage imageNamed:@"KgcoJ.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(11, 13, 11, 13)];
        UIImage *bakMynd =[[UIImage imageNamed:@"UoS0A.png"] resizableImageWithCapInsets:UIEdgeInsetsMake(11, 13, 11, 13)];
        [self.progress setProgressImage:mynd];
        [self.progress setTrackImage:bakMynd];

Note you may have to change the image names and the numbers in the UIEdgeInsetsMake depending on your image sizes. That will give you the following: Progress bar with Images

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top