custom annotation view for maps - ios

I am trying to draw an annotation for map , my view is a subclass of MKAnnotationView
I need a shape something like shown below
What I am getting is like this :
Here is the code which I am using :
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}

You can achieve the desired effect if you replace your lines with quad curves:
- (void)drawRect:(CGRect)rect
{
CGContextRef ctx= UIGraphicsGetCurrentContext();
UIGraphicsPushContext(ctx);
CGRect bounds = [self bounds];
CGPoint topLeft = CGPointMake(CGRectGetMinX(rect), CGRectGetMinY(rect));
CGPoint topRight = CGPointMake(CGRectGetMaxX(rect), CGRectGetMinY(rect));
CGPoint midBottom = CGPointMake(CGRectGetMidX(rect), CGRectGetMaxY(rect));
CGFloat height = bounds.size.height;
CGFloat width = bounds.size.width;
//draw semi circle
CGContextBeginPath(ctx);
CGContextAddArc(ctx, width/2, height/2, width/2, 0 ,M_PI, YES);
//draw bottom cone
CGContextAddQuadCurveToPoint(ctx, topLeft.x, height * 2 / 3, midBottom.x, midBottom.y);
CGContextAddQuadCurveToPoint(ctx, topRight.x, height * 2 / 3, topRight.x, topRight.y + height/2);
// CGContextAddLineToPoint(ctx, midBottom.x, midBottom.y);
// CGContextAddLineToPoint(ctx, topRight.x, topRight.y + height/2); // mid right
CGContextClosePath(ctx);
CGContextSetFillColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextFillPath(ctx);
UIGraphicsPopContext();
}
This employs a quadratic bezier curve, which is a good curve when you don't want inflection points. You can achieve a similar curve with the cubic bezier, but if you're not careful with your control points, you can get undesired inflection points. The quadratic curve is just a little easier with only one control point per curve.
By choosing control points with x values the same as the start and end of the semicircle, it ensures a smooth transition from the circle to the curve leading down to the point. By choosing y values for those control points which are relatively close to the start and end of the semicircle, it ensures that the curve will transition quickly from the semicircle to the point. You can adjust these control points, but hopefully this illustrates the idea.
For illustration of the difference between these two types of curves, see the Curves section of the Paths chapter of the Quartz 2D Programming Guide.

Related

iOS - 2D line drawing on negative coordinates

