Create the indented look found in UINavigationBarButton - programmatically - ios

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.

Related

How to make custom drawing in ios?

I want to make custom drawing so that i could convert it to image.
i have heard of UIBezierPath but donot know much about it, my purpose is to change color of it on basis of user's selection of color.
Create a CGGraphcisContext and get an image like this:
UIGraphicsBeginImageContextWithOptions(bounds.size, NO , [[UIScreen mainScreen] scale]);
// set the fill color (UIColor *)
[userSelectedColor setFill];
//create your path
UIBezierPath *path = ...
//fill the path with your color
[path fill];
UIImage *outputImage = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
You might have to combine multiple paths to get your desired shape. First create the 'drop' with bezier paths. The path might look something like this:
//Create the top half of the circle
UIBezierPath *drop = [UIBezierPath bezierPathWithArcCenter:CGPointMake(CGRectGetWidth(bounds)*0.5f, CGRectGetWidth(bounds)*0.5f)
radius:CGRectGetWidth(bounds)*0.5f
startAngle:0
endAngle:DEGREES_TO_RADIANS(180)
clockwise:NO];
//Add the first half of the bottom part
[drop addCurveToPoint:CGPointMake(CGRectGetWidth(bounds)*0.5f,CGRectGetHeight(bounds))
controlPoint1:CGPointMake(CGRectGetWidth(bounds),CGRectGetWidth(bounds)*0.5f+CGRectGetHeight(bounds)*0.1f)]
controlPoint2:CGPointMake(CGRectGetWidth(bounds)*0.6f,CGRectGetHeight(bounds)*0.8f)];
//Add the second half of the bottom part beginning from the sharp corner
[drop addCurveToPoint:CGPointMake(0,CGRectGetWidth(bounds)*0.5f)
controlPoint1:CGPointMake(CGRectGetWidth(bounds)*0.4f,CGRectGetHeight(bounds)*0.8f)
controlPoint2:CGPointMake(0,CGRectGetWidth(bounds)*0.5f+CGRectGetHeight(bounds)*0.1f)];
[drop closePath];
Not entirely sure if this works since I couldn't test it right now. You might have to play with the controls points a bit. It could be that I made some error with the orientation.

core gore graphics and alpha values

I am draw some circle on a map with
CGContextFillEllipseInRect
This works great, but the only issue is that I have the alpha value set to .5
If tow or more circle overlap the the overlap area is darker because of the two alpha values being added I suppose. Is there an easy way to maintain the same alpha value regardless if the circle overlap?
Thanks
If you're working with closed shapes and drawing them all in the same drawrect (or at least in the same context) you can accomplish what you want by using UIBezierPath.
// create a path with a circle
UIBezierPath* path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(50, 50) radius:50.0f startAngle:0 endAngle:M_PI * 2.0f clockwise:YES];
// add another circle to the path
[path appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:50.0f startAngle:0 endAngle:M_PI * 2.0f clockwise:YES]];
// draw them both, since they are in a single path they will be treated as a single shape
[path fill];
Draw the circles at full opacity, and set the opacity of the view/layer to 0.5.
EDIT:
To clarify, I assume you know how to stop drawing your circles at half opacity, since you managed to set it up that way in the first place. So make the opacity of your circles be 1.0. Then, set your view's opacity to 0.5 by doing the following:
view.alpha = 0.5;
I'm assuming you have a UIView subclass, so this can either be in the place in your code where you create the view or it could be inside the subclass in your init method for it. So if you're doing it from within the UIView subclass, it would be self instead of view:
self.alpha = 0.5;
Also, this will make all of the drawing you do in this view be 0.5 opacity. If you want to have some content that's full opacity and some that's half opacity, the simplest solution I can give you is to have two different views, one for the half opacity content, the other for the full opacity content. There are better ways to accomplish this, but for the sake of communicating easily a solution, this is probably easiest.

Can I add a custom line cap to a UIBezierPath?

