In one of my application, I am trying to draw a gradient arc with rounded edges. Like the following image.
This is what I have done so far using the following code.
-(void)startArc
{
UIBezierPath *roundedArc = [self arcWithRoundedCornerAt:centerPoint startAngle:DEGREES_TO_RADIANS(-90) endAngle:DEGREES_TO_RADIANS(90) innerRadius:width-20 outerRadius:width cornerRadius:0];
CAShapeLayer *mask = [CAShapeLayer new];
[mask setPath:roundedArc.CGPath];
[mask setFrame:_outerView.bounds];
[mask setShouldRasterize:YES];
[mask setRasterizationScale:[UIScreen mainScreen].scale];
CAGradientLayer *gradient = [CAGradientLayer new];
[gradient setFrame:_outerView.bounds];
// [gradient setColors:[NSArray arrayWithObjects:(id)[[UIColor colorWithRed:0.86 green:0.91 blue:0.96 alpha:1.0f] CGColor],(id)[[UIColor colorWithRed:0.98 green:0.99 blue:0.99 alpha:1.0f] CGColor], nil]];
[gradient setColors:[NSArray arrayWithObjects:(id)[UIColor colorWithRed:0.19 green:0.64 blue:0.89 alpha:1.0].CGColor,(id)[UIColor colorWithRed:0.14 green:0.76 blue:0.56 alpha:1.0f].CGColor, nil]];
[gradient setMask:mask];
[_outerView.layer addSublayer:gradient];
}
- (UIBezierPath *)arcWithRoundedCornerAt:(CGPoint)center
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
innerRadius:(CGFloat)innerRadius
outerRadius:(CGFloat)outerRadius
cornerRadius:(CGFloat)cornerRadius
{
CGFloat innerTheta = asin(cornerRadius / 2.0 / (innerRadius + cornerRadius)) * 2.0;
CGFloat outerTheta = asin(cornerRadius / 2.0 / (outerRadius - cornerRadius)) * 2.0;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:center
radius:innerRadius + cornerRadius
startAngle:endAngle - innerTheta
endAngle:startAngle + innerTheta
clockwise:false];
[path addArcWithCenter:center
radius:outerRadius - cornerRadius
startAngle:startAngle + outerTheta
endAngle:endAngle - outerTheta
clockwise:true];
[path closePath];
return path;
}
With the above code, I have achieved the following
Can someone let me know on how to achieve that rounded edges ? The one which I have implemented have sharp edges.
The easiest approach is probably:
Create a single arc path (along the middle of your target arc) as a path
Set the line width (20 in your case) and the line cap (rounded caps)
Convert the path into a stroked path (CGContextReplacePathWithStrokedPath)
Continue with your existing code for the gradient
The stroking will convert your 90 degree arc path, which is infinitely narrow, into an outline of a line that is 20 pixel wide and has rounded line endings.
The code could approximately look like this:
- (UIBezierPath *)arcWithRoundedCornerAt:(CGPoint)center
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
innerRadius:(CGFloat)innerRadius
outerRadius:(CGFloat)outerRadius
cornerRadius:(CGFloat)cornerRadius
context:(CGContext)context
{
CGFloat radius = (innerRadius + outerRadius) / 2.0;
CGContextAddArc(context, center.x, center.y, radius, startAngle, endAngle, true);
CGContextSetLineCap(context, kCGLineCapRound);
CGContextSetLineWidth(context, outerRadius - innerRadius);
CGContextReplacePathWithStrokedPath(context)
return [UIBezierPath bezierPathWithCGPath: CGContextCopyPath(context)];
}
Related
Is it possible to draw a path around the visible part of a UIBezierPath?
Here's an example of my problem
Here's what I'd like to accomplish
Here's what I got so far:
- (void)drawRect:(CGRect)rect {
CGFloat side = MIN(rect.size.width, rect.size.height);
CGPoint center = CGPointMake(rect.size.width / 2.0f, rect.size.height / 2.0f);
UIColor *yinYangColor = [UIColor whiteColor];
UIBezierPath *yinYangPath = [UIBezierPath bezierPath];
// Draw Yin&Yang part
[yinYangPath addArcWithCenter:CGPointMake(center.x, center.y - side / 4.0f) radius:side / 4.0f startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:YES];
[yinYangPath addArcWithCenter:CGPointMake(center.x, center.y + side / 4.0f) radius:side / 4.0f startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
[yinYangPath addArcWithCenter:CGPointMake(center.x, center.y) radius:side / 2.0f startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
[yinYangPath closePath];
[yinYangColor setFill];
[yinYangPath fill];
// Add border
CAShapeLayer *borderLayer = [[CAShapeLayer alloc] init];
borderLayer.path = yinYangPath.CGPath;
borderLayer.fillColor = [UIColor clearColor].CGColor;
borderLayer.strokeColor = [UIColor blackColor].CGColor;
borderLayer.lineWidth = 5.0f;
[self.layer addSublayer:borderLayer];
}
The angle π/2 radians is along the positive y axis.
In standard UIKit geometry, the positive y axis points down toward the bottom of the screen. Therefore the top arc (at center.y - side/4) needs to start at angle -π/2 and end at angle π/2. Since you got these backward, your second arc doesn't start where your first arc ended, so your path contains a straight line connecting those points. Ditto for your second and third arcs. The single straight line visible in your image is actually the combination of those two lines.
Also, incidentally, the rect passed to drawRect: is in theory not necessarily the bounds of the view. It's better not to treat it as such.
Also also, you shouldn't add sublayers in drawRect:. You should do that in init or layoutSubviews and you should make sure you don't duplicate layers. I guess maybe you're using a CAShapeLayer because you don't want the border cut off. I would solve that by insetting the view bounds by the border width:
- (void)drawRect:(CGRect)dirtyRect {
CGFloat lineWidth = 4;
CGRect rect = CGRectInset(self.bounds, lineWidth / 2, lineWidth / 2);
CGFloat side = MIN(rect.size.width, rect.size.height);
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat smallRadius = side / 4;
[path addArcWithCenter:CGPointMake(center.x, center.y - smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
[path addArcWithCenter:CGPointMake(center.x, center.y + smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
[path addArcWithCenter:center radius:side / 2 startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
[path closePath];
[path setLineJoinStyle:kCGLineJoinRound];
[path setLineWidth:lineWidth];
[[UIColor whiteColor] setFill];
[path fill];
[[UIColor blackColor] setStroke];
[path stroke];
}
Result:
If you want the bottom tip to be more pointy, I would do that by clipping all drawing to the path, then drawing the border twice as thick. Half the border will be drawn outside the path and clipped away, leaving a sharp point. In this case, you don't have to inset the bounds.
- (void)drawRect:(CGRect)dirtyRect {
CGFloat lineWidth = 4 * 2;
CGRect rect = self.bounds;
CGFloat side = MIN(rect.size.width, rect.size.height);
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
UIBezierPath *path = [UIBezierPath bezierPath];
CGFloat smallRadius = side / 4;
[path addArcWithCenter:CGPointMake(center.x, center.y - smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:NO];
[path addArcWithCenter:CGPointMake(center.x, center.y + smallRadius) radius:smallRadius startAngle:-M_PI_2 endAngle:M_PI_2 clockwise:YES];
[path addArcWithCenter:center radius:side / 2 startAngle:M_PI_2 endAngle:-M_PI_2 clockwise:NO];
[path closePath];
[path setLineJoinStyle:kCGLineJoinRound];
[path addClip];
[path setLineWidth:lineWidth];
[[UIColor whiteColor] setFill];
[path fill];
[[UIColor blackColor] setStroke];
[path stroke];
}
Result:
I am using lineWidth to create a circle with stroke width 4. I am using UIBezierPath for creating the circle. But, due to some reason, lineWidth is always rendering a circle with a thin stroke no matter what value I assign to lineWidth. I have also tried to put path.lineWidth = 100.0, but no change in stroke width.
This is my code:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(progressView.frame.size.width, progressView.frame.size.height), NO, 0.0);
[[UIColor colorWithRed:246.0/255.0 green:80.0/255.0 blue:36.0/255.0 alpha:1.0] setStroke];
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(progressView.frame.size.width/2 ,progressView.frame.size.height/2) radius:progressView.frame.size.width/2 - 5 startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(angle) clockwise:YES];
[path stroke];
path.lineWidth = 4;
UIImage* pathImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[progressView setImage:pathImg];
I googled a lot, but could not find any solution to this problem.
You need to set the stroke width before you actually stroke the path:
UIGraphicsBeginImageContextWithOptions(CGSizeMake(progressView.frame.size.width, progressView.frame.size.height), NO, 0.0);
[[UIColor colorWithRed:246.0/255.0 green:80.0/255.0 blue:36.0/255.0 alpha:1.0] setStroke];
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(progressView.frame.size.width/2 ,progressView.frame.size.height/2) radius:progressView.frame.size.width/2 - 5 startAngle:DEGREES_TO_RADIANS(0) endAngle:DEGREES_TO_RADIANS(angle) clockwise:YES];
path.lineWidth = 4;
[path stroke];
UIImage* pathImg = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
[progressView setImage:pathImg];
i try to draw a circle for a given percent value (like if i have 25% i just draw a quarter of the circle). At the moment im just able to draw a full circle within my view. Any ideas to my problem?
Code atm:
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius
{
self.circleCenter = location;
self.circleRadius = radius;
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:self.circleCenter
radius:self.circleRadius
startAngle:0.0
endAngle:M_PI * 2.0
clockwise:YES];
return path;
}
- (void)drawCircleForLocation{
CGPoint location = CGPointZero;
location.x = self.frame.size.width/2;
location.y = self.frame.size.height/2;
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.path = [[self makeCircleAtLocation:location radius:9] CGPath];
shapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
shapeLayer.fillColor = nil;
shapeLayer.lineWidth = 1.5;
[self.layer addSublayer:shapeLayer];
}
2 Pi RAD = 360° = full circle.
25% of circle = 2 PI * 25%
- (UIBezierPath *)makeCircleAtLocation:(CGPoint)location radius:(CGFloat)radius percent:(CGFloat)percent
{
self.circleCenter = location; //????
self.circleRadius = radius; //????
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:location
radius:radius
startAngle:0.0
endAngle:((M_PI * 2.0) * percent)
clockwise:YES];
[path addLineToPoint:location];
[path closePath];
return path;
}
I'm writing an app with a circular progress bar, and have recently changed it to be drawn with a couple of CAShapeLayers (one for the white background, one for the purple progress) so I can animate the purple line, rather than making new UIBeizerPaths in the drawRect of a UIView.
Having made this change, I've had some issues with how it looks now. Below is a screenshot of each way, and a diff (using “Diff” an image using ImageMagick)
My main issue with it is that it looks slightly blurry when using the CAShapeLayers - and I can't for the life of me figure out how to make it look sharper. The other issue is some of the white background is showing through the purple, but I can get around that by changing the width of the white line to be slightly less wide.
The drawRect code as it was originally written is as follows:
- (void)drawRect:(CGRect)rect {
UIBezierPath *backCircle = [UIBezierPath bezierPath];
[backCircle addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:(rect.size.width/2.0f) - 4.0f
startAngle:(self.endAngle - self.startAngle) * _percent + self.startAngle
endAngle:self.endAngle
clockwise:YES];
backCircle.lineWidth = 5;
[[UIColor whiteColor] setStroke];
[backCircle stroke];
UIBezierPath *bezierPath = [UIBezierPath bezierPath];
[bezierPath addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:(rect.size.width/2.0f) - 4.0f
startAngle:self.startAngle
endAngle:(self.endAngle - self.startAngle) * _percent + self.startAngle
clockwise:YES];
bezierPath.lineWidth = 5;
[[UIColor purpleColor] setStroke];
[bezierPath stroke];
}
with the init code:
self.startAngle = M_PI * 1.5;
self.endAngle = self.startAngle + (M_PI * 2);
And then with the 2 layers:
- (void)setUpView
{
CGFloat startAngle = M_PI * 1.5;
CGFloat endAngle = startAngle + (M_PI * 2);
UIBezierPath *processPath = [UIBezierPath bezierPath];
[processPath addArcWithCenter:self.boundsCenter
radius:self.radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
self.backgroundShapeLayer.path = [processPath CGPath];
self.progressShapeLayer.path = [processPath CGPath];
}
- (CGPoint)boundsCenter
{
return CGPointMake((self.bounds.size.width ) / 2.0, (self.bounds.size.height ) / 2.0);
}
- (CGFloat)radius
{
return (self.bounds.size.width / 2.0) - 4.0f;
}
- (CAShapeLayer *)progressShapeLayer
{
if (_progressShapeLayer == nil) {
_progressShapeLayer = [CAShapeLayer layer];
_progressShapeLayer.fillColor = [[UIColor clearColor] CGColor];
_progressShapeLayer.lineWidth = 5.0;
_progressShapeLayer.strokeStart = 0.0;
_progressShapeLayer.contentsScale = [[UIScreen mainScreen] scale];
[self.layer addSublayer:_progressShapeLayer];
}
return _progressShapeLayer;
}
- (CAShapeLayer *)backgroundShapeLayer
{
if (_backgroundShapeLayer == nil) {
_backgroundShapeLayer = [CAShapeLayer layer];
_backgroundShapeLayer.fillColor = [[UIColor clearColor] CGColor];
_backgroundShapeLayer.strokeColor = [[UIColor whiteColor] CGColor];
_backgroundShapeLayer.lineWidth = 5.0;
_backgroundShapeLayer.strokeStart = 0.0;
_backgroundShapeLayer.contentsScale = [[UIScreen mainScreen] scale];
[self.layer addSublayer:_backgroundShapeLayer];
}
return _backgroundShapeLayer;
}
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated
{
if(isnan(progress) || progress < 0) return;
_progress = progress;
if(!animated) {
[CATransaction setDisableActions:YES];
}
self.progressShapeLayer.strokeColor = [self.progressColour CGColor];
self.progressShapeLayer.strokeEnd = _progress;
}
I really feel like I'm going a bit crazy staring at the zoomed in screen, but it just doesn't look quite right...
Hiiii
I'm trying to draw in a view an arc with dynamic width.
I have built an ARRAY with several UIBezierPath with arcs then I draw one after the other. this is the code:
- (void)drawRect:(CGRect)rect
{
// Drawing code
CGFloat radious;
if (self.bounds.size.width < self.bounds.size.height) {
radious = self.bounds.size.width / 2;
}
else{
radious = self.bounds.size.height / 2;
}
float oneGradeInRadians = (float)(2 * M_PI) / 360.f;
float radiandToDraw = (float)self.finalAngleRadians - self.initialAngleRadians;
float splitMeasure = oneGradeInRadians;
int numberOfArcs = radiandToDraw / splitMeasure;
NSMutableArray *arrayOfBeziersPaths = [NSMutableArray arrayWithCapacity:numberOfArcs];
for (int i = 0; i < numberOfArcs; i++) {
float startAngle = self.initialAngleRadians + (i * splitMeasure);
float endAngle = self.initialAngleRadians +((i + 1) * splitMeasure);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2, self.frame.size.height/2) radius:radious - self.widthLine.floatValue startAngle:startAngle endAngle:endAngle clockwise:YES];
bezierPath.lineWidth = self.widthLine.floatValue + i/20;
float hue = (float)(i / 3.f);
UIColor *color = [UIColor colorWithHue:hue/360.0 saturation:1 brightness:1 alpha:1];
[color setStroke];
[arrayOfBeziersPaths addObject:bezierPath];
}
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineJoin(context, kCGLineJoinMiter);
//We saved the context
CGContextSaveGState(context);
for (int i = 0; i < numberOfArcs; i++) {
[(UIBezierPath *)[arrayOfBeziersPaths objectAtIndex:i] stroke];
[(UIBezierPath *)[arrayOfBeziersPaths objectAtIndex:i] fill];
}
//Restore the contex
CGContextRestoreGState(context);
}
In this way I get the right shape but, also I get an annoying lines between the arcs:
I think the problem may be a property to change between arcs, but I'm totally lost, or maybe there is another better way to build this.
I tried creating several UIBezierPath paths and I added them to an unique UIBezierPath, then I stoke and fill that path. I didn't get the annoying lines, but the problem is I can not modify the line width, so I can not get the same effect.
Any idea? thanks
dont create many such paths. just create two arcs (inner circle, exterior circle) and two straight lines joining the ends. then fill the path.
Add below code in a viewDidLoad of an empty viewController and check.
- (void)viewDidLoad
{
[super viewDidLoad];
CGFloat k =0.5522847498;
UIBezierPath *path =[UIBezierPath bezierPath];
CGFloat radius=130;
[path addArcWithCenter:CGPointMake(0, 0) radius:radius startAngle:M_PI_2 endAngle:M_PI clockwise:NO];
CGFloat start=10;
CGFloat increment=5;
[path addLineToPoint:CGPointMake(-radius-start, 0)];
[path addCurveToPoint:CGPointMake(0, -radius-start-increment) controlPoint1:CGPointMake(-radius-start, -(radius +start)*k) controlPoint2:CGPointMake(-(radius+start)*k, -radius-start-increment)];
[path addCurveToPoint:CGPointMake(radius+start+2*increment, 0) controlPoint1:CGPointMake((radius+start+increment)*k, -radius-start-increment) controlPoint2:CGPointMake(radius+start+2*increment, (-radius-start-increment)*k)];
[path addCurveToPoint:CGPointMake(0, radius+start+3*increment) controlPoint1:CGPointMake(radius+start+2*increment, (radius+start+2*increment)*k) controlPoint2:CGPointMake((radius+start+2*increment)*k,radius+start+3*increment)];
[path addLineToPoint:CGPointMake(0,radius)];
CAShapeLayer *layer =[CAShapeLayer layer];
[layer setFrame:CGRectMake(150, 200, 300, 300)];
[layer setFillColor:[UIColor greenColor].CGColor];
[layer setStrokeColor:[UIColor blackColor].CGColor];
[layer setPath:path.CGPath];
[self.view.layer addSublayer:layer];
}
It produced result like,
The problem here is that you’ve chosen a crazy approach to drawing the shape you’re after. If you take a look at it, you’ll see that it’s actually a filled region bounded on the outside by a circular arc, and on the inner edge by a spiral.
What you should do, therefore, is create a single NSBezierPath, add the outer circular arc to it, then add a line to the start of your spiral, append a spiral (you’ll want to approximate it with Bézier segments) and finally call -closePath. After that, you can -fill the path and you’ll get a good looking result rather than the mess you have above.