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.
Related
I am drawing a circle in my custom UIView's drawRect:
- (void)drawRect:(CGRect)rect {
...
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect:rect];
[UIColor.whiteColor setStroke];
ovalPath.lineWidth = 1;
[ovalPath stroke];
}
The oval is always clipped on the edge. How can I avoid the clipping? Is insetting the rect the only way?
CG draws the stroke centered on the path -- half inside the path, half outside the path. Therefore, part of the stroke is outside of your view, and you don't see it.
Inset the rect by half the stroke width.
CGRect rectToStroke = CGRectInset(rect, 0.5, 0.5);
UIBezierPath *ovalPath = [UIBezierPath bezierPathWithOvalInRect: rectToStroke];
Anything you draw inside of -drawRect: using CG or UIKit goes into a bitmap context which is the size of your view's bounds.
If you need to show something bigger than bounds.size, you have two options: make the view's bounds bigger, or draw via some other method, such as:
add a subview which is bigger (but it will appear on top of your view, so you'll need to make it partially transparent)
add a CALayer or CAShapeLayer to your view's layer (with the same caveat)
set your view's layer's borderWidth to draw a border on top of the contents
(For all of these, you may find that you also need to set your view's clipsToBounds property to NO, if it isn't already.)
I am drawing image on a custom UIView. On resizing the view, the drawing performance goes down and it starts lagging.
My image drawing code is below:
- (void)drawRect:(CGRect)rect
{
CGContextRef context = UIGraphicsGetCurrentContext();
UIBezierPath *bpath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, width, height)];
CGContextAddPath(context, bpath.CGPath);
CGContextClip(context);
CGContextDrawImage(context, [self bounds], image.CGImage);
}
Is this approach correct?
You would be better using Instruments to find where the bottleneck is than asking on here.
However, what you will probably find is that every time the frame changes slightly the entire view will be redrawn.
If you're just using the drawRect to clip the view into an oval (I guess there's an image behind it or something) then you would be better off using a CAShapeLayer.
Create a CAShapeLayer and give it a CGPath then add it as a clipping layer to the view.layer.
Then you can change the path on the CAShapeLayer and it will update. You'll find (I think) that it performs much better too.
If your height and width are the same, you could just use a UIImageView instead of needing a custom view, and get the circular clipping by setting properties on the image view's layer. That approach draws nice and quickly.
Just set up a UIImageView (called "image" in my example) and then have your view controller do this once:
image.layer.cornerRadius = image.size.width / 2.0;
image.layer.masksToBounds = YES;
I have one component that has an UIView subclass and a custom CAlayer into it.
In the UIView there is a circle that is drawn with CoreGraphics, and this is the code:
CGRect b = self.bounds;
int strokeSize = 2;
CGRect arcBounds = CGRectMake(b.origin.x+1, b.origin.y+1, b.size.width-2, b.size.height-2);
CGContextSaveGState(ctx); {
CGContextSetLineWidth(ctx, strokeSize);
CGContextSetStrokeColorWithColor(ctx, [UIColor lightGrayColor].CGColor);
CGContextStrokeEllipseInRect(ctx, arcBounds);
} CGContextRestoreGState(ctx);
when I draw that circle in the drawRect method inside the UIView it works perfect and the circle is drawn smooth and looks great.
The problem appears when I draw another circle just over this one, but the second one is drawn in the CALayer, actually in the drawInContext method of my custom CALayer. Using just the same code the circle doesn't looks good, and have some "pixellation" on the borders.
Any clues on what can be happening? Thanks in advance.
This is due to the contentsScale property. When you have a custom CALayer the default value of this property is 1.0.
The default value of this property is 1.0. For layers attached to a
view, the view changes the scale factor automatically to a value that
is appropriate for the current screen. For layers you create and
manage yourself, you must set the value of this property yourself
based on the resolution of the screen and the content you are
providing. Core Animation uses the value you specify as a cue to
determine how to render your content. Source.
If you have a retina device and you draw with the contentsScale set to 1.0, it will result in that pixelated look you described. In order to fix this you should set the layer's contentsScale to the one of the screen.
[self.layer setContentsScale:[[UIScreen mainScreen] scale]];
This issue does not happen when you draw the circle in the drawRect method of your UIView since there the default contentsScaleFactor is already the one of the screen.
For views that implement a custom drawRect: method and are associated
with a window, the default value for this property is the scale factor
associated with the screen currently displaying the view. Source.
The question
How do you create a separate context for your layer and incorporate that context into super layer?
Why I want to do this
I want abstract the drawing of a view layers into separate objects/files. I want to construct a view out of layers, then position then on top of one another and have other possibilities as that.
The problem is that I'm not aware of you you're supposed to draw a part of your view into a layer without drawing straight into the context of the main views sublayer.
Here's an example, I have subclassed CALayer with HFFoundation:
#implementation HFFoundation
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
UIBezierPath *foundation = [UIBezierPath bezierPath];
// some drawing goes on here in the form of messages to [foundation]
}
#end
Now when I instantiate my custom layer inside a views (void)drawRect:(CGRect)rect I this way:
HFFoundation *foundation = [HFFoundation new];
foundation.frame = CGRectMake(40, viewHeight - 100, 300, 100);
foundation.backgroundColor = [[UIColor colorWithRed:1 green:.4 blue:1 alpha:0.2] CGColor];
[self.window.rootViewController.view.layer addSublayer:foundation];
I get this result (no drawings appear, just the bg color inside the layer frame):
If I [foundation drawLayer:foundation inContext:context]; afterwards, the drawing appears, but it appears inside the top layers context, not in foundation layer. Since foundation layer is also lower in the hierarchy, it hides the drawing (unless I reduce it's alpha, which I've dont in the picture):
How do you draw into the layer itself, I.e. foundation in this case?
To insert a sublayer and set its index you want this piece of code:
[view.layer insertSublayer:foundation atIndex:0];
I'm trying to programmatically recreate the indented button look that can be seen on a UINavigationBarButton. Not the shiny two tone look or the gradient, just the perimeter shading:
It looks like an internal dark shadowing around the entire view perimeter, slightly darker at the top? And then an external highlighting shadow around the lower view perimeter.
I've played a bit with Core Graphics, and experimented with QuartzCore and shadowing with view.layer.shadowRadius and .shadowOffset, but can't even get the lower highlighting to look right. I'm also not sure where to start to achieve both a dark shadowing with internal offset and a light shadowing with external offset.
It seems as though you want a border that looks looks like a shadow. Since the shadow appears to some sort of gradient, setting a border as a gradient won't be possible at first glance. However, it is possible to create a path that represents the border and then fill that with a gradient. Apple provides what seems to be a little known function called CGPathCreateCopyByStrokingPath. This takes a path (say, a rounded rect, for example) and creates a new path that would be the stroke of the old path given the settings you pass into the function (like line width, join/cap setting, miter limit, etc). So lets say you define a path (this isn't exactly what Apple provides, but's it's similar):
+ (UIBezierPath *) bezierPathForBackButtonInRect:(CGRect)rect withRoundingRadius:(CGFloat)radius{
UIBezierPath *path = [UIBezierPath bezierPath];
CGPoint mPoint = CGPointMake(CGRectGetMaxX(rect) - radius, rect.origin.y);
CGPoint ctrlPoint = mPoint;
[path moveToPoint:mPoint];
ctrlPoint.y += radius;
mPoint.x += radius;
mPoint.y += radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:M_PI + M_PI_2 endAngle:0 clockwise:YES];
mPoint.y = CGRectGetMaxY(rect) - radius;
[path addLineToPoint:mPoint];
ctrlPoint = mPoint;
mPoint.y += radius;
mPoint.x -= radius;
ctrlPoint.x -= radius;
if (radius > 0) [path addArcWithCenter:ctrlPoint radius:radius startAngle:0 endAngle:M_PI_2 clockwise:YES];
mPoint.x = rect.origin.x + (10.0f);
[path addLineToPoint:mPoint];
[path addLineToPoint:CGPointMake(rect.origin.x, CGRectGetMidY(rect))];
mPoint.y = rect.origin.y;
[path addLineToPoint:mPoint];
[path closePath];
return path;
}
This returns a path similar to Apple's back button (I use this in my app). I have added this method (along with dozens more) as a category to UIBezierPath.
Now lets add that inner shadow in a drawing routine:
- (void) drawRect:(CGRect)rect{
UIBezierPath *path = [UIBezierPath bezierPathForBackButtonInRect:rect withRoundingRadius:5.0f];
//Just fill with blue color, do what you want here for the button
[[UIColor blueColor] setFill];
[path fill];
[path addClip]; //Not completely necessary, but borders are actually drawn 'around' the path edge, so that half is inside your path, half is outside adding this will ensure the shadow only fills inside the path
//This strokes the standard path, however you might want to might want to inset the rect, create a new 'back button path' off the inset rect and create the inner shadow path off that.
//The line width of 2.0f will actually show up as 1.0f with the above clip: [path addClip];, due to the fact that borders are drawn around the edge
UIBezierPath *innerShadow = [UIBezierPath bezierPathWithCGPath: CGPathCreateCopyByStrokingPath(path.CGPath, NULL, 2.0f, path.lineCapStyle, path.lineJoinStyle, path.miterLimit)];
//You need this, otherwise the center (inside your path) will also be filled with the gradient, which you don't want
innerShadow.usesEvenOddFillRule = YES;
[innerShadow addClip];
//Now lets fill it with a vertical gradient
CGContextRef context = UIGraphicsGetCurrentContext();
CGPoint start = CGPointMake(0, 0);
CGPoint end = CGPointMake(0, CGRectGetMaxY(rect));
CGFloat locations[2] = { 0.0f, 1.0f};
NSArray *colors = [NSArray arrayWithObjects:(id)[UIColor colorWithWhite:.7f alpha:.5f].CGColor, (id)[UIColor colorWithWhite:.3f alpha:.5f].CGColor, nil];
CGGradientRef gradRef = CGGradientCreateWithColors(CGColorSpaceCreateDeviceRGB(), (__bridge CFArrayRef)colors, locations);
CGContextDrawLinearGradient(context, gradRef, start, end, 0);
CGGradientRelease(gradRef);
}
Now this is just a simple example. I don't save/restore contexts or anything, which you'll probably want to do. There are things you might still want to do to make it better, like maybe inset the 'shadow' path if you want to use a normal border. You might want to use more/different colors and locations. But this should get you started.
UPDATE
There is another method you can use to create this effect. I wrote an algorithm to bevel arbitrary bezier paths in core graphics. This can be used to create the effect you're looking for. This is an example of how I use it in my app:
You pass to the routine the CGContextRef, CGPathRef, size of the bevel and what colors you want it to use for the highlight/shadow.
The code I used for this can be found here:Github - Beveling Algorithm.
I also explain the code and my methodology here: Beveling-Shapes in Core Graphics
Using the layer's shadow won't do it. You need both a light outer shadow and a dark inner shadow to get that effect. A layer can only have one (outer) shadow. (Also, layer shadows are redrawn dynamically, and force CPU-based rendering which kills performance.)
You'll need to do your own drawing with CoreGraphics, either in a view's drawRect: method or a layer's drawInContext: method. (Or you draw into an image context and then reuse the image.) Said drawing will mostly use CGContext functions. (I'll name some below, but this link has documentation for them all.)
For a round rect button, you might find it tedious to create the appropriate CGPath -- instead, you can use +[UIBezierPath bezierPathWithRoundedRect:cornerRadius:] and then the path's CGPath property to set the context's current path with CGContextAddPath.
You can create an inner shadow by setting a clipping path (see CGContextClip and related functions) to the shape of the button, setting up a shadow (see CGContextSetShadowWithColor and related functions), and then drawing around the outside of the shape you want shadowed. For the inner shadow, stroke (CGContextStrokePath) a round-rect that's a bit larger than your button, using a thick stroke width (CGContextSetLineWidth) so there's plenty of "ink" to generate a shadow (remember, this stroke won't be visible due to the clipping path).
You can create an outer shadow in much the same way -- don't use a clipping path this time, because you want the shadow to be outside the shape, and fill (CGContextFillPath) the shape of your button instead of stroking it. Note that drawing a shadow is sort of a "mode": you save the graphics state (CGContextSaveGState), setup a shadow, then draw the shape you want to see a shadow of (the shape itself isn't drawn when you're in this mode), and finally restore state (CGContextRestoreGState) to get out of "shadow mode". Since that mode doesn't draw the shape, only the shadow, you'll need to draw the shape itself separately.
There's an order to do this all in, too. It should be obvious if you think about the order in which you'd paint these things with physical media: First draw the outer shadow, then the button's fill, then the inner shadow. You might add a stroke after that if the inner shadow doesn't give you a pronounced enough outline.
There are a few drawing tools which can output source code for CoreGraphics: Opacity is one that I use. Be careful with these, though, as they code they generate may not be efficient.