How to draw an Arc using core draw - ios

I want to draw an arc like in the following figure using core draw.
Not exactly same but I have background images set. I need to draw arc based on the file download. Now the percentage of download number I have. How can I do it using core draw?

The image above are just two circles - one is cutting a hole in another. This is not near to what you want to accomplish.
You need to use CGContextAddArc.
Do something like this:
open a path
move to a point
start drawing the Arc with CGContextAddArc
move (and draw a line) inwards to the arc center the desired width of the slice
draw an backward arc
close the path

Use Mid point Algorithm, If you want to draw circles , to make concentric circle provide difference in radius of your circles

I have what you want:
// CircularProgress.h
#import <UIKit/UIKit.h>
#interface CircularProgress : UIView
#property (nonatomic, assign) CGFloat percent;
#end
// CircularProgress.m
#import "CircularProgress.h"
#implementation CircularProgress
- (void)setPercent:(CGFloat)percent
{
_percent = percent;
[self setNeedsDisplay];
}
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
CGPoint center = CGPointMake(bounds.size.width / 2.0, bounds.size.height / 2.0);
CGFloat lineWidth = 8.0;
CGFloat innerRadius = (bounds.size.width / 2.0) - lineWidth;
CGFloat outerRadius = innerRadius + lineWidth;
CGFloat startAngle = -((float)M_PI / 2.0);
CGFloat endAngle = ((self.percent / 100.0) * 2 * (float)M_PI) + startAngle;
UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath];
processBackgroundPath.lineWidth = lineWidth;
CGFloat radius = (self.bounds.size.width - lineWidth) / 2.0;
CGFloat fullAngle = (2.0 * (float)M_PI) + startAngle;
[processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:fullAngle clockwise:YES];
[[UIColor whiteColor] set];
[processBackgroundPath stroke];
CGMutablePathRef progressPath = CGPathCreateMutable();
CGPathMoveToPoint(progressPath, NULL, center.x, center.y - innerRadius);
CGPathAddArc(progressPath, NULL, center.x, center.y, innerRadius, startAngle, endAngle, YES);
CGPathAddArc(progressPath, NULL, center.x, center.y, outerRadius, endAngle, startAngle, NO);
CGPathCloseSubpath(progressPath);
UIColor *aColor = [UIColor colorWithRed:0.941 green:0.776 blue:0.216 alpha:1.0];
[aColor setFill];
CGContextAddPath(context, progressPath);
CGContextFillPath(context);
CGPathRelease(progressPath);
}
#end
You just need to create an object of CircularProgress class of the desired size (it should be square) and add it as subview to your main view. Then simply change the percent value and enjoy result. The color and width are hardcoded for now because it's not the finished control but you should catch the idea.

Related

Draw Triangles inside Circle Objective-C [duplicate]

