iOS - Quartz drawing issue with parent/child views - ios

Scenario
I have two views. One is the "parent" view which contains a "child" view that does the drawing. I refer to the child as QuartzView in the code to follow. QuartzView knows how to draw a square to it's own context.
Issue
When I tell the QuartzView on it's self to draw a square it does so as expected. When I use the parent view to tell QuartsView to draw a square on it's self it draws the square in the lower left corner of the screen at about 1/5 the expected size.
Question
I assume there's some parent/child or context issues here but I'm not sure what they are. How can I get both squares to draw in the exact same place at the exact same size?
Parent ViewController
- (void)drawASquare {
// this code draws the "goofy" square that is smaller and off in the bottom left corner
x = qv.frame.size.width / 2;
y = qv.frame.size.height / 2;
CGPoint center = CGPointMake(x, y);
[qv drawRectWithCenter:center andWidth:50 andHeight:50 andFillColor:[UIColor blueColor]];
}
Child QuartzView
- (void)drawRect:(CGRect)rect
{
self.context = UIGraphicsGetCurrentContext();
UIColor *color = [UIColor colorWithRed:0 green:1 blue:0 alpha:0.5];
// this code draws a square as expected
float w = self.frame.size.width / 2;
float h = self.frame.size.height / 2;
color = [UIColor blueColor];
CGPoint center = CGPointMake(w, h);
[self drawRectWithCenter:center andWidth:20 andHeight:20 andFillColor:color];
}
- (void)drawRectWithCenter:(CGPoint)center andWidth:(float)w andHeight:(float)h andFillColor:(UIColor *)color
{
CGContextSetFillColorWithColor(self.context, color.CGColor);
CGContextSetRGBStrokeColor(self.context, 0.0, 1.0, 0.0, 1);
CGRect rectangle = CGRectMake(center.x - w / 2, center.x - w / 2, w, h);
CGContextFillRect(self.context, rectangle);
CGContextStrokeRect(self.context, rectangle);
}
Note
The opacities are the same for both squares
I turned off "Autoresize subviews" with no noticeable difference
view.contentScaleFactor = [[UIScreen mainScreen] scale]; has not helped
Edit
I'm noticing that the x/y values of the square when drawn the parent starting from the bottom left as 0,0 whereas normally 0,0 would be the top left.

The return value from UIGraphicsGetCurrentContext() is only valid inside the drawRect method. You can not and must not use that context in any other method. So the self.context property should just be a local variable.
In the drawRectWithCenter method, you should store all of the parameters in properties, and then request a view update with [self setNeedsDisplay]. That way, the framework will call drawRect with the new information. The drawRectWithCenter method should look something like this
- (void)drawRectWithCenter:(CGPoint)center andWidth:(float)w andHeight:(float)h andFillColor:(UIColor *)color
{
self.showCenter = center;
self.showWidth = w;
self.showHeight = h;
self.showFillColor = color;
[self setNeedsDisplay];
}
And of course, the drawRect function needs to take that information, and do the appropriate drawing.

I assume there's some parent/child or context issues here but I'm not sure what they are. How can I get both squares to draw in the exact same place at the exact same size?
You normally don't need to worry about the graphics context in your -drawRect: method because Cocoa Touch will set up the context for you before calling -drawRect:. But your -drawASquare method in the view controller calls -drawRectWithCenter:... to draw outside the normal drawing process, so the context isn't set up for your view. You should really have the view do its drawing in -drawRect:. If your view controller wants to make the view redraw, it should call -setNeedsDisplay, like:
[qv setNeedsDisplay];
That will add the view to the drawing list, and the graphics system will set up the graphics context and call the view's -drawRect: for you.
I'm noticing that the x/y values of the square when drawn the parent starting from the bottom left as 0,0 whereas normally 0,0 would be the top left.
UIKit and Core Animation use an upper left origin, but Core Graphics (a.k.a. Quartz) normally uses a lower left origin. The docs say:
The default coordinate system used by Core Graphics framework is LLO-based.

