iOS 5 changed the way the built-in Google Maps App draws routes:
I would now like to replicate the design of the route overlay in my own app but I am currently only able to draw a plain blue line. I would like to add the 3D-effect with the gradient, borders and the glow. Any ideas on how to accomplish this?
Currently I'm using the following code:
CGContextSetFillColorWithColor(context, fillColor.CGColor);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, lineWidth);
CGContextAddPath(context, path);
CGContextReplacePathWithStrokedPath(context);
CGContextFillPath(context);
Resulting in a rather ugly line:
Thanks!
Update: The solution should work on iOS 4.0 and up.
I think that #ChrisMiles is correct in that the segments are probably being drawn individually. (I initially thought that this might have been doable using CGPatternRef but you don't have any access to the CTM or path endpoints inside the pattern drawing callback.)
With this in mind, here is an exceedingly crude, back-of-the-envelope example of how you might begin such an effort (filling the segments individually). Note that:
gradient colors are guessed
end caps are nonexistent and will need to be separately implemented
some aliasing artifacts remain
not a great deal of attention was paid to performance
Hopefully this can get you started at least (and works through some of the analytic geometry).
- (CGGradientRef)lineGradient
{
static CGGradientRef gradient = NULL;
if (gradient == NULL) {
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGColorRef white = [[UIColor colorWithWhite:1.f
alpha:0.7f] CGColor];
CGColorRef blue = [[UIColor colorWithRed:0.1f
green:0.2f
blue:1.f
alpha:0.7f] CGColor];
CGColorRef lightBlue = [[UIColor colorWithRed:0.4f
green:0.6f
blue:1.f
alpha:0.7f] CGColor];
CFMutableArrayRef colors = CFArrayCreateMutable(kCFAllocatorDefault,
8,
NULL);
CFArrayAppendValue(colors, blue);
CFArrayAppendValue(colors, blue);
CFArrayAppendValue(colors, white);
CFArrayAppendValue(colors, white);
CFArrayAppendValue(colors, lightBlue);
CFArrayAppendValue(colors, lightBlue);
CFArrayAppendValue(colors, blue);
CFArrayAppendValue(colors, blue);
CGFloat locations[8] = {0.f, 0.08f, 0.14f, 0.21f, 0.29f, 0.86f, 0.93f, 1.f};
gradient = CGGradientCreateWithColors(colorSpace,
colors,
locations);
CFRelease(colors);
CGColorSpaceRelease(colorSpace);
}
return gradient;
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
CGContextSetAllowsAntialiasing(context, YES);
CGContextSetShouldAntialias(context, YES);
// Fill background color
[[UIColor whiteColor] setFill];
UIRectFill(rect);
// Build a path
CGFloat strokeWidth = 10.f;
CGContextSetLineWidth(context, strokeWidth);
CGGradientRef gradient = [self lineGradient];
CGPoint points[9] = {
CGPointMake(10.f, 25.f),
CGPointMake(100.f, 100.f),
CGPointMake(100.f, 150.f),
CGPointMake(22.f, 300.f),
CGPointMake(230.f, 400.f),
CGPointMake(230.f, 200.f),
CGPointMake(300.f, 200.f),
CGPointMake(310.f, 160.f),
CGPointMake(280.f, 100.f)
};
for (NSUInteger i = 1; i < 9; i++) {
CGPoint start = points[i - 1];
CGPoint end = points[i];
CGFloat dy = end.y - start.y;
CGFloat dx = end.x - start.x;
CGFloat xOffset, yOffset;
// Remember that, unlike Cartesian geometry, origin is in *upper* left!
if (dx == 0) {
// Vertical to start, gradient is horizontal
xOffset = 0.5 * strokeWidth;
yOffset = 0.f;
if (dy < 0) {
xOffset *= -1;
}
}
else if (dy == 0) {
// Horizontal to start, gradient is vertical
xOffset = 0.f;
yOffset = 0.5 * strokeWidth;
}
else {
// Sloped
CGFloat gradientSlope = - dx / dy;
xOffset = 0.5 * strokeWidth / sqrt(1 + gradientSlope * gradientSlope);
yOffset = 0.5 * strokeWidth / sqrt(1 + 1 / (gradientSlope * gradientSlope));
if (dx < 0 && dy > 0) {
yOffset *= -1;
}
else if (dx > 0 && dy < 0) {
xOffset *= -1;
}
else if (dx < 0 && dy < 0) {
yOffset *= -1;
xOffset *= -1;
}
else {
}
}
CGAffineTransform startTransform = CGAffineTransformMakeTranslation(-xOffset, yOffset);
CGAffineTransform endTransform = CGAffineTransformMakeTranslation(xOffset, -yOffset);
CGPoint gradientStart = CGPointApplyAffineTransform(start, startTransform);
CGPoint gradientEnd = CGPointApplyAffineTransform(start, endTransform);
CGContextSaveGState(context);
CGContextMoveToPoint(context, start.x, start.y);
CGContextAddLineToPoint(context, end.x, end.y);
CGContextReplacePathWithStrokedPath(context);
CGContextClip(context);
CGContextDrawLinearGradient(context,
gradient,
gradientStart,
gradientEnd,
kCGGradientDrawsAfterEndLocation | kCGGradientDrawsBeforeStartLocation);
CGContextRestoreGState(context);
}
CGContextRestoreGState(context);
}
I would say they are drawing a CGPath around the original line, stroking the edges and gradient filling it. The ends are capped by adding a semi circle to the CGPath.
Would be a bit more work than simply drawing a single line and stroking it, but gives them full control over the style of the rendered path.
Related
I used CGContext to draw dashed lines in iOS.
My code is as follow.
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGFloat dashes[] = {5,5};
CGContextSetLineDash(context, 0, dashes, 2);
float startx = x_percentileValues[0];
float starty = orgy-numskipPixels_v*(3-minWeight);
for(int i=0; i<[MeasuredInfos.retval count]; i++) {
float x = numskipPixels_h*hoursBetweenDates*3+orgx;
float y = orgy-([[MeasuredInfos.retval objectAtIndex: i] getWeight] - minWeight)*numskipPixels_v;
CGContextMoveToPoint(context, startx, starty);
CGContextAddLineToPoint(context, x, y);
CGContextStrokePath(context);
startx = x;
starty = y;
}
It is fine if the slope of the line is steep. If not steep, the dashed line has problem as shown in the attached pictures.
I checked in this link, nothing much discussion for the CGContextSetLineDash.
I found the problem. The problem is that I used two CGContextRef in two separate functions. One is in
- (void)drawRect:(CGRect)rect {
//Get the CGContext from this view
CGContextRef context = UIGraphicsGetCurrentContext();
//Set the stroke (pen) color
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
//Set the width of the pen mark
CGContextSetLineWidth(context, 2.0);
float startx = x_percentileValues[0];
float starty = orgy-numskipPixels_v*(3-minWeight);
for(int i=0; i<[MeasuredInfos.retval count]; i++) {
float x = numskipPixels_h*hoursBetweenDates*3+orgx;
float y = orgy-([[MeasuredInfos.retval objectAtIndex: i] getWeight] - minWeight)*numskipPixels_v;
CGContextMoveToPoint(context, startx, starty);
CGContextAddLineToPoint(context, x, y);
CGContextStrokePath(context);
startx = x;
starty = y;
[self drawStar_atX:x andatY:y];
}
}
Another one in drawStar_atX
-(void)drawStar_atX:(float)xCenter andatY:(float)yCenter
{
int aSize = 5.0;
CGFloat color[4] = { 1.0, 0.0, 0.0, 1.0 }; // Red
CGColorRef aColor = CGColorCreate(CGColorSpaceCreateDeviceRGB(), color);
CGContextRef context1 = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context1, aSize);
float w = 15.0;
double r = w / 2.0;
float flip = -1.0;
CGContextSetFillColorWithColor(context1, aColor);
CGContextSetStrokeColorWithColor(context1, aColor);
double theta = 2.0 * M_PI * (2.0 / 5.0); // 144 degrees
CGContextMoveToPoint(context1, xCenter, r*flip+yCenter);
for (NSUInteger k=1; k<5; k++)
{
float x = r * sin(k * theta);
float y = r * cos(k * theta);
CGContextAddLineToPoint(context1, x+xCenter, y*flip+yCenter);
}
CGContextClosePath(context1);
CGContextFillPath(context1);
return;
}
I called drawStar_atX from drawRect. context and context1 in two functions have some problems and start from the second segment the dashed line's width becomes bigger. Actually they are declared in different functions with different widths, shouldn't have any relationship. Now I solved the problem as context and context1 has same width. Still learning why they have some relations.
Pardon me, but my knowledge of CGContext is fairly limited.
I am using the code from the accepted answer HERE to draw stars in a UIView. What I want to achieve is show the stars in 2 different colors (like a rating view). The problem is, I cannot seem to use 2 different colors for CGContextSetFillColorWithColor().
Relevant code:
if (i < 3) {
NSLog(#"__BLACK__");
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
} else {
NSLog(#"__RED__");
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
}
Full code:
- (void) drawRect:(CGRect)rect {
int aSize = 20;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, aSize);
CGFloat xCenter = 15.0;
CGFloat yCenter = 12.5;
float w = 25.0;
double r = w / 2.0;
float flip = -1.0;
for (NSUInteger i = 0; i < 5; i++) {
if (i < 3) {
NSLog(#"__BLACK__");
CGContextSetFillColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
} else {
NSLog(#"__RED__");
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
}
double theta = 2.0 * M_PI * (2.0 / 5.0); // 144 degrees
CGContextMoveToPoint(context, xCenter, r * flip + yCenter);
for (NSUInteger k = 1; k < 5; k++) {
float x = r * sin(k * theta);
float y = r * cos(k * theta);
CGContextAddLineToPoint(context, x + xCenter, y * flip + yCenter);
}
xCenter += 37.5;
}
CGContextClosePath(context);
CGContextFillPath(context);
}
On compilation and execution i get the log :
__BLACK__
__BLACK__
__BLACK__
__RED__
__RED__
However, the fill colors don't seem to take effect, and the result is:
I don't understand what I'm doing wrong here.
When you set the fill color for the context, it's used on every object in that context. In your case, all 5 stars are drawn inside the same context, so only the last color setting (red) persists.
You can probably achieve what you want to achieve with 5 distinct CGPaths drawn in one context.
I'm in the process of creating a custom 'star' control in that you would be able to pass a float into the control as the rating i.e. 2.5, and 2.5 out of 5 stars would be coloured red and the rest, gray.
I'm drawing the stars using a UIBezierPath with 5 points and this is working perfectly. However, as I am using floats, I need to make sure that the decimals are taken into account. I thought that the best way to accomplish this would be by clipping the bezier path to a proportion of the final width, however, this method doesn't seem to have any effect on the drawing itself; the stars are drawn as normal, not taking into account the decimals.
As you probably expected me to say, I have indeed only just started dabbling in CoreGraphics and would like an explanation as to why my method doesn't work and a method to fix it, in order to help with my progression through the framework.
Look forward to hearing some responses!
- (void)drawStarsWithRating:(float)rating maxRating:(float)maxRating yOrigin:(CGFloat)yOrigin inContext:(CGContextRef)context {
float width = MKRGlyphSize;
CGFloat xCenter = MKRLeftBorderPadding + (0.5 * width);
CGFloat yCenter = yOrigin + (0.5 * width);
double r = width / 2.0;
float flip = -1.0;
for (NSUInteger i = 0; i < maxRating; i++) {
CGContextSaveGState(context);
if (i < rating) {
if (self.selected) {
CGContextSetFillColorWithColor(context, RGB(125, 212, 67).CGColor);
}
else {
CGContextSetFillColorWithColor(context, RGB(215, 35, 32).CGColor);
}
}
else {
if (self.selected) {
CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
}
else {
CGContextSetFillColorWithColor(context, RGB(178, 178, 178).CGColor);
}
}
double theta = 2.0 * M_PI * (2.0 / 5.0);
UIBezierPath *bezier = [UIBezierPath bezierPath];
[bezier moveToPoint:CGPointMake(xCenter, r * flip + yCenter)];
for (NSUInteger k = 1; k < 5; k++) {
float x = r * sin(k * theta);
float y = r * cos(k * theta);
[bezier addLineToPoint:CGPointMake(x + xCenter, y * flip + yCenter)];
}
[bezier setLineWidth:1.0f];
[bezier setLineJoinStyle:kCGLineJoinMiter];
[bezier closePath];
[bezier fill];
if (rating - floorf(rating) > 0) {
CGRect clipRect = CGRectMake(xCenter, yOrigin, width * (rating - floorf(rating)), width);
CGContextClipToRect(context, clipRect);
}
xCenter += width;
CGContextRestoreGState(context);
}
}
One problem I noticed is that you [fill] the path only once. this means for fractional stars you would've only seen one half of the star and not the other half. In the code below, each star is filled with white, and then if any portion of the star is within the rating, then it is filled again with blue.
I also noticed that the clipping rectangle you were using started its X at xCenter instead instead of the lefthand side of the star.
I also adjusted the math a bit to calculate the % filled for each star more consistently.
- (void)drawStarsWithRating:(float)rating maxRating:(float)maxRating yOrigin:(CGFloat)yOrigin inContext:(CGContextRef)context {
float width = MKRGlyphSize;
CGFloat xCenter = MKRLeftBorderPadding + (0.5 * width);
CGFloat yCenter = yOrigin + (0.5 * width);
double r = width / 2.0;
float flip = -1.0;
for (NSUInteger i = 0; i < maxRating; i++) {
CGContextSaveGState(context);
// for clarity, i removed the colors from the top
// and ignore selected state. i use blue/white
// colors hard coded below
//
// you can easily change those colors just as you
// had before
// create star path
double theta = 2.0 * M_PI * (2.0 / 5.0);
UIBezierPath *bezier = [UIBezierPath bezierPath];
[bezier moveToPoint:CGPointMake(xCenter, r * flip + yCenter)];
for (NSUInteger k = 1; k < 5; k++) {
float x = r * sin(k * theta);
float y = r * cos(k * theta);
[bezier addLineToPoint:CGPointMake(x + xCenter, y * flip + yCenter)];
}
[bezier setLineWidth:1.0f];
[bezier setLineJoinStyle:kCGLineJoinMiter];
[bezier closePath];
// fill background of star with white
[[UIColor whiteColor] setFill];
[bezier fill];
// calculate the percentage of this star
// that we should fill
CGFloat currStar = i;
CGFloat percentOfStar;
if(rating > currStar){
// at least some of the star should be filled
percentOfStar = rating - currStar > 0 ? rating - currStar : 0;
percentOfStar = percentOfStar > 1 ? 1 : percentOfStar;
}else{
// none of the star should be filled
percentOfStar = 0;
}
if (percentOfStar) {
// if we need at least a little filling, then clip to that % of the star
// notice (xCenter - .5*width) to align the clipRect to the left side of
// the star.
// now fill the selected portion of the star with blue
CGRect clipRect = CGRectMake(xCenter - .5*width, yOrigin, width * (percentOfStar), width);
CGContextClipToRect(context, clipRect);
[[UIColor blueColor] setFill];
[bezier fill];
}
xCenter += width;
CGContextRestoreGState(context);
}
}
You could do it slightly differently:
fill the background of your view with the background color
Use the percentOfStar to create a rectangle path that reflects the rating.
Use the star path to clip.
I have a for loop inside the drawRect method that draws a number of circles to fill the screen. I'm trying to make it so each circle has a new random stroke. For some reason nothing is showing up. Here is my randomColor method:
-(UIColor *) randomColor
{
int red, green, blue, alpha;
red = arc4random_uniform(255);
green = arc4random_uniform(255);
blue = arc4random_uniform(255);
alpha = arc4random_uniform(255);
UIColor *colorToReturn = [[UIColor alloc] initWithRed:red green:green blue:blue alpha:alpha];
return colorToReturn;
}
and I try implementing it here:
-(void) drawRect:(CGRect)rect
{
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
// Firgure out the center of the bounds rectangle
CGPoint center;
center.x = bounds.origin.x + bounds.size.width / 2.0;
center.y = bounds.origin.y + bounds.size.height / 2.0;
// The radius of the circle should be nearly as big as the view
float maxRadius = hypot(bounds.size.width, bounds.size.height) / 2.0;
// The thickness of the line should be 10 points wide
CGContextSetLineWidth(ctx, 10);
// The color of the line should be gray (red/green/blue = 0.6, alpha = 1.0)
// CGContextSetRGBStrokeColor(ctx, 0.6, 0.6, 0.6, 1.0);
// The same as
// [[UIColor colorWithRed:0.6 green:0.6 blue:0.6 alpha:1.0] setStroke];
// The same as
// [[UIColor redColor] setStroke];
// Draw concentric circles from the outside in
for (float currentRadius = maxRadius; currentRadius > 0; currentRadius -= 20) {
// Add a path to the context
CGContextAddArc(ctx, center.x, center.y, currentRadius, 0.0, M_PI * 2.0, YES);
[[self randomColor] setStroke];
// Perform drawing instructions; removes path
CGContextStrokePath(ctx);
}
UIColor takes a float between 0 and 1 as a value for its RGB components:
UIColor *colorToReturn = [[UIColor alloc] initWithRed:red/255.0 green:green/255.0 blue:blue/255.0 alpha:alpha];
I use the two macros below to get a random color. The first one is a straightforward macro that I often use while setting colors. The second one returns a random color using it:
#define _RGB(r,g,b,a) [UIColor colorWithRed:r/255.0 green:g/255.0 blue:b/255.0 alpha:a]
#define kCLR_RANDOM_COLOR _RGB(arc4random()%255, arc4random()%255, arc4random()%255, 1)
I am trying to code a routine that would allow me to create a UIImage that is a rectangle filled with stripes on a gradient background.
My code is below and works fine for most of the cases I tried it out. Interestingly, and this is the hook, it doesn't work when I pass it 21 as height and 5 as stripedWidth.
Once I do this, the stripes appear as they should... horizontally.. but vertically they start at like (y=) -40 and end at about (y=) 4 or so. To see this better, each this image showing the effect in question:
Has anyone any idea why this is happening, or even better, what I can do against it?
-(UIImage*) stripedTextureWithStartingColor:(UIColor*) startColor withEndingColor:(UIColor*) endColor withHeight:(NSUInteger) height withStripeWidth:(NSUInteger) stripeWidth withStripeColor:(UIColor*) stripedColor {
CGSize size = CGSizeMake(2 * stripeWidth, height);
UIGraphicsBeginImageContext(size);
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSaveGState(context);
#try {
CGContextSetAllowsAntialiasing(context, true);
CGContextSetShouldAntialias(context, true);
NSArray* colors = [NSArray arrayWithObjects:(id) startColor.CGColor, (id) endColor.CGColor, nil];
CGFloat locations[2];
locations[0] = 0.0;
locations[1] = 1.0;
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
CGGradientRef gradient = CGGradientCreateWithColors(colorSpace, (__bridge CFArrayRef) colors, locations);
CGColorSpaceRelease(colorSpace);
CGContextDrawLinearGradient(context, gradient, CGPointZero, CGPointMake(0, size.height - 1), 0);
CGGradientRelease(gradient);
CGContextFillPath(context);
int lineWidth = (int) stripeWidth;
CGContextSetLineWidth(context, lineWidth / 2);
int lineCount = (float) size.height / (float) lineWidth;
lineCount -= 2;
for (int i=0; i<lineCount; i++) {
CGContextSetStrokeColorWithColor(context, stripedColor.CGColor);
float x1 = -size.height + i * 2 * lineWidth - lineWidth;
float y1 = size.height - 1 + lineWidth;
float x2 = -size.height + i * 2 * lineWidth + size.height - lineWidth;
float y2 = -lineWidth;
CGContextMoveToPoint(context, x1, y1);
CGContextAddLineToPoint(context, x2, y2);
CGContextStrokePath(context);
}
UIColor* lineTopColor = [[UIColor whiteColor] colorWithAlphaComponent:0.9];
UIColor* lineBottomColor = [[UIColor darkGrayColor] colorWithAlphaComponent:0.5];
CGContextSetStrokeColorWithColor(context, lineTopColor.CGColor);
CGContextMoveToPoint(context, 0, 0);
CGContextAddLineToPoint(context, size.width + 1, 0);
CGContextStrokePath(context);
CGContextSetStrokeColorWithColor(context, lineBottomColor.CGColor);
CGContextMoveToPoint(context, 0, size.height - 1);
CGContextAddLineToPoint(context, size.width + 1, size.height - 1);
CGContextStrokePath(context);
}
#finally {
CGContextRestoreGState(context);
}
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
Found it, my algorithm was slightly incorrect with the starting of the lines