I am developing an app in which user can draw lines. The desktop version of the same app is able to draw lines on negative coordinates and I would like to have same capability in iOS app too.
What I've tried so far:-
I have a UIViewController inside which I have overridden - (void)drawRect:(CGRect)rect and drawn the line. Here is the code I am using:-
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextClearRect(context, self.bounds);
CGPoint startPoint = CGPointMake(self.startHandle.frame.origin.x + self.startHandle.frame.size.width/2, self.startHandle.frame.origin.y + self.startHandle.frame.size.height/2);
CGPoint endPoint = CGPointMake(self.endHandle.frame.origin.x + self.endHandle.frame.size.width/2, self.endHandle.frame.origin.y + self.endHandle.frame.size.height/2);
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
CGContextSetLineWidth(context, 2.0f);
CGContextMoveToPoint(context, startPoint.x, startPoint.y ); //start at this point
CGContextAddLineToPoint(context, endPoint.x, endPoint.y); //draw to this point
It works very well until I have startPoint.x as a negative value. With negative value, I don't see the portion of line that is behind 0,0 coordinates. Any help or information is appreciated.
Following on from what #matt said (he has since deleted his answer, shame as it was still useful), you simply want to apply a transform to the context in order to draw in a standard cartesian coordinate system, where (0,0) is the center of the screen.
The context provided to you from within drawRect has its origin at the top left of the screen, with the positive y-direction going down the screen.
So first, we'll want to first flip the y-axis so that the positive y-direction goes up the screen.
We can do this by using CGContextScaleCTM(context, 1, -1). This will reflect the y-axis of the context. Therefore, this reflected context will have a y-axis where the positive direction goes up the screen.
Next, we want to translate the context to the correct position, so that (0,0) corresponds to the center of the screen. We should therefore shift the y-axis down by half of the height (so that 0 on the y-axis is now at the center), and the x-axis to the right by half of the width.
We can do this by using CGContextTranslateCTM(context, width*0.5, -height*0.5), where width and height are the width and height of your view respectively.
Now your context coordinates will look like this:
You could implement this into your drawRect like so:
-(void) drawRect:(CGRect)rect {
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat width = self.bounds.size.width;
CGFloat height = self.bounds.size.height;
// scale and translate to the standard cartesian coordinate system where the (0,0) is the center of the screen.
CGContextScaleCTM(ctx, 1, -1);
CGContextTranslateCTM(ctx, width*0.5, -height*0.5);
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 2.0);
// add y-axis
CGContextMoveToPoint(ctx, 0, -height*0.5);
CGContextAddLineToPoint(ctx, 0, height*0.5);
// add x-axis
CGContextMoveToPoint(ctx, -width*0.5, 0);
CGContextAddLineToPoint(ctx, width*0.5, 0);
// stroke axis
CGContextStrokePath(ctx);
// define start and end points
CGPoint startPoint = CGPointMake(-100, 100);
CGPoint endPoint = CGPointMake(100, -200);
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextSetLineWidth(ctx, 5.0);
CGContextMoveToPoint(ctx, startPoint.x, startPoint.y );
CGContextAddLineToPoint(ctx, endPoint.x, endPoint.y);
CGContextStrokePath(ctx);
}
This will give the following output:
I added the red lines to represent the y and x axis, in order to clarify what we've done here.
CGContextClearRect(context, self.bounds);
You can draw out of this bounds,please make a larger rect.

CGContextMoveToPoint moves to wrong position

I'm new to core graphics and I'm struggling with a simple task of putting a sweeping circle inside a square. The outcome I got looks like this:
The circle won't appear at the center of the square, and the size of the circle appears much smaller than I specified.
Below is my drawRect method for drawing the circle. I have put the printed-out variable values while debugging in the comments for your convenience. I also printed out the value passed to initWithFrame: frame=(0 0; 256 256). The frame is the orange square you see in the picture.
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGFloat midX = CGRectGetMidX(self.bounds); // bounds = (0 0; 256 256); midX = 128
CGFloat midY = CGRectGetMidY(self.bounds); // midY = 128
CGFloat radius = midY - 4; // radius = 124
// Outer grey pie
[endColor setFill];
CGContextMoveToPoint(context, midX, midY); // move to center
CGContextAddEllipseInRect(context, CGRectMake(midX - radius, midY - radius, radius * 2, radius * 2)); // adds a circle of radius = square_side_length - 4
CGContextFillPath(context); // fill the circle above with grey
// Show the clock
NSTimeInterval seconds = [[NSDate date] timeIntervalSince1970];
CGFloat mod = fmod(seconds, self.period);
CGFloat percent = mod / self.period;
[fillColor setFill];
CGFloat start = -M_PI_2;
CGFloat end = 2 * M_PI;
CGFloat sweep = end * percent + start;
CGContextMoveToPoint(context, midX, midY);
CGContextAddArc(context, midX, midY, radius, start, sweep, 0); // radius = square_side_length - 24
CGContextFillPath(context);
// Innermost white pie
radius -= 50; // radius = square_side_length - 54
[bgColor setFill]; // white
CGContextMoveToPoint(context, midX, midY);
CGContextAddEllipseInRect(context, CGRectMake(midX - radius, midY - radius, radius * 2, radius * 2));
CGContextFillPath(context);
}
And below is the code that adds the clock to its superview:
clock = [[ProgressClock alloc] initWithFrame:self.clockHolder.bounds // bounds=[0 0; 256 256]
period:[TOTPGenerator defaultPeriod]
bgColor:[UIColor whiteColor]
strokeColor:[UIColor colorWithWhite:0 alpha:0.2]
fillColor:[UIColor blueColor]
endColor:[UIColor grayColor]
shade:NO];
[self.clockHolder addSubview:clock];
Can anyone spot the mistake I made? Thanks in advance.
Thanks a lot to #originaluser2's comment, I have fixed this issue simply by moving the clock presenting logic from viewDidLoad to viewDidAppear and the clock showed up perfectly. There was nothing wrong with the drawing code I posted; however the auto-layout initialization and the animation of my clock happened in a sequence that gave my drawing canvas a wrong frame. By putting the drawing logic in viewDidAppear, we are guaranteed that all the auto-layout setup has been completed, thus frames are fixed, before continue onto drawing the circle.

