I'm drawing an arbitrary line with Core Graphics with a width of 4 pixels, now I would like this line to have a 1 pixel outline of another colour. I can't see any CG functions that would achieve this "out of the box" but I'm looking for suggestions on how it could be done. This is my existing code:
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, 4.0);
CGPoint curPoint = [(NSValue*)[points objectAtIndex:0] CGPointValue];
CGContextMoveToPoint(context, curPoint.x, curPoint.y);
for( int i = 1; i < [points count]; i++ ) {
curPoint = [(NSValue*)[points objectAtIndex:i] CGPointValue];
CGContextAddLineToPoint(context, curPoint.x, curPoint.y);
CGContextStrokePath(context);
CGContextMoveToPoint(context, curPoint.x, curPoint.y);
}
This produces the single line. I would like to produce a 4px line with a 1px line highlighting the 4px line like this:
iOS 5.0 added a new feature CGPathCreateCopyByStrokingPath() that does what you want.
First create a CGPathRef for the black path, and then create a copy of it with CGPathCreateCopyByStrokingPath().
This will give you a new path, which you can fill in black and stroke in red, to get what you want.
Also, creating paths is a bit slow. You should avoid creating paths while performing screen drawing. All your paths should be stored in RAM and ready to go before you start drawing to the screen. drawRect: should only draw the path, not create it.
Similar to what Abhi Beckert suggests, but you can use this function:
CGContextReplacePathWithStrokedPath(CGContextRef c);
Which is present in older SDKs too - iOS 4.1, MacOS X 10.6, for example.
Also it is better to create the whole path and then stroke it (or stroke and fill in the same time) at the end - in other words no need to have CGContextStrokePath inside the loop.
I am afraid you will have to draw the path again with the line width set to 1. If you want it to be on the outside of your 4 pixel path, you will have to adjust your path accordingly.
Edit: One other option comes to mind - you can stroke a pattern - see Apple's QuartzDemo for an example how.
To add an answer to my own question, it can be done by drawing a line a few pixels wider in the highlight colour followed by the actual line on top. This produces the outline effect.
There isn't a built-in way to convert a stroke to a path, and then stroke that path. That said, you may be able to approximate this by drawing the line twice: once with a 6 pixel stroke (4 pixels + 1 on each side) and then again with a 4 pixel stroke in a different color
Similar to:
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint curPoint = [(NSValue*)[points objectAtIndex:0] CGPointValue];
CGContextMoveToPoint(context, curPoint.x, curPoint.y);
for( int i = 1; i < [points count]; i++ ) {
curPoint = [(NSValue*)[points objectAtIndex:i] CGPointValue];
CGContextAddLineToPoint(context, curPoint.x, curPoint.y);
}
// Set your 1 pixel highlight color here using CGContextSetRGBStrokeColor or equivalent
// CGContextSetRGBStrokeColor(...)
CGContextSetLineWidth(context, 6.0);
CGContextStrokePath(context);
// Set your 4 pixel stroke color here using CGContextSetRGBStrokeColor or equivalent
// CGContextSetRGBStrokeColor(...)
CGContextSetLineWidth(context, 4.0);
CGContextStrokePath(context);
Another idea would be setting up a shadow via CGContextSetShadowWithColor(context, CGSizeZero, 1.0, yourHighlightColorHere) prior to drawing the stroke, although this won't draw the highlight color with full opacity. (I also can't remember if shadows property shadow strokes - I have only used them with fills)
Related
OK, so I needed a rounded triangle. So what I did was use a technique similar to what I've used in other vector drawing programs. Draw the triangle and use the stroke to create the rounded corners. Worked like a charm too, until I needed to reduce the alpha of the color used to fill and stroke the UIBezierPath. For some reason I keep getting this inset outline that isn't the same color as the Fill and Stroke. Somehow the alpha value isn't being respected. Maybe I'm overlooking something silly here, but try as I might I can't get the triangle all one color with a alpha value lower than 1. This is what I get:
And heres the simple code:
- (void)drawRect:(CGRect)rect
{
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint: CGPointMake(63.5, 10.5)];
[path addLineToPoint: CGPointMake(4.72, 119.5)];
[path addLineToPoint: CGPointMake(122.28, 119.5)];
[path addLineToPoint: CGPointMake(63.5, 10.5)];
[path closePath];
path.miterLimit = 7;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
path.lineWidth = 8;
UIColor *whiteAlph5 = [UIColor colorWithWhite:0.6 alpha:0.5];
[whiteAlph5 setFill];
[whiteAlph5 setStroke];
[path fill];
[path stroke];
}
I can't understand why the line would be anything other than the "whiteAlpha5" if that's the only color I've set for both fill and stroke. I suppose I can just draw the rounded triangle out adding the curves to to corners, but I'm just curious as to why this happens.Thanks in advance...
If you must have the stroke, alter your call to [UIBezierPath stroke] like so:
[path fill];
[path strokeWithBlendMode:kCGBlendModeCopy alpha:1.0];
This should achieve the effect you want (I think - haven't been able to test it)
This is a bit of a guess, but I think you're seeing here is essentially two layers of semitransparent white, one drawn on top of the other. When the triangle is just filled in, it would be what you're expecting. When you stroke, it's drawing the same colour - but it's adding it on top of the existing colour, not replacing it, which is the effect you might expect if you've done this before in paint programs or similar. Thus, where the stroke and fill overlap, you're getting a stronger white than you're after. Just using fill by itself could solve this, but might not get the rounded effect you're after.
If you need a visual demonstration of what I mean, you can do this in Photoshop. Create a new image with a black background and create a new layer above it, set to 50% opacity. Draw a white square on it (which will look grey due to the opacity). Then, without changing layers, draw a line through it. You won't see the line, because it's replacing the existing colour - this is what you expected to happen with your code. Then, add another layer above it, also set to 50% opacity. Draw a line on this layer, through the square. You'll see the line as a brighter grey. This is additive, the white overlapping on both layers - the effect that your code is creating.
The line is because your stroke and your fill are drawing to the same pixels. Since both the stroke and the fill are partially transparent, the colors accumulate.
One way to fix this is to just create a path that outlines your rounded triangle, and fill it without stroking it.
Here's the interface for a category that creates a path outlining a rounded polygon:
#interface UIBezierPath (MyRoundedPolygon)
+ (UIBezierPath *)my_roundedPolygonWithSides:(int)sides center:(CGPoint)center
vertexRadius:(CGFloat)vertexRadius cornerRadius:(CGFloat)cornerRadius
rotationOffset:(CGFloat)rotationOffset;
#end
Here's the implementation:
#implementation UIBezierPath (MyRoundedPolygon)
static CGPoint vertexForPolygon(int sides, CGPoint center, CGFloat circumradius, CGFloat index) {
CGFloat angle = index * 2 * M_PI / sides;
return CGPointMake(center.x + circumradius * cosf(angle),
center.y + circumradius * sinf(angle));
}
+ (UIBezierPath *)my_roundedPolygonWithSides:(int)sides center:(CGPoint)center
vertexRadius:(CGFloat)vertexRadius cornerRadius:(CGFloat)cornerRadius
rotationOffset:(CGFloat)rotationOffset
{
CGFloat circumradius = vertexRadius + cornerRadius;
CGPoint veryLastVertex = vertexForPolygon(sides, center, circumradius, rotationOffset - 1);
CGPoint currentVertex = vertexForPolygon(sides, center, circumradius, rotationOffset);
CGMutablePathRef cgpath = CGPathCreateMutable();
CGPathMoveToPoint(cgpath, NULL, (veryLastVertex.x + currentVertex.x) / 2,
(veryLastVertex.y + currentVertex.y) / 2);
for (CGFloat i = 0; i < sides; ++i) {
CGPoint nextVertex = vertexForPolygon(sides, center, circumradius,
i + 1 + rotationOffset);
CGPathAddArcToPoint(cgpath, NULL, currentVertex.x, currentVertex.y,
nextVertex.x, nextVertex.y, cornerRadius);
currentVertex = nextVertex;
}
CGPathCloseSubpath(cgpath);
UIBezierPath *path = [self bezierPathWithCGPath:cgpath];
CGPathRelease(cgpath);
return path;
}
#end
Here's how you use it:
#implementation MyView
- (void)drawRect:(CGRect)rect
{
CGRect bounds = self.bounds;
CGPoint center = CGPointMake(CGRectGetMidX(bounds), CGRectGetMidY(bounds));
UIBezierPath *path = [UIBezierPath my_roundedPolygonWithSides:3 center:center
vertexRadius:70 cornerRadius:8 rotationOffset:0.25];
[[UIColor colorWithWhite:0.6 alpha:0.5] setFill];
[path fill];
}
#end
And here's the result:
Note that setting rotationOffset to 0.25 rotated the triangle one quarter turn. Setting it to zero will give you a right-pointing triangle.
I am again here with two Question, both inter-related
I want to draw embossed lines with core graphics.
Can any one suggest me how to give inner shadows to line drawn on touch events?
Even for drawing outer shadows.
Shadow drawn overlaps in between. and line drawn with colors other than black is like worm..
Can any one help me?
Following image illustrates what I mean to explain for Question 2:
Shadows creates are not even. They darken at some points
I am adding the code that I am using to draw lines..
for (int i=0; i<[currentPath count]; i++)
{
CGPoint mid1 = [[self midPoint:[currentPath objectAtIndex:i+1] :[currentPath objectAtIndex:i]] CGPointValue];
CGPoint mid2 = [[self midPoint:[currentPath objectAtIndex:i+2] :[currentPath objectAtIndex:i+1]] CGPointValue];
CGContextMoveToPoint(context, mid1.x, mid1.y);
CGContextAddQuadCurveToPoint(context, [[currentPath objectAtIndex:i+1] CGPointValue].x, [[currentPath objectAtIndex:i+1] CGPointValue].y, mid2.x, mid2.y);
CGContextSetShadow(context, CGSizeMake(-2, -2), 3);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetStrokeColorWithColor(context,[color CGColor]);
CGContextSetLineWidth(context, linewidth);
CGContextStrokePath(context);
i+=2;
}
I found my solution..
Problem was very silly...
I was stoking path on every iteration which was creating the issue.. Now I can draw even with alpha less then 1..
CGContextStrokePath(context);
This line goes outside for loop.. And all is working fine now :)
For your overlapping shadows, you want a transparency layer to composite them first. See Transparency Layers in the Quartz 2D Programming Guide.
It looks like you are drawing the path by using a series of circles.
The trouble is that you have set the shadow on the individual dots, and that's why you are getting the strange effects.
A possible solution - dont put a shadow on the dots, put on the path: duplicate the line that you have drawn, draw it in a different colour, offset it and put in under your actual line.
Alternatively, if you are using layers - have a look at shadow paths.
I'm making a legend for a graph that will basically look like this:
[ ] Line 1
[ ] Line 2
[ ] Line 3
The boxes on the left need to be the same color as the lines on the graph.
Anyhow, all I need to know, is whether it's faster to draw the boxes with Core Graphics or just make some pngs with GIMP for the squares and include them.
Use UIView for each legend and set their background color to the color you want.
Both approaches are fast enough that it shouldn't make a difference. However, using Core Graphics has the advantage that you're a lot more flexible, e.g. when you later decide that you need additional colors. Plus, your app will be smaller, because you don't have to include the PNG files.
Drawing boxes is a snap! I would go with Core Graphics everyday, especially since you get retina support for free.
As can be seen in this example you can do it using UIKit only classes:
// Setup colors
[myBoxColor setFill];
[myBoxBorderColor set];
// Setup a path for the box
UIBezierPath* path = [UIBezierBath bezierPathWithRect:rectOfTheBox];
path.lineWidth = 2;
// Draw!
[path fill];
[path stroke];
One warning; stroke fills using the edges of the path as the center of the line. So you will get a blurry line if you stroke a path with integral rect with a 1 point line width.
You can remedy this is you want a 1 point line for the border by doing something like this:
CGRect strokeRect = UIEdgeInsetsInsetRect(rectOfTheBox,
UIEdgeInsetsMake(0.5f,0.5f,0.5f,0.5f));
UIBezierPath* path = [UIBezierPath bezierPathWithRect:strokeRect];
[path stroke];
On iOS, Core Graphics is quite simple to use. In your view's drawRect: method, just do this to draw a square:
- (void)drawRect:(CGRect)frame {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetRGBFillColor(context, 0.5, 0.5, 0.5, 1); // gray
CGContextFillRect(context, CGRectMake(10, 10, 20, 20)); // our rect is {10,10,20,20)
// draw a line
CGContextSetRGBStrokeColor(context, 1, 0, 0, 1);
CGContextBeginPath(context);
CGContextMoveToPoint(context, startX, startY);
CGContextAddLineToPoint(context, endX, endY);
CGContextStrokePath(context);
}
Hope this helps!
I was wondering if there is a standard method in iOS to produce the numbered bubble icon for unread messages as the ones used in mail for iphone and mac.
I'm not talking about the red dots on the application item which is done with badgevalue but about the blue bubble beside the mailboxes.
Of course one can do it manually using coregraphics but it's harder to match the dimensions and color of the standard ones used in mail etc.
here are three ways to do this, in order of difficulty..
screen shot your mail app from your iphone, send the image into photoshop, extract the blue dot and use it as an image in your app. To use it in a tableviewcell, you just set the imageView.image = [UIImage imageName:#"blueDot.png"];
same as #1, except save the image as a grayscale, this way you can use Quartz and overlay your own colors on top of it. so you can make that dot any color you want. Very cool stuff.
Use Quartz to draw the whole thing. Its really not that hard. Let me know if you would like some code for that.
OK, twist my arm... here is the code to draw your own gradient sphere... from quartz.
Make a class that inherits from UIView. add the following code
static float RADIANS_PER_DEGREE=0.0174532925;
-(void) drawInContext:(CGContextRef) context
{
// Drawing code
CGFloat radius = self.frame.size.width/2;
CGFloat start = 0 * RADIANS_PER_DEGREE;
CGFloat end = 360 * RADIANS_PER_DEGREE;
CGPoint startPoint = CGPointMake(0, 0);
CGPoint endPoint = CGPointMake(0, self.bounds.size.height);
//define our grayscale gradient.. we will add color later
CGFloat cc[] =
{
.70,.7,.7,1, //r,g,b,a of color1, as a percentage of full on.
.4,.4,.4,1, //r,g,b,a of color2, as a percentage of full on.
};
//set up our gradient
CGGradientRef gradient;
CGColorSpaceRef rgb = CGColorSpaceCreateDeviceRGB();
gradient = CGGradientCreateWithColorComponents(rgb, cc, NULL, sizeof(cc)/(sizeof(cc[0])*4));
CGColorSpaceRelease(rgb);
//draw the gray gradient on the sphere
CGContextSaveGState(context);
CGContextBeginPath(context);
CGContextAddArc(context, self.bounds.size.width/2, self.bounds.size.height/2, radius,start,end , 0);
CGContextClosePath(context);
CGContextClip(context);
CGContextAddRect(context, self.bounds);
CGContextDrawLinearGradient(context, gradient, startPoint, endPoint, kCGGradientDrawsBeforeStartLocation);
CGGradientRelease(gradient);
//now add our primary color. you could refactor this to draw this from a color property
UIColor *color = [UIColor blueColor];
[color setFill];
CGContextSetBlendMode(context, kCGBlendModeColor); // play with the blend mode for difference looks
CGContextAddRect(context, self.bounds); //just add a rect as we are clipped to a sphere
CGContextFillPath(context);
CGContextRestoreGState(context);
}
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
[self drawInContext:context];
}
If you want to use a graphic resource from iOS, you can find it using the UIKit-Artwork-Extractor tool. Extract everything to the desktop and find the one you want. For example, the red badge for notifications is called SBBadgeBG.png. I don't know which one you mean, so search for it yourself :P
This is what I did to use a badge, the procedure is exactly the same to show a bubble in a subview of your table:
// Badge is an image with 14+1+14 pixels width and 15+1+15 pixels height.
// Setting the caps to 14 and 15 preserves the original size of the sides, so only the pixel in the middle is stretched.
UIImage *image = [UIImage imageNamed:#"badge"];
self.badgeImage = [image stretchableImageWithLeftCapWidth:(image.size.width-1)/2 topCapHeight:(image.size.height-1)/2];
// what size do we need to show 3 digits using the given font?
self.badgeFont = [UIFont fontWithName:#"Helvetica-Bold" size:13.0];
CGSize maxStringSize = [[NSString stringWithString:#"999"] sizeWithFont:self.badgeFont];
// set the annotation frame to the max needed size
self.frame = CGRectMake(0,0,
self.badgeImage.size.width + maxStringSize.width,
self.badgeImage.size.height + maxStringSize.height);
and then override the method drawRect: of your view to paint the badge and the numbers inside:
- (void)drawRect:(CGRect)rect {
// get the string to show and calculate its size
NSString *string = [NSString stringWithFormat:#"%d",self.badgeNumber];
CGSize stringSize = [string sizeWithFont:self.badgeFont];
// paint the image after stretching it enough to acommodate the string
CGSize stretchedSize = CGSizeMake(self.badgeImage.size.width + stringSize.width,
self.badgeImage.size.height);
// -20% lets the text go into the arc of the bubble. There is a weird visual effect without abs.
stretchedSize.width -= abs(stretchedSize.width *.20);
[self.badgeImage drawInRect:CGRectMake(0, 0,
stretchedSize.width,
stretchedSize.height)];
// color of unread messages
[[UIColor yellowColor] set];
// x is the center of the image minus half the width of the string.
// Same thing for y, but 3 pixels less because the image is a bubble plus a 6px shadow underneath.
float height = stretchedSize.height/2 - stringSize.height/2 - 3;
height -= abs(height*.1);
CGRect stringRect = CGRectMake(stretchedSize.width/2 - stringSize.width/2,
height,
stringSize.width,
stringSize.height);
[string drawInRect:stringRect withFont:badgeFont];
}
I don't know anything about graphics or drawing so I assume that's why I can't figure out what to look for to solve this.
Here's what I got in my UITableViewCell's drawRect
- (void)drawRect:(CGRect)rect {
[super drawRect:rect];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetRGBStrokeColor(ctx, 0.0, 0.0, 0.0, 1.0);
CGContextSetLineWidth(ctx, 0.25);
for (int i = 0; i < [columns count]; i++) {
CGFloat f = [((NSNumber*) [columns objectAtIndex:i]) floatValue];
CGContextMoveToPoint(ctx, f, 0);
CGContextAddLineToPoint(ctx, f, self.bounds.size.height);
}
CGContextStrokePath(ctx);
}
I think this should be drawing a black line but the line is gray, blending with what's in the background. How can I draw a solid line that is not influenced by a background image? I've tried all the blend mode's thinking maybe one would be like a blend mode none, because I don't want any blending, but all draw lines that blend in some way with the background.
You can try turning off antialiasing using CGContextSetShouldAntialias(). Also, 0.25 seems like a pretty thin line. Setting it to 1.0 might give you what you're looking for.
Two problems:
* You're drawing a black line that's 0.25 pixels (or 0.5 pixels on iPhone 4) wide; antialiasing will make this appear semi-transparent. Try setting the line width to 1.
* If f is an integer, then the center of the line is aligned on a pixel boundary (you want it on a pixel center). Try adding 0.5f.
If you want thinner lines on iPhone 4, then check if self.layer.scale == 2 and if so, set the line width to 0.5 and add 0.25f.
Alternatively, set the fill colour and call CGContextFillRect() instead, which means you don't have to worry about pixel centers.
UIView *line = [[UIView alloc]initWithFrame:CGRectMake(100,0,1,44)];
line.backgroundColor = [UIColor colorWithRed:244.0f/255.0f green:244.0f/255.0f blue:244.0f/255.0f alpha:1];
[cell addSubview:line];