I'd like to draw a circle without filling (only border of the circle) step by step (like animated timer). 1 spin is equal 1 day (24 hours). I really don't know what to do.
Steps I've made
1) I've tried https://github.com/danielamitay/DACircularProgress (it's too wide line of progress)
2) I've tried to draw a circle with many arcs.
Can you put me some code please. I really confused. Thanks in advance.
EDIT
I'd like to use NSTimer because I have a button which allow user to stop drawning a circle. If user touch a button again - drawning will have to continue.
What I would do is to create a path that is a circle and use that with a CAShapeLayer and animate the strokeEnd similar to what I did in this answer.
It would look something like this (but I didn't run this code so there may be typos and other mistakes):
UIBezierPath *circle = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:0
endAngle:2.0*M_PI
clockwise:YES];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.bounds = CGRectMake(0, 0, 2.0*radius, 2.0*radius);
circleLayer.path = circle.CGPath;
circleLayer.strokeColor = [UIColor orangeColor].CGColor;
circleLayer.lineWidth = 3.0; // your line width
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 10.0; // your duration
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = #0;
drawAnimation.toValue = #1;
Just note that both the path and the shape layer has a position so the circle path should be defined relative to the origin of the shape layers frame. It might be clearer to define the shape layer first and then create an oval inside of its bounds:
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.bounds = CGRectMake(0, 0, 2.0*radius, 2.0*radius);
circleLayer.position = center; // Set center of the circle
// Create a circle inside of the shape layers bounds
UIBezierPath *circle = [UIBezierPath bezierPathWithOvalInRect:circleLayer.bounds];
circleLayer.path = circle.CGPath;
// Same appearance configuration as before
circleLayer.strokeColor = [UIColor orangeColor].CGColor;
circleLayer.lineWidth = 3.0; // your line width
If DACircleProgressenter link description here otherwise works for you, it looks like you can easily set the line thickness.
As opposed to have a simple lineWidth type property, it seems the author of that library sets the thickness based on a ratio to the radius of the circle. This exists as the thicknessRatio property of that class. For example, if your radius is 40, then setting thicknessRatio to 0.025 should yield a line width of 1. That library seems simple and well thought out - consider using it, or learning from it.
The default is set to 0.3, so a circle with a radius of 40 would have a progress line thickness of 12. That's probably what you were seeing.
Good luck!
Related
I am using a UIBezierPath to draw a curved line. The curve works however, I am unable to curve the edges of the line.
If you look at the top/bottom end of the curve, you can see that the edges are flattened out, thats not what I want. Is there a way to curve the edges?
I am using a simple UIBezierPath to draw an (almost) semi circle. This creates the curved line shape that I want:
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(0, 0) radius:70 startAngle:1.0472 endAngle:5.23599 clockwise:NO].CGPath;
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor colorWithWhite:1.0 alpha:0.2].CGColor;
circle.lineWidth = 18.0;
circle.cornerRadius = 10.0;
circle.position = CGPointMake(100, 100);
circle.anchorPoint = CGPointMake(0.5, 0.5);
[mainView.layer addSublayer:circle];
Despite setting the cornerRadius property, the corners are not curved. What am I doing wrong?
Thanks for your time, Dan.
Include the following.
circle.lineCap = kCALineJoinRound;
For Swift 3.0 and above
circle.lineCapStyle = .Round;
Swift 4.0
circle.lineCap = kCALineCapRound
and if you want your lines intersections to also be rounded set
circle.lineJoin = kCALineCapRound
as well.
Is there a way to draw a UIView circle with a dotted line border? I want to have control over the spacing between the dots, and the size of the dots. I tried specifying my own pattern image, but when I make it into a circle it doesn't look good:
UIView *mainCircle = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
[mainCircle.layer setCornerRadius:100];
[mainCircle.layer setBorderWidth:5.0];
[mainCircle.layer setBorderColor:[[UIColor colorWithPatternImage:[UIImage imageNamed:#"dotted"]] CGColor]];
[self.view addSubview:mainCircle];
[mainCircle setCenter:self.view.center];
Following on from aksh1t's answer and rob's answer, you should use a round line cap, along with a dash pattern to do this.
The only thing I would add is that with the current code, you can end up with results like this:
Notice how at the top, you get an overlap of the dots. This is due to the fact that the circumference of the circle isn't entirely divisible by the number of dots.
You can fix this relatively easily by doing a simple bit of maths before. I wrote a few lines of code that'll allows you to provide an dot diameter value, along with an expected dot spacing - and it will try and approximate the nearest dot spacing that will result in an integral number of dots.
Also, I recommend you take an 100% layered approach, using CAShapeLayer to draw your circle. That way you can easily add animations to it without having to completely re-draw it for each frame.
Something like this should do the trick:
// your dot diameter.
CGFloat dotDiameter = 10.0;
// your 'expected' dot spacing. we'll try to get as closer value to this as possible.
CGFloat expDotSpacing = 20.0;
// the size of your view
CGSize s = self.view.frame.size;
// the radius of your circle, half the width or height (whichever is smaller) with the dot radius subtracted to account for stroking
CGFloat radius = (s.width < s.height) ? s.width*0.5-dotDiameter*0.5 : s.height*0.5-dotDiameter*0.5;
// the circumference of your circle
CGFloat circum = M_PI*radius*2.0;
// the number of dots to draw as given by the circumference divided by the diameter of the dot plus the expected dot spacing.
NSUInteger numberOfDots = round(circum/(dotDiameter+expDotSpacing));
// the calculated dot spacing, as given by the circumference divided by the number of dots, minus the dot diameter.
CGFloat dotSpacing = (circum/numberOfDots)-dotDiameter;
// your shape layer
CAShapeLayer* l = [CAShapeLayer layer];
l.frame = (CGRect){0, 0, s.width, s.height};
// set to the diameter of each dot
l.lineWidth = dotDiameter;
// your stroke color
l.strokeColor = [UIColor blackColor].CGColor;
// the circle path - given the center of the layer as the center and starting at the top of the arc.
UIBezierPath* p = [UIBezierPath bezierPathWithArcCenter:(CGPoint){s.width*0.5, s.height*0.5} radius:radius startAngle:-M_PI*0.5 endAngle:M_PI*1.5 clockwise:YES];
l.path = p.CGPath;
// prevent that layer from filling the area that the path occupies
l.fillColor = [UIColor clearColor].CGColor;
// round shape for your stroke
l.lineCap = kCALineCapRound;
// 0 length for the filled segment (radius calculated from the line width), dot diameter plus the dot spacing for the un-filled section
l.lineDashPattern = #[#(0), #(dotSpacing+dotDiameter)];
[self.view.layer addSublayer:l];
You'll now get the following output:
If you want to use this in a UIView, I would suggest subclassing it and adding the CAShapeLayer as a sublayer. You'll also want to add a masking layer in order to mask the view's contents to inside the border.
I have added an example of this in the full project below.
Full Project: https://github.com/hamishknight/Dotted-Circle-View
The best way to do what you are trying would be to draw a circle UIBezierPath, and set the path to a dotted style. The dotted style path code was taken from this answer.
UIBezierPath * path = [[UIBezierPath alloc] init];
[path addArcWithCenter:center radius:50 startAngle:0 endAngle:2 * M_PI clockwise:YES];
[path setLineWidth:8.0];
CGFloat dashes[] = { path.lineWidth, path.lineWidth * 2 };
[path setLineDash:dashes count:2 phase:0];
[path setLineCapStyle:kCGLineCapRound];
// After you have the path itself, you can either make
// an image and set it in a view or use the path directly
// in the layer of the view you want to.
// This is the code for the image option.
UIGraphicsBeginImageContextWithOptions(CGSizeMake(300, 20), false, 2);
[path stroke];
UIImage * image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
I'm using the following code to draw part of a circle. I'm a bit confused, however, because at the end of the animation it seems to immediately fill in the rest of the circle. Why is this happening?
// Set up the shape of the circle
int radius = 62;
CAShapeLayer *circle = [CAShapeLayer layer];
// Make a circular shape
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Center the shape in self.view
circle.position = CGPointMake(18, 92);
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor whiteColor].CGColor;
circle.lineWidth = 5;
// Add to parent layer
[cell.layer addSublayer:circle];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = .8; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
// Animate from no part of the stroke being drawn to the entire stroke being drawn
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:0.4f];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
This is happening because by default a CAAnimation is removed from the layer when it completes. This means that it will remove the value that you animated to and set it to the default of 1.0 right after the animation finished. Since you are using a CAAnimation the layer is never actually changing its properties it just only looks like it is which is why when the animation is removed it gets set to 1.0 because that is what the layer's value for strokeEnd is since you never changed it. Try to see this yourself by printing the value of strokeEnd while the animation is happening.
This can be solved in 2 ways, setting the final storkeEnd value before you start the animation so that when it is removed it is still 0.4 or by adding certain properties to your CABasicAnimation. The properties you need to set to achieve what you are looking for are:
drawAnimation.fillMode = kCAFillModeForwards
and
drawAnimation.removedOnCompletion = false
Hope that helps
For the whole stroke to be animated drawAnimation.fromValue should be 0.0 and drawAnimation.toValue should be 1.0. These values determine what percentage of the stroke is animated.
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
fromValue : Defines the value the receiver uses to start interpolation.
toValue : Defines the value the receiver uses to end interpolation.
What you used was 0.4 for drawAnimation.toValue. So it ends at 40% of the animation and draws the rest in one go without animating.
For example if you set drawAnimation.fromValue = 0.5 and drawAnimation.toValue = 1.0,
animation will start from half a circle.
if you set drawAnimation.fromValue = 0.0 and drawAnimation.toValue = 0.5,
animation will end at half a circle and draw the rest without animation.
If you only wanted to draw 40% of the circle, you can use a different function to create the circle path.
CGFloat startAngle = (-M_PI/2);
CGFloat endAngle = startAngle + 0.4*(2*M_PI);
circle.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius)
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES].CGPath;
The above code actually generates 40% of a circle path from startAngle = -M_PI/2. You can vary the angle according to your needs.
I have used both UIBezierPath and CAShapeLayer before. But almost every time in conjunction with filling the object contained within the path with color inside. But I would like this time to fill the color outside of the object contained by the UIBezierPath.
I just wrote and ran the following simple code trying to get myself acquainted with the fillRule property:
CAShapeLayer *myLayer = (CAShapeLayer *) self.layer; //size: 320 X 480
UIBezierPath *testPath = [UIBezierPath bezierPathWithOvalInRect:(CGRect){{100, 100}, 100, 100}]; //a simple circle
myLayer.fillRule = kCAFillRuleNonZero; // have tried this as well: kCAFillRuleEvenOdd;
myLayer.path = testPath.CGPath;
myLayer.fillColor = [UIColor whiteColor].CGColor;
But the color is filled nonetheless inside. What I would like to find out is, how can I fill the color outside of the path? If I am using fillRule wrong here, I would like to know if there is other methods that can achieve this. Thanks in advance.
The main problem is that you can't really fill the outside of a shape, since there's no generic way to define what that means. What you need to do is first plot a path around the "outside" of your shape, and then add the circle as a subpath. How you do that depends on which fill rule you want to use. EvenOdd is the easiest:
CAShapeLayer *myLayer = (CAShapeLayer *) self.layer;
UIBezierPath *testPath = [UIBezierPath bezierPathWithRect:self.bounds];
[testPath appendPath:[UIBezierPath bezierPathWithOvalInRect:(CGRect){{100, 100}, 100, 100}]];
myLayer.fillRule = kCAFillRuleEvenOdd;
myLayer.path = testPath.CGPath;
myLayer.fillColor = [UIColor whiteColor].CGColor;
NonZero is a little bit harder because you have to force the path to be counter-clockwise which isn't an option for most of the UIBezierPath convenience methods:
CAShapeLayer *myLayer = (CAShapeLayer *) self.layer;
UIBezierPath *testPath = [UIBezierPath bezierPathWithRect:self.bounds];
UIBezierPath *counterClockwise = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:100 startAngle:0 endAngle:M_PI clockwise:NO];
[counterClockwise appendPath:[UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 100) radius:100 startAngle:M_PI endAngle:0 clockwise:NO]];
[testPath appendPath:counterClockwise];
myLayer.fillRule = kCAFillRuleNonZero;
myLayer.path = testPath.CGPath;
myLayer.fillColor = [UIColor redColor].CGColor;
Depending on how you're constructing your actual path it may not make a difference either way.
If you haven't seen it already, the winding rules documentation has some nice diagrams that I find helpful.
I'm trying to make a donut shape with CALayers. One CALayer will be a large circle, the other one will be a smaller circle positioned in its center, masking it.
The large circle displays fine, but whenever I call circle.mask = circleMask; then the view appears empty.
Here's my code:
AriDonut.h
#import <UIKit/UIKit.h>
#interface AriDonut : UIView
-(id)initWithRadius:(float)radius;
#end
AriDonut.m
#import "AriDonut.h"
#import <QuartzCore/QuartzCore.h>
#implementation AriDonut
-(id)initWithRadius:(float)radius{
self = [super initWithFrame:CGRectMake(0, 0, radius, radius)];
if(self){
//LARGE CIRCLE
CALayer *circle = [CALayer layer];
circle.bounds = CGRectMake(0, 0, radius, radius);
circle.backgroundColor = [UIColor redColor].CGColor;
circle.cornerRadius = radius/2;
circle.position = CGPointMake(radius/2, radius/2);
//SMALL CIRLCE
CALayer *circleMask = [CALayer layer];
circleMask.bounds = CGRectMake(0, 0, 10, 10);
circleMask.cornerRadius = radius/2;
circleMask.position = circle.position;
//circle.mask = circleMask;
[self.layer addSublayer:circle];
}
return self;
}
I've tried setting the large circle's superlayer nil like this:
CALayer *theSuper = circle.superlayer;
theSuper = nil;
But it didin't make a difference.
I also tried setting Circle's masksToBounds property to YES and NO, but it didn't make a difference.
Any thoughts?
Indeed, as #David indicates the current (iOS 5.1) CALayer masks can't be reversed, which poses a problem if you want to use them to make a transparent hole a simple circular CALayer.
What you can do to get a donut is make a circular CALayer's backgroundColor transparent, but give it a borderColor and a wide borderWidth. Here's the dunkin' code:
CALayer *theDonut = [CALayer layer];
theDonut.bounds = CGRectMake(0,0, radius, radius);
theDonut.cornerRadius = radius/2;
theDonut.backgroundColor = [UIColor clearColor].CGColor;
theDonut.borderWidth = radius/5;
theDonut.borderColor = [UIColor orangeColor].CGColor;
[self.layer addSublayer:theDonut];
This is pretty easy using UIBezierPath and a CAShapeLayer as a masking layer. Code sample written as though it's in a UIView subclass.
Objective-C:
CGRect outerRect = self.bounds;
CGFloat inset = 0.2 * outerRect.size.width; // adjust as necessary for more or less meaty donuts
CGFloat innerDiameter = outerRect.size.width - 2.0 * inset;
CGRect innerRect = CGRectMake(inset, inset, innerDiameter, innerDiameter);
UIBezierPath *outerCircle = [UIBezierPath bezierPathWithRoundedRect:outerRect cornerRadius:outerRect.size.width * 0.5];
UIBezierPath *innerCircle = [UIBezierPath bezierPathWithRoundedRect:innerRect cornerRadius:innerRect.size.width * 0.5];
[outerCircle appendPath:innerCircle];
CAShapeLayer *maskLayer = [CAShapeLayer new];
maskLayer.fillRule = kCAFillRuleEvenOdd; // Going from the outside of the layer, each time a path is crossed, add one. Each time the count is odd, we are "inside" the path.
maskLayer.path = outerCircle.CGPath;
self.layer.mask = maskLayer;
Swift:
let outerRect = self.bounds
let inset: CGFloat = 0.2 * outerRect.width // adjust as necessary for more or less meaty donuts
let innerDiameter = outerRect.width - 2.0 * inset
let innerRect = CGRect(x: inset, y: inset, width: innerDiameter, height: innerDiameter)
let outerCircle = UIBezierPath(roundedRect: outerRect, cornerRadius: outerRect.width * 0.5)
let innerCircle = UIBezierPath(roundedRect: innerRect, cornerRadius: innerRect.width * 0.5)
outerCircle.appendPath(innerCircle)
let mask = CAShapeLayer()
mask.fillRule = kCAFillRuleEvenOdd
mask.path = outerCircle.CGPath
self.layer.mask = mask
It is the alpha value of the masking layers content that is used as a mask. (If you would add the mask as a sublayer instead of using it as a mask. Everything that is covered by the sublayer would be visible when used as a mask. Everything that is not covered by the sublayer would be hidden when used as a mask.)
Since your small circle is fully transparent , everything is masked away (is hidden). If you set the backgroundColor of it to any, fully opaque color (only the alpha value is used for the mask) then it will let those pixels through.
Note that this is the reverse of what you want. This will leave you with only "the hole of the donut" visible. There is no built in way to do a reverse mask Instead you would have to draw the content of the mask some other way like using a CAShapeLayer or using drawInContext:.
I succeeded with a CAShapeLayer masking a CALayer. To specify the shape of the masking CAShapeLayer I used UIBezierPath.
I posted the code in my answer to this question: How to Get the reverse path of a UIBezierPath. For the donut shape uncomment the commented line.