I'm drawing an arc by creating a CAShapeLayer and giving it a Bezier path like so:
self.arcLayer = [CAShapeLayer layer];
UIBezierPath *remainingLayerPath = [UIBezierPath bezierPathWithArcCenter:self.center
radius:100
startAngle:DEGREES_TO_RADIANS(135)
endAngle:DEGREES_TO_RADIANS(45)
clockwise:YES];
self.arcLayer.path = remainingLayerPath.CGPath;
self.arcLayer.position = CGPointMake(0,0);
self.arcLayer.fillColor = [UIColor clearColor].CGColor;
self.arcLayer.strokeColor = [UIColor blueColor].CGColor;
self.arcLayer.lineWidth = 15;
This all works well, and I can easily animate the arc from one side to the other. As it stands, this gives a very squared edge to the ends of my lines. Can I round the edges of these line caps with a custom radius, like 3 (one third the line width)? I have played with the lineCap property, but the only real options seem to be completely squared or rounded with a larger corner radius than I want. I also tried the cornerRadius property on the layer, but it didn't seem to have any effect (I assume because the line caps are not treated as actual layer corners).
I can only think of two real options and I'm not excited about either of them. I can come up with a completely custom Bezier path tracing the outside of the arc, complete with my custom rounded edges. I'm concerned however about being able to animate the arc in the same fashion (right now I'm just animating the stroke from 0 to 1). The other option is to leave the end caps square and mask the corners, but my understanding is that masking is relatively expensive, and I'm planning on doing some fairly intensive animations with this view.
Any suggestions would be helpful. Thanks in advance.
I ended up solving this by creating two completely separate layers, one for the left end cap and one for the right end cap. Here's the right end cap example:
self.rightEndCapLayer = [CAShapeLayer layer];
CGRect rightCapRect = CGRectMake(remainingLayerPath.currentPoint.x, remainingLayerPath.currentPoint.y, 0, 0);
rightCapRect = CGRectInset(rightCapRect, self.arcWidth / -2, -1 * endCapRadius);
self.rightEndCapLayer.frame = rightCapRect;
self.rightEndCapLayer.path = [UIBezierPath bezierPathWithRoundedRect:self.rightEndCapLayer.bounds
byRoundingCorners:UIRectCornerBottomLeft | UIRectCornerBottomRight
cornerRadii:CGSizeMake(endCapRadius, endCapRadius)].CGPath;
self.rightEndCapLayer.fillColor = self.remainingColor.CGColor;
// Rotate the end cap
self.rightEndCapLayer.anchorPoint = CGPointMake(.5, 0);
self.rightEndCapLayer.transform = CATransform3DMakeRotation(DEGREES_TO_RADIANS(45), 0.0, 0.0, 1.0);
[self.layer addSublayer:self.rightEndCapLayer];
Using the bezier path's current point saves from doing a lot of math to calculate where the end point should appear. Moving the anchoring point also allows the layers to not overlap, which is important if your arc is at all transparent.
This still isn't entirely ideal, as animations have to be chained through multiple layers. It's better than the alternatives I could come up with though.

How to create the shape using UIBezier Path

I want to achieve the shape shown in image using UIBezier Path, and too the shape is filled with blocks in image it shows one block is filled, how to achieve this.
I have tried the following code taken from here
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, 10)];
[path addQuadCurveToPoint:CGPointMake(200, 10) controlPoint:CGPointMake(100, 5)];
[path addLineToPoint:CGPointMake(200, 0)];
[path addLineToPoint:CGPointMake(0, 0)];
[path closePath];
Thanks.
It looks to me like both the outline and also each block has the same shape. What you would probably do is to make one shape for the outline, and stroke it, and one shape for each cell and fill it.
Creating the shape
Each shape could be created something like this (as I've previously explained in this answer). It's done by stroking one path (the orange arc) which is a simple arc from one angle to another to get another path (the dashed outline)
Before we can stroke the path we to create it. CGPath's work just like UIBezierPath but with a C API. First we move to the start point, then we add an arc around the center from the one angle to another angle.
CGMutablePathRef arc = CGPathCreateMutable();
CGPathMoveToPoint(arc, NULL,
startPoint.x, startPoint.y);
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
YES);
Now that we have the centered arc, we can create one shape path by stroking it with a certain width. The resulting path is going to have the two straight lines (because we specify the "butt" line cap style) and the two arcs (inner and outer). As you saw in the image above, the stroke happens from the center an equal distance inwards and outwards.
CGFloat lineWidth = 10.0;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
You would do this a couple of times to create one path for the stroked outline and one path for each cell.
Drawing the shape
Depending on if it's the outline or a cell you would either stroke it or fill it. You can either do this with Core Graphics inside drawRect: or with Core Animation using CAShapeLayers. Choose one and don't between them :)
Core Graphics
When using Core Graphics (inside drawRect:) you get the graphics context, configure the colors on it and then stroke the path. For example, the outline with a gray fill color and a black stroke color would look like this:
I know that your shape is filled white (or maybe it's clear) with a light blue stroke but I already had a gray and black image and I didn't want to create a new one ;)
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc); // the path we created above
CGContextSetFillColorWithColor(c, [UIColor lightGrayColor].CGColor);
CGContextSetStrokeColorWithColor(c, [UIColor blackColor].CGColor);
CGContextDrawPath(c, kCGPathFillStroke); // both fill and stroke
That will put something like this on screen
Core Animation
The same drawing could be done with a shape layer like this:
CAShapeLayer *outline = [CAShapeLayer layer];
outline.fillColor = [UIColor lightGrayColor].CGColor;
outline.strokeColor = [UIColor blackColor].CGColor;
outline.lineWidth = 1.0;
outline.path = strokedArc; // the path we created above
[self.view.layer addSublayer: outline];

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.

Resources