Clip CGPath and fill with two different colours - ios

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.

Related

Crosshatch in IOS using CoreGraphics?

How would one crosshatch (apply a set of parallel lines at 45 degrees) across the fill of a shape in IOS using core graphics? Sample code?
(I'm specially interested in use with an MKPolygon in MKMapKit, however for the moment just trying to see if it's possible in a UIView using drawRect?. So fill the background of a UIView with crosshatch'ing)
for swift 3., using approach from #user3230875
final class CrossHatchView: UIView {
// MARK: - LifeCycle
override func draw(_ rect: CGRect) {
// create rect path with bounds that equal to the
// size of a view, in addition it adds rounded corners, this will
// be used later as a canvas for dash drawing
let path:UIBezierPath = UIBezierPath(roundedRect: bounds, cornerRadius: 5)
// specify the new area where the our drawing will be visible
// check [link][1] for more
path.addClip()
// grab the size of drawing area
let pathBounds = path.bounds
// cleanUp rounded rect, that is drawn above
// just remove roundedRect in the words
path.removeAllPoints()
// get start and end point of the line
let p1 = CGPoint(x:pathBounds.maxX, y:0)
let p2 = CGPoint(x:0, y:pathBounds.maxX)
// draw line
path.move(to: p1)
path.addLine(to: p2)
// set line width equal to double width of view
// because we later will draw this line using dash pattern
path.lineWidth = bounds.width * 2
// set dash pattern with some interval
let dashes:[CGFloat] = [0.5, 7.0]
path.setLineDash(dashes, count: 2, phase: 0.0)
// set color for line
UIColor.lightGray.withAlphaComponent(0.5).set()
// actually draw a line using specific
// color and dash pattern
path.stroke()
}
}
result:
Create a UIImage containing your crosshatch pattern in whatever way you want (e.g. by drawing it with Core Graphics or by loading it from a PNG file).
Then use +[UIColor colorWithPatternImage:] (Swift UIColor(patternImage:)) to create a “color” that draws the crosshatch image.
Finally, set the pattern color as your fill color, and fill the shape (presumably by filling a path that outlines the shape, or by using UIRectFill).
If you need more control over the pattern (to change how it's tiled or aligned), you can drop down to the Core Graphics level and use CGPatternCreate and CGColorCreateWithPattern.
Here's what I was talking about over in the Apple Developer Forum:
#import "CrossHatchView.h"
#implementation CrossHatchView
static CGFloat sides = 5.0;
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat xCentre = CGRectGetMidX(bounds);
CGFloat yCentre = CGRectGetMidY(bounds);
CGFloat radius = 0.0;
if (CGRectGetWidth(bounds) > CGRectGetHeight(bounds)) {
radius = CGRectGetHeight(bounds) / 2.0;
} else {
radius = CGRectGetWidth(bounds) / 2.0;
}
CGFloat angleIncrement = 2.0 * M_PI / sides;
CGFloat initialAngle = ( M_PI + (2.0 * M_PI / sides) ) / 2.0;
for (NSUInteger i = 0; i < sides; i++) {
CGFloat angle = initialAngle + i * angleIncrement;
CGFloat x = xCentre + radius * cos(angle);
CGFloat y = yCentre + radius * sin(angle);
CGPoint point = CGPointMake(x, y);
if (i == 0) {
[path moveToPoint:point];
} else {
[path addLineToPoint:point];
}
}
[path closePath];
[[UIColor cyanColor] set];
[path addClip];
CGRect pathBounds = [path bounds];
[path removeAllPoints];
CGPoint p1 = pathBounds.origin;
CGPoint p2 = CGPointMake(CGRectGetMaxX(pathBounds), CGRectGetMaxY(pathBounds));
[path moveToPoint:p1];
[path addLineToPoint:p2];
path.lineWidth = 400.0;
CGFloat dashes[] = { 2.0, 2.0 };
[path setLineDash:dashes count:2 phase:0.0];
[[UIColor blackColor] set];
[path stroke];
}
#end
hey try this sample code which i tried on a 300x300 UIView
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 1.5);
CGContextSetStrokeColorWithColor(context, [UIColor grayColor].CGColor);
int backward=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, backward, 0);
CGContextAddLineToPoint(context, 300, 300-backward);
backward=backward+20;
}
int backwardNegitive=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, 0,backwardNegitive);
CGContextAddLineToPoint(context, 300-backwardNegitive,300);
backwardNegitive=backwardNegitive+20;
}
int forward=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, 300-forward, 0);
CGContextAddLineToPoint(context, 0, 300-forward);
forward=forward+20;
}
int forwardNegative=0;
for (int i=0;i<15; i++)
{
CGContextMoveToPoint(context, 0,300+forwardNegative);
CGContextAddLineToPoint(context,300+forwardNegative,0);
forwardNegative=forwardNegative+20;
}
CGContextStrokePath(context);
}
Hope this help you.