For an iPhone application I want to draw a circle, that is only for an x percentage filled.
Something like this:
I have no problems calculating the radius, the degrees or the radians, that is no problem. Also drawing the circle is already done. But how do I get the iPhone SDK to draw the part that is filled.
I can draw a rectangle that size, but not part of a circle.
I just want to draw that on a a normal context.
Hope someone can give me any pointers here.
A lot of people have showed you how this can be done in Core Graphics but it can also be done with Core Animation which gives the big addition of easily being able to animate the percentage of the pie shape.
The following code will create both the ring and the partly filled layers (even though you said that you already can draw the ring) since its nice to have both the ring and the pie shape to be drawn using the same method.
If you animate the strokeStart or strokeEnd properties of the pieShape layer you will have the percentage animate. As with all Core Animation code you will need to add QuartzCore.framework to your project and include <QuartzCore/QuartzCore.h> in your code.
// Create a white ring that fills the entire frame and is 2 points wide.
// Its frame is inset 1 point to fit for the 2 point stroke width
CGFloat radius = MIN(self.frame.size.width,self.frame.size.height)/2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
// Create a white pie-chart-like shape inside the white ring (above).
// The outside of the shape should be inside the ring, therefore the
// frame needs to be inset radius/2 (for its outside to be on
// the outside of the ring) + 2 (to be 2 points in).
CAShapeLayer *pieShape = [CAShapeLayer layer];
inset = radius/2 + 2; // The inset is updated here
pieShape.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
pieShape.fillColor = [UIColor clearColor].CGColor;
pieShape.strokeColor = [UIColor whiteColor].CGColor;
pieShape.lineWidth = (radius-inset)*2;
// Add sublayers
// NOTE: the following code is used in a UIView subclass (thus self is a view)
// If you instead chose to use this code in a view controller you should instead
// use self.view.layer to access the view of your view controller.
[self.layer addSublayer:ring];
[self.layer addSublayer:pieShape];
Use CGContext's arc functions:
CGContextAddArc(context,
centerX,
centerY,
radius,
startAngleRadians,
endAngleRadians,
clockwise ? 1 : 0);
See the documentation for CGContextAddArc().
Try this:
CGContextMoveToPoint(the center point)
CGContextAddLineToPoint(the starting point of the fill path on the circumference)
CGContextAddArcToPoint(the ending point of the fill path on the circumference)
CGContextAddLineToPoint(the center point)
CGContextFillPath
I implemented a pie progress view that looks similar to what you are doing. It's open source. Hopefully the source code will help.
SSPieProgressView.h source
SSPieProgressView.m source
CircleViewController.h
#import <UIKit/UIKit.h>
#interface CircleViewController : UIViewController
#end
CircleViewController.m
#import "CircleViewController.h"
#import "GraphView.h"
#interface CircleViewController ()
#end
#implementation CircleViewController
- (void)viewDidLoad {
[super viewDidLoad];
GraphView *graphView = [[GraphView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
graphView.backgroundColor = [UIColor whiteColor];
graphView.layer.borderColor = [UIColor redColor].CGColor;
graphView.layer.borderWidth = 1.0f;
[self.view addSubview:graphView];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
#end
GraphView.h
#import <UIKit/UIKit.h>
#interface GraphView : UIView
#end
GraphView.m
#import "GraphView.h"
#implementation GraphView
- (void)drawRect:(CGRect)rect {
CGPoint circleCenter = CGPointMake(self.bounds.size.width / 2, self.bounds.size.height / 2);
[self drawCircleWithCircleCenter:(CGPoint) circleCenter radius:80 firstColor:[UIColor blueColor].CGColor secondeColor:[UIColor redColor].CGColor lineWidth:2 startDegree:0 currentDegree:90];
//[self drawCircleWithCircleCenter2:(CGPoint) circleCenter radius:80 firstColor:[UIColor blueColor].CGColor secondeColor:[UIColor redColor].CGColor lineWidth:2 startDegree:0 currentDegree:90];
}
- (void)drawCircleWithCircleCenter:(CGPoint) circleCenter
radius:(CGFloat)radius
firstColor:(CGColorRef)firstColor
secondeColor:(CGColorRef)secondeColor
lineWidth:(CGFloat)lineWidth
startDegree:(float)startDegree
currentDegree:(float)endDegree {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, lineWidth);
CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
CGContextAddArc(context, circleCenter.x , circleCenter.y, radius, [self radians:startDegree], [self radians:endDegree], 0);
CGContextSetFillColorWithColor(context, firstColor);
CGContextFillPath(context);
CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
CGContextAddArc(context, circleCenter.x, circleCenter.y, radius, [self radians:endDegree], [self radians:startDegree], 0);
CGContextSetFillColorWithColor(context, secondeColor);
CGContextFillPath(context);
}
- (void)drawCircleWithCircleCenter2:(CGPoint) circleCenter
radius:(CGFloat)radius
firstColor:(CGColorRef)firstColor
secondeColor:(CGColorRef)secondeColor
lineWidth:(CGFloat)lineWidth
startDegree:(float)startDegree
currentDegree:(float)endDegree {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetLineWidth(context, lineWidth);
CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
CGContextAddArc(context, circleCenter.x , circleCenter.y, radius, [self radians:startDegree], [self radians:endDegree], 0);
CGContextSetFillColorWithColor(context, firstColor);
CGContextFillPath(context);
CGContextMoveToPoint(context, circleCenter.x, circleCenter.y);
CGContextAddArc(context, circleCenter.x, circleCenter.y, radius, [self radians:endDegree], [self radians:startDegree], 0);
CGContextSetStrokeColorWithColor(context, secondeColor);
CGContextStrokePath(context);
}
-(float) radians:(double) degrees {
return degrees * M_PI / 180;
}
#end
note: you can use one of the 2 methods:
"drawCircleWithCircleCenter" or "drawCircleWithCircleCenter2"
this code if you want to split cell on 2 parts only
if you want to split cell on more than 2 parts you can check this : "Drawing a circle ,filled different parts with different color" and check the answer start with this Phrase "we have 6 class"
Well, since nobody used NSBezierPath so far, I figured I could provide the solution I recently used for the same problem:
-(void)drawRect:(NSRect)dirtyRect
{
double start = -10.0; //degrees
double end = 190.0; //degrees
NSPoint center = NSMakePoint(350, 200);
double radius = 50;
NSBezierPath *sector = [NSBezierPath bezierPath];
[sector moveToPoint:center];
[sector appendBezierPathWithArcWithCenter:center radius:radius startAngle:start endAngle:end];
[sector lineToPoint:center];
[sector fill];
}
Below is a full method I am using that does this with Core Graphics, adapting and expanding on mharper's comment above.
This code is for OSX Cocoa, but could easily be changed to iOS, by modifying how you get the context.
- (void)drawPieShapedCircleWithRadius:(CGFloat)radius
strokeColor:(CGColorRef)strokeColor
fillColor:(CGColorRef)fillColor
lineWidth:(CGFloat)lineWidth
currentDegrees:(float)currentDegrees
startDegrees:(float)startDegrees {
// get the context
CGContextRef context = [[NSGraphicsContext currentContext] graphicsPort];
// Set the color of the circle stroke and fill
CGContextSetStrokeColorWithColor(context, strokeColor);
CGContextSetFillColorWithColor(context, fillColor);
// Set the line width of the circle
CGContextSetLineWidth(context, 1);
// Calculate the middle of the circle
CGPoint circleCenter = CGPointMake(self.frame.size.width / 2, self.frame.size.height / 2);
// Move the bezier to the center of the circle
CGContextMoveToPoint(context, circleCenter.x, circleCenter.y); // move to the center point
// Draw the arc from the start point (hardcoded as the bottom of the circle) to the center
CGContextAddLineToPoint(context, circleCenter.x, circleCenter.y + radius);
// Draw the arc around the circle from the start degrees point to the current degrees point
CGContextAddArc(context, circleCenter.x , circleCenter.y, radius, [self radians:startDegrees], [self radians:startDegrees + currentDegrees], 0);
// Draw the line back into the center of the circle
CGContextAddLineToPoint(context, circleCenter.x, circleCenter.y);
// Fill the circle
CGContextFillPath(context);
// Draw the line around the circle
CGContextStrokePath(context);
}
Try this code in a UIView, Example "MyChartClass"...
- (void)drawRect:(CGRect)rect {
int c=(int)[itemArray count];
CGFloat angleArray[c];
CGFloat offset;
int sum=0;
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetAllowsAntialiasing(context, false);
CGContextSetShouldAntialias(context, false);
for(int i=0;i<[itemArray count];i++) {
sum+=[[itemArray objectAtIndex:i] intValue];
}
for(int i=0;i<[itemArray count];i++) {
angleArray[i]=(float)(([[itemArray objectAtIndex:i] intValue])/(float)sum)*(2*3.14);
CGContextMoveToPoint(context, radius, radius);
if(i==0)
CGContextAddArc(context, radius, radius, radius, 0,angleArray[i], 0);
else
CGContextAddArc(context, radius, radius, radius,offset,offset+angleArray[i], 0);
offset+=angleArray[i];
CGContextSetFillColorWithColor(context, ((UIColor *)[myColorArray objectAtIndex:i]).CGColor);
CGContextClosePath(context);
CGContextFillPath(context);
}
}
Implementation in your UIViewController
MyChartClass *myChartClass=[[MyChartClass alloc]initWithFrame:CGRectMake(0, 0, 200, 200)];
myChartClass.backgroundColor = [UIColor clearColor];
myChartClass.itemArray=[[NSArray alloc]initWithObjects:#"75",#"25", nil];
myChartClass.myColorArray=[[NSArray alloc]initWithObjects:[UIColor blackColor],[UIColor whiteColor], nil];
myChartClass.radius=100;
[self.view addSubview:myChartClass];
Regards.

Using Core Graphics from ObjectiveC, how to you curve an arrow around a circle?

Our designer has asked me to recreate this:
Im subclassing UIView, and I've overridden the drawRect command like this:
[super drawRect:frame];
CGFloat x = self.frame.origin.x;
CGFloat y = self.frame.origin.y;
CGFloat w = self.frame.size.width;
CGFloat h = self.frame.size.height;
CGFloat lineWidth = lineWidthRequested;
CGPoint centerPoint = CGPointMake(w/2, h/2);
CGFloat radius = radiusRequested;
CGFloat startAngle = 3 * M_PI / 2;
CGFloat endAngle = startAngle + percentage * 2 * M_PI;
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
centerPoint.x, centerPoint.y,
radius,
startAngle,
endAngle,
NO);
CGPathRef strokedArc = CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CGContextRef c = UIGraphicsGetCurrentContext();
CGContextAddPath(c, strokedArc);
CGContextSetFillColorWithColor(c, [UIColor colorWithRed:239/255.0 green:101/255.0 blue:47/255.0 alpha:1.0].CGColor);
CGContextDrawPath(c, kCGPathFill);
What I ended up with is this:
Gotta still draw the arrowhead. Gonna be easy, right?
After struggling to remember my trig, I found rotation of points around a center on this page:
Rotating a point around an origin in VB
But when I tried translation to objective C to draw the arrowhead, I'm getting very odd results. Here's the code further down in drawRect:
CGFloat triangle[3][2] = {{centerPoint.x + 10, h - (centerPoint.y + radius)},
{centerPoint.x, h - (centerPoint.y + radius + lineWidth/2)},
{centerPoint.x, h - (centerPoint.y + radius - lineWidth/2)}};
for (int idx=0;idx < 3; idx++) {
// translate to origin
triangle[idx][0] -= centerPoint.x;
triangle[idx][1] -= centerPoint.y;
}
CGFloat angDistance = endAngle - startAngle;
CGFloat ct = cos(angDistance);
CGFloat st = sin(angDistance);
for (int idx=0;idx < 3; idx++) {
// rotate
triangle[idx][0] = ct * triangle[idx][0] - st * triangle[idx][1];
triangle[idx][1] = -st * triangle[idx][0] + ct * triangle[idx][1];
}
for (int idx=0;idx < 3; idx++) {
// translate back to position
triangle[idx][0] += centerPoint.x;
triangle[idx][1] += centerPoint.y;
}
NSLog(#"Rotating through %g, %06.1f,%06.1f , ct - %g, st - %g",angDistance, triangle[0][0],triangle[0][1],ct, st);
// XXX todo draw the filled triangle at end.
// draw a red triangle, the point of the arrow
CGContextSetFillColorWithColor(c, [[UIColor greenColor] CGColor]);
CGContextMoveToPoint(c, triangle[0][0], triangle[0][1]);
CGContextAddLineToPoint(c, triangle[1][0], triangle[1][1]);
CGContextAddLineToPoint(c, triangle[2][0], triangle[2][1]);
CGContextFillPath(c);
I was expecting that I make these points, then translate them to an origin, rotate, and then translate them back, I'd be laughing.
However, that's not what's happening...as the percentage increases from 0 to 2pi, the arrowhead draws itself in a vaguely triangular route. When the angDistance is zero or pi, the arrowhead is in the right place. As I head towards pi/2 or 3pi/2 though, the arrowhead heads off towards the lower corners of an enclosing rect.
I must be doing something blatantly stupid, but I can't for the life of me see it.
Any ideas?
Thanks,
-Ken
I'd suggest constructing a path for the entire outline of the desired shape and then "fill" that path with the desired color. That eliminates any risk of any gaps or anything not quite lining up.
Thus, this path might consisting of an arc for the outside of the arrow, two lines for the head of the arrow, an arc back for the inside of the arrow, and then close the path. That might look like:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextSetFillColorWithColor(context, self.arrowColor.CGColor);
CGPoint center = CGPointMake(rect.size.width / 2.0, rect.size.height / 2.0);
CGContextMoveToPoint(context, center.x + cosf(self.startAngle) * (self.radius + self.lineWidth / 2.0),
center.y + sinf(self.startAngle) * (self.radius + self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius + self.lineWidth / 2.0, self.startAngle, self.endAngle, !self.clockwise);
CGFloat theta = asinf(self.lineWidth / self.radius / 2.0) * (self.clockwise ? 1.0 : -1.0);
CGFloat pointDistance = self.radius / cosf(theta);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle + theta) * pointDistance,
center.y + sinf(self.endAngle + theta) * pointDistance);
CGContextAddLineToPoint(context, center.x + cosf(self.endAngle) * (self.radius - self.lineWidth / 2.0),
center.y + sinf(self.endAngle) * (self.radius - self.lineWidth / 2.0));
CGContextAddArc(context, center.x, center.y, self.radius - self.lineWidth / 2.0, self.endAngle, self.startAngle, self.clockwise);
CGContextClosePath(context);
CGContextDrawPath(context, kCGPathFill);
}
The only trick here was coming up with the right point for the end of the arrow. I've improved the choice to handle fatter arrows a little better, but you should feel free to use whatever you feel is best for your application.
Thus, the following code:
self.arrowView.radius = 100;
self.arrowView.arrowColor = [UIColor blueColor];
self.arrowView.lineWidth = 40;
self.arrowView.startAngle = -M_PI_2;
self.arrowView.endAngle = M_PI;
self.arrowView.clockwise = TRUE;
would yield the following (which I'm animating with a CADisplayLink):
This uses the start angle of zero as meaning the "3 o'clock" position, but you can obviously tweak this as you see fit. But hopefully it illustrates one approach to the problem.
By the way, while I've answered the question of how to do this with CoreGraphics, I wouldn't necessarily suggest doing so. For example, in https://github.com/robertmryan/CircularArrowDemo, I don't implement drawRect, but instead update a CAShapeLayer. By doing this, not only do I avoid drawRect inefficiencies, but one could theoretically also change how you use this CAShapeLayer (e.g. use it as a mask for some UIView, revealing some more interesting color gradation (or other image) behind it).
Here is another solution (not very scalable though). This solution assumes this is like a logo where the angle/percent of circle drawn will not change.
- (void)drawRect:(CGRect)rect {
UIBezierPath *circleOutline = [UIBezierPath bezierPath];
[self.circleColor setStroke];
[circleOutline setLineWidth:self.bounds.size.width*0.15];
[circleOutline addArcWithCenter:CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2) radius:self.bounds.size.width/2-circleOutline.lineWidth/2 startAngle:3*M_PI/2 endAngle:3*M_PI/4 clockwise:YES];
[circleOutline stroke];
[self addArrowView:circleOutline];
}
- (void)addArrowView:(UIBezierPath *)path {
for (int x = 0; x < self.bounds.size.width/2; x++) {
for (int y = self.bounds.size.height/2; y < self.bounds.size.height; y++) {
if ([path containsPoint:CGPointMake(x, y)]) {
// Pythagorean Theorem - We want the diagonal length of the square to be lineWidth, so we need to calculate what size
// to make each side of the square to make the diagonal equal to lineWidth
double sideLength = sqrt((path.lineWidth*path.lineWidth)/2);
UIView *arrowView = [[UIView alloc] initWithFrame:CGRectMake(x-sideLength/2, y-sideLength/2, sideLength, sideLength)];
arrowView.backgroundColor = self.circleColor;
[self addSubview:arrowView];
return;
}
}
}
}
would yield:

Draw circle with hole by using MKOverlayRenderer does not work well

I'd like to draw circle with hole (like donut) on mapview.
my code is here.
- (void)drawMapRect:(MKMapRect)mapRect zoomScale:(MKZoomScale)zoomScale inContext:(CG ContextRef)context {
WPCircleOverlay * circleOverlay = self.overlay;
CGPoint centerPoint = [self pointForMapPoint:MKMapPointForCoordinate(circleOverlay.coordinate)];
CGFloat innerRadius = MKMapPointsPerMeterAtLatitude(circleOverlay.coordinate.latitude) * circleOverlay.innerRadius;
CGFloat outerRadius = MKMapPointsPerMeterAtLatitude(circleOverlay.coordinate.latitude) * circleOverlay.outerRadius;
CGMutablePathRef path = CGPathCreateMutable();
//CGPathMoveToPoint(path, ...);
CGPathAddArc(path, NULL, centerPoint.x, centerPoint.y, outerRadius, 0, 2 * M_PI, true);
CGPathCloseSubpath(path);
// Add the inner arc to the path (later used to substract the inner area)
CGPathAddArc(path, NULL, centerPoint.x, centerPoint.y, innerRadius, 0, 2 * M_PI, true);
CGPathCloseSubpath(path);
// Add the path to the context
CGContextAddPath(context, path);
CGContextSetFillColorWithColor(context, self.fillColor.CGColor);
CGContextEOFillPath(context);
CGPathRelease(path);
It works well in simulator, but on device it doesn't.
On device, outer circle is filled with color, and inner circle wasn't clipped.
How can I modify my code to work well on device?
I fixed it by using CGContextAddEllipseInRect method instead of CGContextAddArc.
WPCircleOverlay * circleOverlay = self.overlay;
CGRect rectForMapRect = [self rectForMapRect:mapRect];
CGPoint centerPoint = [self pointForMapPoint:MKMapPointForCoordinate(circleOverlay.coordinate)];
CGFloat innerRadius = MKMapPointsPerMeterAtLatitude(circleOverlay.coordinate.latitude) * circleOverlay.innerRadius;
CGFloat outerRadius = MKMapPointsPerMeterAtLatitude(circleOverlay.coordinate.latitude) * circleOverlay.outerRadius;
CGRect innerRect = CGRectMake(centerPoint.x - innerRadius, centerPoint.y - innerRadius, innerRadius * 2.0, innerRadius * 2.0);
CGRect outerRect = CGRectMake(centerPoint.x - outerRadius, centerPoint.y - outerRadius, outerRadius * 2.0, outerRadius * 2.0);
if (CGRectIntersectsRect(rectForMapRect, outerRect)) {
CGContextAddRect(context, rectForMapRect);
CGContextSaveGState(context);
CGContextClip(context);
CGContextAddEllipseInRect(context, outerRect);
CGContextAddEllipseInRect(context, innerRect);
CGContextSaveGState(context);
CGContextEOClip(context);
UIColor * color = [self.fillColor copy];
CGContextSetFillColorWithColor(context, color.CGColor);
CGContextFillRect(context, outerRect);
CGContextRestoreGState(context);
CGContextRestoreGState(context);
UIGraphicsPopContext();
}

Animated pie chart in iOS

I'm trying to create an animated pie chart in iOS that acts basically like this:
In a nutshell, it starts as a grey circle, and as the animation progresses the arrow moves around the circle until it gets to the percentage I specify.
I've used the sample code that Zachary Waldowski posted in this SO question:
Animated CAShapeLayer Pie
That's gotten me to the point where I can create the basic animation. The maroon pie piece grows to hit the correct size. What I'm struggling with is how to map the arrow to the animation so that it gets dragged along as the pie piece grows.
Any thoughts on how I can accomplish this?
Okay, I've found the solution.
Building on the work Zachary Waldowski created (see my original post), I was able to do the entire thing in a CALayer.
In a nutshell, I draw one outer circle in maroon, a smaller circle in light gray, a stroked path in white, then draw the triangle for the tip of the arrow by hand.
Here's the relevant section of code that does the magic:
- (void)drawInContext:(CGContextRef)context {
CGRect circleRect = CGRectInset(self.bounds, 1, 1);
CGFloat startAngle = -M_PI / 2;
CGFloat endAngle = self.progress * 2 * M_PI + startAngle;
CGColorRef outerPieColor = [[UIColor colorWithRed: 137.0 / 255.0 green: 12.0 / 255.0 blue: 88.0 / 255.0 alpha: 1.0] CGColor];
CGColorRef innerPieColor = [[UIColor colorWithRed: 235.0 / 255.0 green: 214.0 / 255.0 blue: 227.0 / 255.0 alpha: 1.0] CGColor];
CGColorRef arrowColor = [[UIColor whiteColor] CGColor];
// Draw outer pie
CGFloat outerRadius = CGRectGetMidX(circleRect);
CGPoint center = CGPointMake(CGRectGetMidX(circleRect), CGRectGetMidY(circleRect));
CGContextSetFillColorWithColor(context, outerPieColor);
CGContextMoveToPoint(context, center.x, center.y);
CGContextAddArc(context, center.x, center.y, outerRadius, startAngle, endAngle, 0);
CGContextClosePath(context);
CGContextFillPath(context);
// Draw inner pie
CGFloat innerRadius = CGRectGetMidX(circleRect) * 0.45;
CGContextSetFillColorWithColor(context, innerPieColor);
CGContextMoveToPoint(context, center.x, center.y);
CGContextAddArc(context, center.x, center.y, innerRadius, startAngle, endAngle, 0);
CGContextClosePath(context);
CGContextFillPath(context);
// Draw the White Line
CGFloat lineRadius = CGRectGetMidX(circleRect) * 0.72;
CGFloat arrowWidth = 0.35;
CGContextSetStrokeColorWithColor(context, arrowColor);
CGContextSetFillColorWithColor(context, arrowColor);
CGMutablePathRef path = CGPathCreateMutable();
CGContextSetLineWidth(context, 16);
CGFloat lineEndAngle = ((endAngle - startAngle) >= arrowWidth) ? endAngle - arrowWidth : endAngle;
CGPathAddArc(path, NULL, center.x, center.y, lineRadius, startAngle, lineEndAngle, 0);
CGContextAddPath(context, path);
CGContextStrokePath(context);
// Draw the Triangle pointer
CGFloat arrowStartAngle = lineEndAngle - 0.01;
CGFloat arrowOuterRadius = CGRectGetMidX(circleRect) * 0.90;
CGFloat arrowInnerRadius = CGRectGetMidX(circleRect) * 0.54;
CGFloat arrowX = center.x + (arrowOuterRadius * cosf(arrowStartAngle));
CGFloat arrowY = center.y + (arrowOuterRadius * sinf(arrowStartAngle));
CGContextMoveToPoint (context, arrowX, arrowY); // top corner
arrowX = center.x + (arrowInnerRadius * cosf(arrowStartAngle));
arrowY = center.y + (arrowInnerRadius * sinf(arrowStartAngle));
CGContextAddLineToPoint(context, arrowX, arrowY); // bottom corner
arrowX = center.x + (lineRadius * cosf(endAngle));
arrowY = center.y + (lineRadius * sinf(endAngle));
CGContextAddLineToPoint(context, arrowX, arrowY); // point
CGContextClosePath(context);
CGContextFillPath(context);
[super drawInContext: context];
}
Have the pie with the arrow as an image.
In every step, draw this image, rotating it a little. Draw the gray pie over the image, choosing correct angles for the image rotation.

How can I make crisper images using quartz?

I'm trying to make high-resolution graphics dynamically. Unfortunately when I make arcs they look really fuzzy.
Here's my code
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
CGContextRef context = UIGraphicsGetCurrentContext();
CGRect bounds = [self bounds];
CGPoint center = CGPointMake(bounds.size.width / 2.0, bounds.size.height / 2.0);
CGFloat lineWidth = 30.0;
CGFloat innerRadius = (bounds.size.width / 2.0) - lineWidth;
CGFloat outerRadius = innerRadius + lineWidth;
CGFloat startAngle = -((float)M_PI / 2.0);
CGFloat endAngle = ((self.percent / 100.0) * 2 * (float)M_PI) + startAngle;
UIBezierPath *processBackgroundPath = [UIBezierPath bezierPath];
processBackgroundPath.lineWidth = lineWidth;
CGFloat radius = (self.bounds.size.width - lineWidth) / 2.0;
CGFloat fullAngle = (2.0 * (float)M_PI) + startAngle;
[processBackgroundPath addArcWithCenter:center radius:radius startAngle:startAngle endAngle:fullAngle clockwise:YES];
[[UIColor whiteColor] set];
[processBackgroundPath stroke];
CGMutablePathRef progressPath = CGPathCreateMutable();
CGPathMoveToPoint(progressPath, NULL, center.x, center.y - innerRadius);
CGPathAddArc(progressPath, NULL, center.x, center.y, innerRadius, startAngle, endAngle, YES);
CGPathAddArc(progressPath, NULL, center.x, center.y, outerRadius, endAngle, startAngle, NO);
CGPathCloseSubpath(progressPath);
UIColor *aColor = [UIColor colorWithRed:0.941 green:0.776 blue:0.216 alpha:1.0];
[aColor setFill];
CGContextAddPath(context, progressPath);
CGContextFillPath(context);
CGPathRelease(progressPath);
}
What do I need to do to make the donut more crisp?

Resources