How to draw a triangle over a UIImage in a UIImageView

I'm having trouble drawing atop an image in a UIImageView, I've already looked at "Draw another image on a UIImage" But it was only so much help. Here's my scenario: I have a UIPopoverController with a UITableView in it and I want to display a triangle pointing up when the user is at the bottom of scrollable table view.
My code:
- (UIImage *) drawTriangleInViewForSize:(CGSize)sizeOfView
imageToDrawOn:(UIImage *)underImage
isAtTop:(BOOL)top{
CGPoint firstPoint;
CGPoint secondPoint;
CGPoint thirdPoint;
if(!top){
//I want to draw a equilateral triangle (side length 10) in the center of the image in
//the imageView, these are the coordinates I am using.
firstPoint = CGPointMake(underImage.size.width * 0.5 + 5, underImage.size.height * 0.5 - 5);
secondPoint = CGPointMake((firstPoint.x - 10), firstPoint.y);
thirdPoint = CGPointMake(underImage.size.width * 0.5,
underImage.size.width * 0.5 + 5);
}
*/
**DISREGARD**
else{
firstPoint = CGPointMake(sizeOfView.width * 0.5 + 5,
self.tableViewChoices.rowHeight * 0.5 - 5);
secondPoint = CGPointMake((firstPoint.x - 10), firstPoint.y);
thirdPoint = CGPointMake(sizeOfView.width * 0.5,
self.tableViewChoices.rowHeight * 0.5 + 5);
}*/
//get the size of the image for the drawInRect: method
CGFloat imageViewWidth = sizeOfView.width;
CGFloat imageViewHeight = self.tableViewChoices.rowHeight;
//set the graphics context to be the size of the image
UIGraphicsBeginImageContextWithOptions(underImage.size, YES, 0.0);
[underImage drawInRect:CGRectMake(0.0, 0.0, imageViewWidth, imageViewHeight)];
//set the line attributes
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextSetFillColorWithColor(ctx, [UIColor clearColor].CGColor);
CGContextSetLineWidth(ctx, 0.05);
UIGraphicsPushContext(ctx);
//draw the triangle
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, firstPoint.x, firstPoint.y);
CGContextAddLineToPoint(ctx, secondPoint.x, secondPoint.y);
CGContextAddLineToPoint(ctx, thirdPoint.x, thirdPoint.y);
CGContextAddLineToPoint(ctx, firstPoint.x, firstPoint.y);
CGContextClosePath(ctx);
UIGraphicsPopContext();
//get the image
UIImage *rslt = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return rslt;
}
I can't quite seem to draw to the triangle to the UIImage in the imageView, the end result is either a completely black ImageView (the UIImage is just a white.png), or one line at the top of the imageview (depending on whether I use drawInRect: or drawAtPoint: and what coordinates I use). All I need to do is draw a triangle pointing up, not a hard thing, and it really looks like I'm doing it "by the book."
Here is my completed code to draw a triangle onto a UIImage in a UIImageView.
//draws triangle up or down on a UIImage.
+(UIImage *) drawTriangleInViewForSize:(CGSize)sizeOfView
imageToDrawOn:(UIImage *)underImage
isAtTop:(BOOL)top
{
CGFloat rowHeight = underImage.size.height; //44; //self.tableViewChoices.rowHeight;
CGPoint firstPoint;
CGPoint secondPoint;
CGPoint thirdPoint;
CGFloat imageViewWidth = sizeOfView.width;
CGFloat imageViewHeight = rowHeight;
if(!top){
//draw a upward facing triangle in the center of the view.
firstPoint = CGPointMake(imageViewWidth * 0.5 + 15, imageViewHeight * 0.5 - 5);
secondPoint = CGPointMake((firstPoint.x - 30), firstPoint.y);
thirdPoint = CGPointMake(imageViewWidth * 0.5,
imageViewHeight * 0.5 + 5);
}else{
//disregard this 'else'
firstPoint = CGPointMake(sizeOfView.width * 0.5 + 15,
rowHeight * 0.5 - 5);
secondPoint = CGPointMake((firstPoint.x - 10), firstPoint.y);
thirdPoint = CGPointMake(sizeOfView.width * 0.5,
rowHeight * 0.5 + 5);
}
//get the image context with options(recommended funct to use)
//get the size of the imageView
UIGraphicsBeginImageContextWithOptions(CGSizeMake(imageViewWidth, imageViewHeight), YES, 0.0);
//use the the image that is going to be drawn on as the receiver
[underImage drawInRect:CGRectMake(0.0, 0.0, imageViewWidth, imageViewHeight)];
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(ctx, 0.5);
//UIGraphicsPushContext(ctx);
//uses path ref
CGMutablePathRef path = CGPathCreateMutable();
//draw the triangle
CGPathMoveToPoint(path, NULL, firstPoint.x, firstPoint.y);
CGPathAddLineToPoint(path, NULL, secondPoint.x, secondPoint.y);
CGPathAddLineToPoint(path, NULL, thirdPoint.x, thirdPoint.y);
CGPathAddLineToPoint(path, NULL, firstPoint.x, firstPoint.y);
//close the path
CGPathCloseSubpath(path);
//add the path to the context
CGContextAddPath(ctx, path);
CGContextSetFillColorWithColor(ctx, [UIColor whiteColor].CGColor);
CGContextSetStrokeColorWithColor(ctx, [UIColor blackColor].CGColor);
CGContextFillPath(ctx);
CGContextAddPath(ctx, path);
CGContextStrokePath(ctx);
CGPathRelease(path);
//UIGraphicsPopContext();
//get the new image with the triangle
UIImage *rslt = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return rslt;
}
Your code looks too complicated for that kind of task. Why don't you use simple UIImageView with any triangle image you ever want? Just show it when the table at the bottom and that's all.
Note, that when you're implementing delegate for UITableView you're also able to catch it's UIScrollView's delegate messages such as scrollView:didEndDecelerating: and so on.
Regarding your code you've missed an actual path draw:
CGContextAddPath(context, trianglePath);
CGContextFillPath(context);
To make it possible you should use path-related functions. Using CGContext-related functions seems to be improper since such approach may remove old path. So the better way is to create your own mutable path in the context, draw your triangle in it and then draw this path into context by functions mentioned above.