Draw dashed line in iOS

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.

Using Core Graphics from ObjectiveC, how to you curve an arrow around a circle?

Our designer has asked me to recreate this:
Im subclassing UIView, and I've overridden the drawRect command like this:
[super drawRect:frame];
CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y;
CGFloat w = self.frame.size.width;
CGFloat h = self.frame.size.height;
CGFloat lineWidth = lineWidthRequested;
CGPoint centerPoint = CGPointMake(w/2, h/2);
CGFloat radius = radiusRequested;
CGFloat startAngle = 3 * M_PI / 2;
CGFloat endAngle = startAngle + percentage * 2 * M_PI;
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
NO);
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor colorWithRed:239/255.0 green:101/255.0 blue:47/255.0 alpha:1.0].CGColor);
CGContextDrawPath(c, kCGPathFill);
What I ended up with is this:
Gotta still draw the arrowhead. Gonna be easy, right?
After struggling to remember my trig, I found rotation of points around a center on this page:
Rotating a point around an origin in VB
But when I tried translation to objective C to draw the arrowhead, I'm getting very odd results. Here's the code further down in drawRect:
CGFloat triangle[3][2] = {{centerPoint.x + 10, h - (centerPoint.y + radius)},
{centerPoint.x, h - (centerPoint.y + radius + lineWidth/2)},
{centerPoint.x, h - (centerPoint.y + radius - lineWidth/2)}};
for (int idx=0;idx < 3; idx++) {
// translate to origin
triangle[idx][0] -= centerPoint.x;
triangle[idx][1] -= centerPoint.y;
}
CGFloat angDistance = endAngle - startAngle;
CGFloat ct = cos(angDistance);
CGFloat st = sin(angDistance);
for (int idx=0;idx < 3; idx++) {
// rotate
triangle[idx][0] = ct * triangle[idx][0] - st * triangle[idx][1];
triangle[idx][1] = -st * triangle[idx][0] + ct * triangle[idx][1];
}
for (int idx=0;idx < 3; idx++) {
// translate back to position
triangle[idx][0] += centerPoint.x;
triangle[idx][1] += centerPoint.y;
}
NSLog(#"Rotating through %g, %06.1f,%06.1f , ct - %g, st - %g",angDistance, triangle[0][0],triangle[0][1],ct, st);
// XXX todo draw the filled triangle at end.
// draw a red triangle, the point of the arrow
CGContextSetFillColorWithColor(c, [[UIColor greenColor] CGColor]);
CGContextMoveToPoint(c, triangle[0][0], triangle[0][1]);
CGContextAddLineToPoint(c, triangle[1][0], triangle[1][1]);
CGContextAddLineToPoint(c, triangle[2][0], triangle[2][1]);
CGContextFillPath(c);
I was expecting that I make these points, then translate them to an origin, rotate, and then translate them back, I'd be laughing.
However, that's not what's happening...as the percentage increases from 0 to 2pi, the arrowhead draws itself in a vaguely triangular route. When the angDistance is zero or pi, the arrowhead is in the right place. As I head towards pi/2 or 3pi/2 though, the arrowhead heads off towards the lower corners of an enclosing rect.
I must be doing something blatantly stupid, but I can't for the life of me see it.
Any ideas?
Thanks,
-Ken
I'd suggest constructing a path for the entire outline of the desired shape and then "fill" that path with the desired color. That eliminates any risk of any gaps or anything not quite lining up.
Thus, this path might consisting of an arc for the outside of the arrow, two lines for the head of the arrow, an arc back for the inside of the arrow, and then close the path. That might look like:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, self.arrowColor.CGColor);
CGPoint center = CGPointMake(rect.size.width / 2.0, rect.size.height / 2.0);
CGContextMoveToPoint(context, center.x + cosf(self.startAngle) * (self.radius + self.lineWidth / 2.0),
center.y + sinf(self.startAngle) * (self.radius + self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius + self.lineWidth / 2.0, self.startAngle, self.endAngle, !self.clockwise);
CGFloat theta = asinf(self.lineWidth / self.radius / 2.0) * (self.clockwise ? 1.0 : -1.0);
CGFloat pointDistance = self.radius / cosf(theta);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle + theta) * pointDistance,
center.y + sinf(self.endAngle + theta) * pointDistance);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle) * (self.radius - self.lineWidth / 2.0),
center.y + sinf(self.endAngle) * (self.radius - self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius - self.lineWidth / 2.0, self.endAngle, self.startAngle, self.clockwise);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
}
The only trick here was coming up with the right point for the end of the arrow. I've improved the choice to handle fatter arrows a little better, but you should feel free to use whatever you feel is best for your application.
Thus, the following code:
self.arrowView.radius = 100;
self.arrowView.arrowColor = [UIColor blueColor];
self.arrowView.lineWidth = 40;
self.arrowView.startAngle = -M_PI_2;
self.arrowView.endAngle = M_PI;
self.arrowView.clockwise = TRUE;
would yield the following (which I'm animating with a CADisplayLink):
This uses the start angle of zero as meaning the "3 o'clock" position, but you can obviously tweak this as you see fit. But hopefully it illustrates one approach to the problem.
By the way, while I've answered the question of how to do this with CoreGraphics, I wouldn't necessarily suggest doing so. For example, in https://github.com/robertmryan/CircularArrowDemo, I don't implement drawRect, but instead update a CAShapeLayer. By doing this, not only do I avoid drawRect inefficiencies, but one could theoretically also change how you use this CAShapeLayer (e.g. use it as a mask for some UIView, revealing some more interesting color gradation (or other image) behind it).
Here is another solution (not very scalable though). This solution assumes this is like a logo where the angle/percent of circle drawn will not change.
- (void)drawRect:(CGRect)rect {
UIBezierPath *circleOutline = [UIBezierPath bezierPath];
[self.circleColor setStroke];
[circleOutline setLineWidth:self.bounds.size.width*0.15];
[circleOutline addArcWithCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/2-circleOutline.lineWidth/2 startAngle:3*M_PI/2 endAngle:3*M_PI/4 clockwise:YES];
[circleOutline stroke];
[self addArrowView:circleOutline];
}
- (void)addArrowView:(UIBezierPath *)path {
for (int x = 0; x < self.bounds.size.width/2; x++) {
for (int y = self.bounds.size.height/2; y < self.bounds.size.height; y++) {
if ([path containsPoint:CGPointMake(x, y)]) {
// Pythagorean Theorem - We want the diagonal length of the square to be lineWidth, so we need to calculate what size
// to make each side of the square to make the diagonal equal to lineWidth
double sideLength = sqrt((path.lineWidth*path.lineWidth)/2);
UIView *arrowView = [[UIView alloc] initWithFrame:CGRectMake(x-sideLength/2, y-sideLength/2, sideLength, sideLength)];
arrowView.backgroundColor = self.circleColor;
[self addSubview:arrowView];
return;
}
}
}
}
would yield:

Unable to set fill color in CGContext (CGContextSetFillColorWithColor)

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.

Drawing MKMapView Overlay like Google Maps Directions

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.

Resources