Related

Efficiently draw a large scrollable area

I'm working on a simple game which uses a hexagonal grid layout. The grid is very large (a few thousand pixels in width and height). I need to be able to scroll and zoom it within a scrollView, and there are a lot of individual hexagons. I have written the drawing code in CoreGraphics. The hexagons are drawn in the drawRect: method of their view. This drawing code is called for each of the hexagons:
- (void)drawInContext:(CGContextRef)context colour:(UIColor *)colour size:(CGSize)size {
CGFloat width = size.width;
CGFloat height = size.height;
CGFloat x = self.offset.x;
CGFloat y = self.offset.y;
CGContextMoveToPoint(context, (width/2)+x, y);
CGContextAddLineToPoint(context, width+x, (height / 4)+y);
CGContextAddLineToPoint(context, width+x, (height * 3 / 4)+y);
CGContextAddLineToPoint(context, (width / 2)+x, height+y);
CGContextAddLineToPoint(context, x, (height * 3 / 4)+y);
CGContextAddLineToPoint(context, x, (height / 4)+y);
CGContextClosePath(context);
CGContextSetFillColorWithColor(context, colour.CGColor);
CGContextSetStrokeColorWithColor(context, [[UIColor whiteColor] CGColor]);
CGContextDrawPath(context, kCGPathFillStroke);
NSString *text = [NSString stringWithFormat:#"I:%ld\nR:%ld\nC:%ld", self.creationIndex, self.row, self.column];
[text drawAtPoint:CGPointMake(self.offset.x+20, self.offset.y+20) withAttributes:#{NSForegroundColorAttributeName: [UIColor blackColor], NSFontAttributeName: [UIFont systemFontOfSize:[UIFont systemFontSize]]}];
}
I call setNeedsDisplay on the view when a change is needed (like a hexagon changing colour). The problem is that this seems very inefficient. It takes approximately half a second for the map to redraw, which makes everything feel sluggish.
I have tried the following:
Calculate the visible rect of the scrollView and only draw that part of it. This causes problems when zooming to a different rect, as only the destination rect is drawn, causing black space to be displayed in the part being scrolled across.
Set a flag on the hexagons to indicate that they require an update, and only drawing the hexagons which have changed. This resulted in only the changed hexagons being visible, since drawRect: seems to fill the view in black before carrying out the drawing operation, rather than leaving the previous image there and drawing the changed hexagons over the top.
Using UIKit to build the grid of hexagons. This was simply too slow, as there were hundreds of individual views.
To summarise, my question is if there is a way of optimising CoreGraphics drawing, or if there is an alternative way of drawing which is more efficient.
There should not be any need to calc the visible rect, this is done by UIScrollView.
See Scrollview Programming Guide
Furthermore from the class documentation : The object that manages the drawing of content displayed in a scroll view should tile the content’s subviews so that no view exceeds the size of the screen. As users scroll in the scroll view, this object should add and remove subviews as necessary.

How to LIGHTLY erase drawn path in cgcontext?

I managed to implement erase drawings on CGContext
UIImageView *maskImgView = [self.view viewWithTag:K_MASKIMG];
UIGraphicsBeginImageContext(maskImgView.image.size);
[maskImgView.image drawAtPoint:CGPointMake(0,0)];
float alp = 0.5;
UIImage *oriBrush = [UIImage imageNamed:_brushName];
//sets the style for the endpoints of lines drawn in a graphics context
CGContextRef ctx = UIGraphicsGetCurrentContext();
CGFloat eraseSize = oriBrush.size.width*_brushSize/_zoomCurrentFactor;
CGContextSetLineCap(ctx, kCGLineCapRound);
CGContextSetLineJoin(ctx, kCGLineJoinRound);
CGContextSetLineWidth(ctx,eraseSize);
CGContextSetRGBStrokeColor(ctx, 1, 1, 1, alp);
CGContextSetBlendMode(ctx, kCGBlendModeClear);
CGContextBeginPath(ctx);
CGContextMoveToPoint(ctx, lastPoint.x,lastPoint.y);
CGPoint vector = CGPointMake(currentPoint.x - lastPoint.x, currentPoint.y - lastPoint.y);
CGFloat distance = hypotf(vector.x, vector.y);
vector.x /= distance;
vector.y /= distance;
for (CGFloat i = 0; i < distance; i += 1.0f) {
CGPoint p = CGPointMake(lastPoint.x + i * vector.x, lastPoint.y + i * vector.y);
CGContextAddLineToPoint(ctx, p.x, p.y);
}
CGContextStrokePath(ctx);
maskImgView.image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
lastPoint = currentPoint;
Problem is, this TOTALLY erase anything. The alpha set in this function ( CGContextSetRGBStrokeColor(ctx, 1, 1, 1, alp);) seems to be ignored totally.
I want to erase just lightly and repeated erasing will then totally removes the drawing.
Any ideas?
EDIT: As per request, I add more details about this code:
alp=_brushAlpha is a property delcared in this ViewController class. It ranges from 0.1 to 1.0. At testing I set it to 0.5. This drawing code is triggered by pan gesture recognizer (change state). It is basically following the finger (draw/erase by finger).
You've set the blending mode to clear. That ignores stroke color. You should play with the various modes a bit, but I suspect you want something like sourceAtop or maybe screen. See the CGBlendMode docs for full details.
You have a flag named clearsContextBeforeDrawing in UIView. if you set it to YES it will clear it before every draw.
according to documentation
A Boolean value that determines whether the view’s bounds should be automatically cleared before drawing.
When set to YES, the drawing buffer is automatically cleared to transparent black before the drawRect: method is called. This behavior ensures that there are no visual artifacts left over when the view’s contents are redrawn. If the view’s opaque property is also set to YES, the backgroundColor property of the view must not be nil or drawing errors may occur. The default value of this property is YES.
If you set the value of this property to NO, you are responsible for ensuring the contents of the view are drawn properly in your drawRect: method. If your drawing code is already heavily optimized, setting this property is NO can improve performance, especially during scrolling when only a portion of the view might need to be redrawn.

iOS drawing in CALayers drawInContext is either pixelated or blurry on retina

I have a custom UIView that draws something in an overwritten
- (void)drawRect:(CGRect)rect
this works fine and gives sharp results on retina screens.
However, now I would like to make the properties on which the drawing is based animatable. Creating animatable properties seems to be possible only on the CALayer, so instead of doing the drawing in UIView, I create a custom CALayer subclass and do the drawing inside
- (void) drawInContext:(CGContextRef)ctx
I use pretty much the exact same drawing code in this function as I used in the drawRect function of the custom UIView.
The result looks the same - however, it's not retina resolution but pixelated (large square pixels)
if I put
self.contentsScale = [UIScreen mainScreen].scale;
To the beginning of my drawInContext implementation, then instead of a pixelated result, I get a blurry result (as if the rendering is still performed in non-retina resolution and then upscaled to retina resolution).
what's the correct way to render sharp retina paths in CALayers drawInContext ?
here are some screenshots (the blue line is part of the custom drawing in question. the yellow part is just an image)
Drawn inside custom UIView's drawRect:
Drawn inside custom CALayer's drawInContext:
Drawin inside custom CALayer's drawInContext, with setting self.contentScale first:
For completeness, here's (a stripped down version of the) drawing code:
//if drawing inside custom UIView sublcass:
- (void)drawRect:(CGRect)rect
{
CGContextRef currenctContext = UIGraphicsGetCurrentContext();
[[UIColor blackColor] set];
CGContextSetLineWidth(currenctContext, _lineWidth);
CGContextSetLineJoin(currenctContext,kCGLineJoinRound);
CGContextMoveToPoint(currenctContext,x1, y1);
CGContextAddLineToPoint(currenctContext,x2, y2);
CGContextStrokePath(currenctContext);
}
//if drawing inside custom CALayer subclass:
- (void) drawInContext:(CGContextRef)ctx {
{
//self.contentsScale = [UIScreen mainScreen].scale;
CGContextRef currenctContext = ctx;
CGContextSetStrokeColorWithColor(currenctContext, [UIColor blackColor].CGColor);
CGContextSetLineWidth(currenctContext, _lineWidth);
CGContextSetLineJoin(currenctContext,kCGLineJoinRound);
CGContextMoveToPoint(currenctContext,x1, y1);
CGContextAddLineToPoint(currenctContext,x2, y2);
CGContextStrokePath(currenctContext);
}
To restate what I want to achieve: I want to achieve the same crisp retina rendering as in the UIView approach, but when rendering in CALayer
The issue is most likely to be contentScale here; be aware that if you're assigning this to a custom view by overriding its layerClass function, the layer's content scale may be reset. There may be some other instances in which this also happens. To be safe, set the content scale only after the layer has been added to a view.
Try assigning the main screen's scale to your custom layer during your custom view's init method. In Swift 3 that looks like this:
layer.contentsScale = UIScreen.mainScreen().scale
Or, in Swift 4:
layer.contentsScale = UIScreen.main.scale
Are you using shouldRasterize = YES in the layer? Try drawing in the CALayer subclass but set the rasterizationScale to the screen's scale.
After adding layer to its superlayer. set shouldRasterize to YES , set contentsScale and resterizatioinScale to screen scale:
[self.progressView.layer addSublayer:self.progressLayer];
self.progressLayer.shouldRasterize = YES;
self.progressLayer.contentsScale = kScreenScale;
self.progressLayer.rasterizationScale = kScreenScale;
CABasicAnimation *animate = [CABasicAnimation animationWithKeyPath:#"progress"];// progress is a customized property of progressLayer
animate.duration = 1.5;
animate.beginTime = 0;
animate.fromValue = #0;
animate.toValue = #1;
animate.fillMode = kCAFillModeForwards;
animate.removedOnCompletion = NO;
animate.repeatCount = HUGE_VALF;
[self.progressLayer addAnimation:animate forKey:#"progressAnimation"];

Drawing outside a UIView

I have a UIView where I would like to draw a Circle that extends past the frame of the UIView,
I have set the masksToBounds to NO - expecting that I can draw past outside the bounds of the UIView by 5 pixels on the right and bottom.
I expect the oval to not get clipped but it does get clipped and does not draw outside the bounds?
- (void)drawRect:(CGRect)rect
{
int width = self.bounds.size.width;
int height = self.bounds.size.height;
self.layer.masksToBounds = NO;
//// Rounded Rectangle Drawing
//// Oval Drawing
UIBezierPath* ovalPath = [UIBezierPath bezierPathWithOvalInRect: CGRectMake(0, 0, width+5, height+5)];
[[UIColor magentaColor] setFill];
[ovalPath fill];
[[UIColor blackColor] setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
From http://developer.apple.com/library/ios/#documentation/general/conceptual/Devpedia-CocoaApp/DrawingModel.html
UIView and NSView automatically configure the drawing environment of a
view before its drawRect: method is invoked. (In the AppKit framework,
configuring the drawing environment is called locking focus.) As part
of this configuration, the view class creates a graphics context for
the current drawing environment.
This graphics context is a Quartz object (CGContext) that contains
information the drawing system requires, such as the colors to apply,
the drawing mode (stroke or fill), line width and style information,
font information, and compositing options. (In the AppKit, an object
of the NSGraphicsContext class wraps a CGContext object.) A graphics
context object is associated with a window, bitmap, PDF file, or other
output device and maintains information about the current state of the
drawing environment for that entity. A view draws using a graphics
context associated with the view’s window. For a view, the graphics
context sets the default clipping region to coincide with the view’s
bounds and puts the default drawing origin at the origin of a view’s
boundaries.
Once the clipping region is set, you can only make it smaller. So, what you're trying to do isn't possible in a UIView drawRect:.
I'm not certain this will fix your problem, but it's something to look into. You're setting self.layer.masksToBounds = NO every single time you enter drawRect. You should try setting it inside the init method just once instead, A) because it's unnecessary to do it multiple times and B) because maybe there's a problem with setting it after drawRect has already been called--who knows.

Is there a way to add text using Paths Drawing

I have a map custom view that inherit from MKOverlayPathView. I need this custom view to display circle, line and text.
I already managed to draw circle and line using path drawing CGPathAddArc and CGPathAddLineToPoint functions.
However, I still need to add text.
I tried to add text using
[text drawAtPoint:centerPoint withFont:font];
but I got invalid context error.
any idea?
With MKOverlayPathView, I think the easiest way to add text is to override drawMapRect:zoomScale:inContext: and put the path and text drawing there (and do nothing in or don't implement createPath).
But if you're going to use drawMapRect anyway, you might want to just switch to subclassing a plain MKOverlayView instead of MKOverlayPathView.
With an MKOverlayView, override the drawMapRect:zoomScale:inContext: method and draw the circle using CGContextAddArc (or CGContextAddEllipseInRect or CGPathAddArc).
You can draw the text using drawAtPoint in this method which will have the required context.
For example:
-(void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CGContextRef)context
{
//calculate CG values from circle coordinate and radius...
CLLocationCoordinate2D center = circle_overlay_center_coordinate_here;
CGPoint centerPoint =
[self pointForMapPoint:MKMapPointForCoordinate(center)];
CGFloat radius = MKMapPointsPerMeterAtLatitude(center.latitude) *
circle_overlay_radius_here;
CGFloat roadWidth = MKRoadWidthAtZoomScale(zoomScale);
//draw the circle...
CGContextSetStrokeColorWithColor(context, [UIColor blueColor].CGColor);
CGContextSetFillColorWithColor(context, [[UIColor blueColor] colorWithAlphaComponent:0.2].CGColor);
CGContextSetLineWidth(context, roadWidth);
CGContextAddArc(context, centerPoint.x, centerPoint.y, radius, 0, 2 * M_PI, true);
CGContextDrawPath(context, kCGPathFillStroke);
//draw the text...
NSString *text = #"Hello";
UIGraphicsPushContext(context);
[[UIColor redColor] set];
[text drawAtPoint:centerPoint
withFont:[UIFont systemFontOfSize:(5.0 * roadWidth)]];
UIGraphicsPopContext();
}
In relation to a comment in another answer...
When the center coordinate or radius (or whatever) of the associated MKOverlay changes, you can make the MKOverlayView "move" by calling setNeedsDisplayInMapRect: on it (instead of removing and adding the overlay again). (When using a MKOverlayPathView, you can call invalidatePath instead.)
When calling setNeedsDisplayInMapRect:, you can pass the boundingMapRect of the overlay for the map rect parameter.
In the LocationReminders sample app from WWDC 2010, the overlay view uses KVO to observe changes to the associated MKOverlay and makes itself move whenever it detects a change to the circle's properties but you could monitor the changes in other ways and call setNeedsDisplayInMapRect: explicitly from outside the overlay view.
(In a comment on another answer I did mention using MKOverlayPathView and that is how the LocationReminders app implements a moving circle overlay view. But I should have mentioned how you can also use MKOverlayView to draw a circle. Sorry about that.)
Pushing the context with UIGraphicsPushContext generated a problem for me. Remind that the method drawMapRect:zoomScale:inContext: is called from different threads in the same time so I had to synchronize the piece of code starting where the UIGraphicsPushContext is called down to UIGraphicsPopContext call.
Also when calculating the font size like in [UIFont systemFontOfSize:(5.0 * roadWidth)] one should take into consideration the [UIScreen mainScreen].scale, which for iPad, iPad2, iPhone3 is 1 and for iPhone4 - 5 and iPad3 is 2. Otherwise the text size will be different from iPad2 to iPad3.
So for me it ended like this: [UIFont boldSystemFontOfSize:(6.0f * [UIScreen mainScreen].scale * roadWidth)]

Resources