I have a UIView subclass on which the user can add a random CGPath. The CGPath is added by processing UIPanGestures.
I would like to resize the UIView to the minimal rect possible that contains the CGPath. In my UIView subclass, I have overridden sizeThatFits to return the minimal size as such:
- (CGSize) sizeThatFits:(CGSize)size {
CGRect box = CGPathGetBoundingBox(sigPath);
return box.size;
}
This works as expected and the UIView is resized to the value returned, but the CGPath is also "resized" proportionally resulting in a different path that what the user had originally drawn. As an example, this is the view with a path as drawn by the user:
And this is the view with the path after resizing:
How can I resize my UIView and not "resize" the path?
Use the CGPathGetBoundingBox. From Apple documentation:
Returns the bounding box containing all points in a graphics path. The
bounding box is the smallest rectangle completely enclosing all points
in the path, including control points for Bézier and quadratic curves.
Here a small proof-of-concept drawRect methods. Hope it helps you!
- (void)drawRect:(CGRect)rect {
//Get the CGContext from this view
CGContextRef context = UIGraphicsGetCurrentContext();
//Clear context rect
CGContextClearRect(context, rect);
//Set the stroke (pen) color
CGContextSetStrokeColorWithColor(context, [UIColor blackColor].CGColor);
//Set the width of the pen mark
CGContextSetLineWidth(context, 1.0);
CGPoint startPoint = CGPointMake(50, 50);
CGPoint arrowPoint = CGPointMake(60, 110);
//Start at this point
CGContextMoveToPoint(context, startPoint.x, startPoint.y);
CGContextAddLineToPoint(context, startPoint.x+100, startPoint.y);
CGContextAddLineToPoint(context, startPoint.x+100, startPoint.y+90);
CGContextAddLineToPoint(context, startPoint.x+50, startPoint.y+90);
CGContextAddLineToPoint(context, arrowPoint.x, arrowPoint.y);
CGContextAddLineToPoint(context, startPoint.x+40, startPoint.y+90);
CGContextAddLineToPoint(context, startPoint.x, startPoint.y+90);
CGContextAddLineToPoint(context, startPoint.x, startPoint.y);
//Draw it
//CGContextStrokePath(context);
CGPathRef aPathRef = CGContextCopyPath(context);
// Close the path
CGContextClosePath(context);
CGRect boundingBox = CGPathGetBoundingBox(aPathRef);
NSLog(#"your minimal enclosing rect: %.2f %.2f %.2f %.2f", boundingBox.origin.x, boundingBox.origin.y, boundingBox.size.width, boundingBox.size.height);
}
Related
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.
I am using Quartz 2D in a UIView object to draw several curves, like this example:
This is the code I have right now (I have omitted the calculation of control points and other stuff):
for (int i = 1; i < points.count; ++i) {
CGContextMoveToPoint(context, previousXValue, previousYValue);
CGContextAddCurveToPoint(context,
firstControlPointXValue, firstControlPointYValue,
secondControlPointXValue, secondControlPointYValue,
xValue, yValue
);
// CGContextFillPath(context);
CGContextStrokePath(context);
}
Now I would like to fill the area below the curve, but if I use CGContextFillPath the result is the following:
which makes sense, because according to Quartz 2D documentation:
When you fill the current path, Quartz acts as if each subpath contained in the path were closed. It then uses these closed subpaths
and calculates the pixels to fill.
Then I tried to move the path to the lower right corner and close the path, but the fill method doesn't have any effect:
CGContextMoveToPoint(context, rect.size.width, rect.size.height);
CGContextClosePath(context);
CGContextFillPath(context);
How could I fill the whole area below the curve and not just the subarea closed in every subpath?
EDIT:
I have found a temporary solution: drawing a shape using the curve and two vertical lines on each subpath:
for (int i = 1; i < points.count; ++i) {
// Draw the curve
CGContextMoveToPoint(context, previousXValue, previousYValue);
CGContextAddCurveToPoint(context,
firstControlPointXValue, firstControlPointYValue,
secondControlPointXValue, secondControlPointYValue,
xValue, yValue
);
CGContextStrokePath(context);
// Draw a shape using curve's initial and final points
CGContextMoveToPoint(context, previousXValue, rect.size.height);
CGContextAddLineToPoint(context, previousXValue, previousYValue);
CGContextAddCurveToPoint(context,
firstControlPointXValue, firstControlPointYValue,
secondControlPointXValue, secondControlPointYValue,
xValue, yValue
);
CGContextAddLineToPoint(context, xValue, rect.size.height);
CGContextFillPath(context);
}
I don't know if this is overkill, so improvements are also welcomed. Besides, I'm getting this result:
Note that vertical lines appear as a result of drawing the subareas next to each other. How can that be avoided?
The idea is right, but you should not fill the individual curves, but rather, create a single path and then fill that. So, start with CGContextMoveToPoint to the lower left corner, CGContextAddLineToPoint to your first point, do the CGContextAddCurveToPoint for all of the curves, and at the end, do a CGContextAddLineToPoint to the lower right corner (and you might as well do a CGContextClosePath for good measure).
This is a simplified example, where I just have an array of points, but it illustrates the idea:
- (void)drawRect:(CGRect)rect
{
if (!self.points)
[self configurePoints];
DataPoint *point;
CGContextRef context = UIGraphicsGetCurrentContext();
CGColorRef color = [[UIColor redColor] CGColor];
CGContextSetStrokeColorWithColor(context, color);
CGContextSetFillColorWithColor(context, color);
point = self.points[0];
CGContextMoveToPoint(context, point.x, self.bounds.size.height);
CGContextAddLineToPoint(context, point.x, point.y);
for (point in self.points)
{
// you'd do your CGContextAddCurveToPoint here; I'm just adding lines
CGContextAddLineToPoint(context, point.x, point.y);
}
point = [self.points lastObject];
CGContextAddLineToPoint(context, point.x, self.bounds.size.height);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFillStroke);
}
I'm having some issues while trying to draw lines smoothly on touches moved when my UIView is zoomed in. The thing is that lines start to look very pixelated upon certain zoom level.
My view hierarchy is really simple as of now, since this is more like a proof of concept. I have a UIScrollView, my UIView as a child and also it is set as the zoom view.
My drawRect: implementation looks like this:
CGContextRef context = UIGraphicsGetCurrentContext();
[self.layer renderInContext:context];
CGPoint mid1 = midPoint(_previousPoint1, _previousPoint2);
CGPoint mid2 = midPoint(_currentPoint, _previousPoint1);
CGContextSetBlendMode(context, kCGBlendModeNormal);
CGContextMoveToPoint(context, mid1.x, mid1.y);
CGContextAddQuadCurveToPoint(context, _previousPoint1.x, _previousPoint1.y, mid2.x, mid2.y);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineJoin(context, kCGLineJoinRound);
CGContextSetLineWidth(context, self.lineWidth);
CGContextSetStrokeColorWithColor(context, self.lineColor.CGColor);
CGContextStrokePath(context);
Dumping the contents of the layer into the context is an important part since I'm only refreshing the bounding box enclosing the path formed by the last three touched points (_previousPoint1, _previousPoint2 and _currentPoint).
That operation is actually done on touchesMoved method and the function that handles it is the following:
- (void)calculateMinImageArea:(CGPoint)pp1 :(CGPoint)pp2 :(CGPoint)cp
{
// calculate mid point
CGPoint mid1 = midPoint(pp1, pp2);
CGPoint mid2 = midPoint(cp, pp1);
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, mid1.x, mid1.y);
CGPathAddQuadCurveToPoint(path, NULL, pp1.x, pp1.y, mid2.x, mid2.y);
CGRect bounds = CGPathGetBoundingBox(path);
CGPathRelease(path);
CGRect drawBox = [self extendedRect:bounds];
UIGraphicsBeginImageContext(drawBox.size);
[self.layer renderInContext:UIGraphicsGetCurrentContext()];
UIGraphicsEndImageContext();
[self setNeedsDisplayInRect:drawBox];
}
I was able to find a workaround using CAShapeLayers to draw the paths. However, the low performance wasn't acceptable.
I would really appreciate if someone can point me to the right approach.
Thanks.
[self setContentScaleFactor:scale];
use this in scrollViewDidEndZooming: delegate method
Hope this help's you
I'm working on the Stanford CS193p course and am trying to draw a graph. Drawing the axes works, but I can't draw the graph itself. I'm getting the message
CGContextAddLineToPoint: no current point.
when I try to draw. Here's the code.
- (void)drawRect:(CGRect)rect
{
NSLog(#"Drawing Graph View");
CGPoint origin = CGPointMake(20, 350);
CGRect rect2 = CGRectMake(origin.x, origin.y, 250, -250);
[AxesDrawer drawAxesInRect:rect2 originAtPoint:origin scale:[self scale]];
CGContextRef context = UIGraphicsGetCurrentContext();
UIGraphicsPushContext(context);
CGContextSetLineWidth(context, 2);
[[UIColor redColor] setStroke];
CGContextMoveToPoint(context, origin.x, origin.y);
for (CGFloat i=0; i<250; i++) {
CGFloat yChange = [self.dataSource deltaY:self];
CGContextAddLineToPoint(context, origin.x+i, origin.y-yChange);
CGContextStrokePath(context);
}
UIGraphicsPopContext();
}
You have to place CGContextStrokePath(context); outside the for loop. Otherwise it will create a fresh path on every run through the loop and that fails.
Do you not have a problem with:
CGRectMake(origin.x, origin.y, 250, -250)
You specify a negative height!
I am using mapkit on iPhone with iOS 4.
I am using a custom overlay and a custom overlay view, to draw shapes on the map.
At the moment, shapes are just rectangles, but I am planning something more sophisticated. This is why I am not using the MKPolygon overlay type.
This is the code for my overlay view drawing method:
-(void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
// Clip context to bounding rectangle
MKMapRect boundingMapRect = [[self overlay] boundingMapRect];
CGRect boundingRect = [self rectForMapRect:boundingMapRect];
CGContextAddRect(context, boundingRect);
CGContextClip(context);
// Define shape
CGRect shapeRect = CGRectMake(0.5f, 0.5f, boundingRect.size.width - 1.0f, boundingRect.size.height - 1.0f);
// Fill
CGContextSetRGBFillColor(context, 0.5f, 0.5f, 0.5f, 0.5f);
CGContextFillRect(context, shapeRect);
// Stroke
CGContextSetRGBStrokeColor(context, 0, 0, 0, 0.75f);
CGContextSetLineWidth(context, 1.0f);
CGContextStrokeRect(context, shapeRect);
}
The problem is that rectangles get correctly filled (so it appears their bounding rect is correctly set), but they don't get stroked.
Can anyone help?
Thanks!
As reported in some previous comments, the problem is with line width. More generally, all drawing is automatically scaled to follow the map zooming, so if you want some of your drawing metrics to be zoom-independent, you have to divide it by zoomScale.
Here is the new code, that works correctly on my iPhone 4:
-(void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
// Clip context to bounding rectangle
MKMapRect boundingMapRect = [[self overlay] boundingMapRect];
CGRect boundingRect = [self rectForMapRect:boundingMapRect];
CGContextAddRect(context, boundingRect);
CGContextClip(context);
// Define shape
CGRect shapeRect = CGRectInset(boundingRect, 2.0f / zoomScale, 2.0f / zoomScale);
// Fill
CGContextSetRGBFillColor(context, 0.5f, 0.5f, 0.5f, 0.5f);
CGContextFillRect(context, shapeRect);
// Stroke
CGContextSetRGBStrokeColor(context, 0, 0, 0, 0.75f);
CGContextSetLineWidth(context, 4.0f / zoomScale);
CGContextStrokeRect(context, shapeRect);
}
I will also report the code I am using in the overlay to calculate and return the bounding rectangle, because I think it can help:
-(MKMapRect)boundingMapRect
{
// Overlay bounds
CLLocationCoordinate2D topLeftcoordinate = <the top-left coordinate of overlay>;
CLLocationCoordinate2D bottomRightCoordinate = <the bottom-right coordinate of overlay>;
// Convert them to map points
MKMapPoint topLeftPoint = MKMapPointForCoordinate(topLeftcoordinate);
MKMapPoint bottomRightPoint = MKMapPointForCoordinate(bottomRightCoordinate);
// Calculate map rect
return MKMapRectMake(topLeftPoint.x, topLeftPoint.y, bottomRightPoint.x - topLeftPoint.x, topLeftPoint.y - bottomRightPoint.y);
}
Thank you all for your comments and suggestions.
Currently, we should use MKOverlayRenderer instead of MKOverlayView.
In method -(void)drawMapRect:(MKMapRect)mapRect
zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context,
use scaleFactor for configuring the stroke width of lines or other
attributes that might be affected by the scale of the map’s
contents.
Refer to Apple official site drawMapRect:zoomScale:inContext: of MKOverlayRenderer.