Core graphics rotate rectangle

With this formula I got angle
double rotateAngle = atan2(y,x)
with this code I can draw a rectangle
CGRect rect = CGRectMake(x,y , width ,height);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
How can I rotate the rectangle around the angle ?
Here's how you'd do that:
CGContextSaveGState(context);
CGFloat halfWidth = width / 2.0;
CGFloat halfHeight = height / 2.0;
CGPoint center = CGPointMake(x + halfWidth, y + halfHeight);
// Move to the center of the rectangle:
CGContextTranslateCTM(context, center.x, center.y);
// Rotate:
CGContextRotateCTM(context, rotateAngle);
// Draw the rectangle centered about the center:
CGRect rect = CGRectMake(-halfWidth, -halfHeight, width, height);
CGContextAddRect(context, rect);
CGContextStrokePath(context);
CGContextRestoreGState(context);

UIView CGGradient clipped to drawn "frame"

I'm trying to make a more sophisticated drawing of a UILabels (or a UIView, putting the UILabel on top, should that be required) background. Since I want this to resize automatically when the user changes iPhone orientation, I am trying to implement this in a subclasses drawRect function. I would love to be able to tune this all in code, omitting the need for pattern images, if it's possible.
I got some hints from some former posts on the subject:
Gradients on UIView and UILabels On iPhone
and
Make Background of UIView a Gradient Without Sub Classing
Unfortunately they both miss the target, since I wanted to combine gradient with custom drawing. I want the CGGradient to be clipped by the line frame I am drawing (visible at the rounded corners) but I can't accomplish this.
The alternative would be to use the CAGradientLayer, which seem to work quite good, but that would require some resising of the frame at rotation, and the gradient layer seem to draw on top of the text, no matter how I try.
My question therefor is twofold
How do I modify the code below to make the gradient clip to the drawn "frame"
How do I make CAGradientLayer draw behind the text of the UILabel (or do I have to put a UIView behind the UILabel with the CAGradientLayer as background)
Here is my drawrect function:
- (void)drawRect:(CGRect)rect {
// Drawing code
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(c, [[UIColor clearColor] CGColor]);
CGContextSetStrokeColorWithColor(c, [strokeColor CGColor]);
CGContextSetLineWidth(c, 1);
CGFloat minx = CGRectGetMinX(rect), midx = CGRectGetMidX(rect), maxx = CGRectGetMaxX(rect) ;
CGFloat miny = CGRectGetMinY(rect), midy = CGRectGetMidY(rect), maxy = CGRectGetMaxY(rect) ;
minx = minx + 1;
miny = miny + 1;
maxx = maxx - 1;
maxy = maxy - 1;
CGGradientRef glossGradient;
CGColorSpaceRef rgbColorspace;
size_t num_locations = 2;
CGFloat locations[2] = { 0.0, 1.0 };
CGFloat components[8] = { 0.6, 0.6, 0.6, 1.0, // Start color
0.3, 0.3, 0.3, 1.0 }; // End color
rgbColorspace = CGColorSpaceCreateDeviceRGB();
glossGradient = CGGradientCreateWithColorComponents(rgbColorspace, components, locations, num_locations);
CGRect currentBounds = [self bounds];
CGPoint topCenter = CGPointMake(CGRectGetMidX(currentBounds), 0.0f);
CGPoint midCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGPoint lowCenter = CGPointMake(CGRectGetMidX(currentBounds), CGRectGetMidY(currentBounds));
CGContextDrawLinearGradient(c, glossGradient, topCenter, midCenter, 0);
CGGradientRelease(glossGradient);
CGColorSpaceRelease(rgbColorspace);
CGContextMoveToPoint(c, minx, midy);
CGContextAddArcToPoint(c, minx, miny, midx, miny, ROUND_SIZE_INFO);
CGContextAddArcToPoint(c, maxx, miny, maxx, midy, ROUND_SIZE_INFO);
CGContextAddArcToPoint(c, maxx, maxy, midx, maxy, ROUND_SIZE_INFO);
CGContextAddArcToPoint(c, minx, maxy, minx, midy, ROUND_SIZE_INFO);
// Close the path
CGContextClosePath(c);
// Fill & stroke the path
CGContextDrawPath(c, kCGPathFillStroke);
// return;
[super drawRect: rect];
What you are looking for is CGContextClip()
Create your path and gradient as in your code, and then
CGContextClip(c);
CGContextDrawLinearGradient(c, glossGradient, topCenter, midCenter, 0);
CGGradientRelease(glossGradient);
(Remove your old call to CGContextDrawLinearGradient)
Also remember to save and restore your context before and after you do any clipping
If your clipping is inverted from what you want, try CGContextEOClip() (or draw your path in reverse order)

Resources