I have an image that I am attempting to mask a circle around so the image appears round. This somewhat works but the circle comes to a point on the top and bottom.
profileImageView.layer.cornerRadius = profileImageView.frame.size.width/2;
profileImageView.layer.masksToBounds = YES;
Should this code be drawing a perfect circle? It seems to draw a circle in one place but in two other places, its not working correctly.
I have had the best results masking the image view with a CAShapeLayer:
CGFloat radius = self.profileImageView.frame.size.width / 2.0;
UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius startAngle:0 endAngle:M_PI * 2.0 clockwise:TRUE];
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
layer.lineWidth = 0;
self.profileImageView.layer.mask = layer;
Should this code be drawing a perfect circle?
Not necessarily. After all, the width and the height of this layer might not be the same. And even if they are, dividing by 2 might not give you a radius that fits perfectly into an integral number of points as they are mapped to pixels on the screen.
It really would be better, if what you want is a mask that's a circle, to give this layer an actual mask that is an actual circle. Misusing the corner radius as you are doing is just lazy (and, as you've discovered, it's error-prone).
Related
Premise: I am rounding the edges of a view with a solid background (to make something like an oval). There are many ways to do this, but the two most common I've see recommended are:
1) set the cornerRadius property on the view layer and bind the mask layer to the view layer bounds (through clipsToBounds or masksToBounds) (https://developer.apple.com/reference/quartzcore/calayer/1410818-cornerradius)
ex:
view.layer.cornerRadius = 2
view.layer.maskToBounds = true
2) create a Bezier Path with [bezierPathWithRoundedRect:byRoundingCorners:cornerRadii] and use it to create a mask layer which you set to the view's layer's mask. (https://developer.apple.com/reference/uikit/uibezierpath/1624368-bezierpathwithroundedrect)
ex:
UIBezierPath* bezierPath = [UIBezierPath bezierPathWithRoundedRect: view.bounds
byRoundingCorners:UIRectCornerAllCorners
cornerRadii: CGSizeMake(2, 2)];
CAShapeLayer* maskLayer = [CAShapeLayer layer];
maskLayer.frame = view.bounds;
maskLayer.path = bezierPath.CGPath;
view.layer.mask = maskLayer;
Question: Both ways work well for me and I see no significant performance of one over the other (admittedly, I'm not using it heavily). In which situations should I use a Bezier path over setting the corner radius (assuming the corner radii are equal)? Do these two methods to creating rounded edges do the same thing behind the scenes? Are there certain platforms that won't support both solutions?
Best Regards
Currently am working on a circular chart,There is no issue on drawing full circle. I also need to draw a 3/4 circle, Is there any specify math to define 3/4 circle path. I tried by reducing missed position values with full circle but cant get accurate result. Could anyone help with this? If any need I
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width/2.0f, self.frame.size.height/2.0f)
radius:(self.frame.size.height * 0.5) - ([_lineWidth floatValue]/2.0f)
startAngle:DEGREES_TO_RADIANS(startAngle)
endAngle:DEGREES_TO_RADIANS(endAngle)
clockwise:clockwise];
circle = [CAShapeLayer layer];
circle.path = circlePath.CGPath;
circle.lineCap = kCALineCapRound;
circle.fillColor = [UIColor clearColor].CGColor;
circle.lineWidth = [_lineWidth floatValue];
circle.zPosition = 1; [self.layer addSublayer:circle];
;
Please refer image below,
Note: I have attached the sample image from web but am sure i need the same results.
You already have the code that you need. The function bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise: takes a start angle and an end angle. If you pass in a start angle of 0 and an end angle of 3π/2 you'll get a 3/4 circle. (3π/2 is 270 degrees, or 3/4 of a full circle.)
Note that if your goal is to animate a circle from 0 to 360 degrees then you need to use a different technique. For that you want to create a path of the full circle, install that into a CAShapeLayer, then animate the layer's strokeEnd property from 0 to 1.
I've got a project where I'm animating a UIBezierPath based on a set progress. The BezierPath is in the shape of a circle and lies in a UIView and animation is done in drawRect using CADisplayLink right now. Simply put, based on a set progress x the path should radially extend (if xis larger than before) or shrink (if x is smaller).
self.drawProgress = (self.displayLink.timestamp - self.startTime)/DURATION;
CGFloat startAngle = -(float)M_PI_2;
CGFloat stopAngle = ((self.x * 2*(float)M_PI) + startAngle);
CGFloat currentEndAngle = ((self.oldX * 2*(float)M_PI) + startAngle);
CGFloat endAngle = currentEndAngle-((currentEndAngle-stopAngle)*drawProgress);
UIBezierPath *guideCirclePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:endAngle clockwise:YES];
This is in the case of x shrinking since our last update. The issues I'm experiencing are actually a few:
The shape always starts drawing at 45º (unless I rotate the view). I have not found any way to change this, and setting the startAngleto -45º makes no difference really because it always "pops" to 45. Is there anything I can do about this, or do I have to resort to other methods of drawing?
Is there any other way that one should animate these things? I've read much about using CAShapeLayer but I haven't quite understood the actual difference (in terms of drawbacks and benefits) in using these two methods. If anyone could clarify I would be very much obliged!
UPDATE: I migrated the code over to CAShapeLayer instead, but now I'm facing a different issue. It's best described with this image:
What's happening is that when the layer is supposed to shrink, the thin outer line is still there (regardless of direction of movement). And when the bar shrinks, the delta of 1-xisn't removed unless I explicitly make a new white shape over it. The code for this follows. Any ideas?
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:center radius:radius startAngle:startAngle endAngle:stopAngle clockwise:YES];
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [circlePath CGPath];
circle.strokeStart = 0;
circle.strokeEnd = 1.0*self.progress;
// Colour and other customizations here.
if (self.progress > self.oldProgress) {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(circle.strokeEnd);
} else {
drawAnimation.fromValue = #(1.0*self.oldProgress);
drawAnimation.toValue = #(1.0*self.progress);
circle.strokeEnd = 1.0*self.progress;
}
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; //kCAMediaTimingFunctionEaseIn
[circle addAnimation:drawAnimation forKey:#"strokeEnd"];
UPDATE 2: I've ironed out most of the other bugs. Turned out it was just me being rather silly the whole time and overcomplicating the whole animation (not to mention multiplying by 1 everywhere, what?). I've made a gif of the bug I can't solve:
Any ideas?
UPDATE 3: (and closure). I managed to get rid of the bug by calling
[self.layer.sublayers makeObjectsPerformSelector:#selector(removeFromSuperlayer)];
And now everything works as it should. Thanks for all the help!
Using CAShapeLayer is much easier and cleaner. The reason is that CAShapeLayer includes properties strokeStart and strokeEnd. These values range from 0 (the beginning of the path) to 1 (the end of the path) and are animatable.
By changing them you can easily draw any arc of your circle (or any part of an arbitrary path, for that matter.) The properties are animatable, so you can create an animation of a growing/shrinking pie slice or section of a ring shape. It's much easier and more performant than implementing code in drawRect.
The doucumet said that the CAShapLayer is anti-aliased ,why got this result.
I also draw a circle with the CGContext in the drawRect method,It's very perfect.
UIBezierPath *path = [UIBezierPath bezierPath];
[path appendPath:[UIBezierPath bezierPathWithOvalInRect:CGRectMake(150, 300, 136, 136)]];
self.circleLayer = [CAShapeLayer layer];
self.circleLayer.path = path.CGPath;
self.circleLayer.frame = self.bounds;
self.circleLayer.fillColor = [[UIColor whiteColor] colorWithAlphaComponent:0.6].CGColor;
[self.layer addSublayer:self.circleLayer];
According to the UIBezierPath docs, bezierPathWithOvalInRect: "creates a closed subpath that approximates the oval using a sequence of Bézier curves," so it may not draw a perfect circle.
If you use a UIBezierPath to draw a circle will not a perfect circle because its an approximation from Bézier curves.
You can draw a perfect circle from a square UIView. For example:
myView.layer.cornerRadius = myView.frame.size.width/2;
In this topic explain three methods to draw a circle -> How to draw a smooth circle with CAShapeLayer and UIBezierPath?
iOS 12:
I had the same issue on iPhone XS and XS Max. I don't know why, but on iPhone 8 there was no issue. The circle was perfectly round.
To solve the issue on XS and XS Max I set the flatness property of UIBezierPath to 0.0 and made sure that the center of the rect used for UIBezierPath is either an integer number or a floating number of e.g. 25.5. The center of my rect was y=51.166666 and this caused the issue that the oval was not round.
Edit: Language updated to improve readability.
I made an image view with 2 rounded corners like this:
UIBezierPath *maskPath = [UIBezierPath bezierPathWithRoundedRect:self.photoImageView.bounds byRoundingCorners:UIRectCornerBottomLeft|UIRectCornerBottomRight cornerRadii:CGSizeMake(10, 10)];
CAShapeLayer *maskLayer = [CAShapeLayer layer];
maskLayer.path = maskPath.CGPath;
self.photoImageView.layer.mask = maskLayer;
But it is slower than making all the corners round using this code.
self.photoImageView.layer.cornerRadius = 10;
Would anyone know what why and how I can improve my '2 corner' code please?
Your code is adding another stage to the drawing. Normally, the background is drawn (with the given cornerRadius) directly to the target, but with a mask specified, it is drawn to a temporary surface, then copied to the target using the mask.
There isn't any built-in functionality for only rounding some background corners in the standard CALayer object.
I do wonder how slow "slower" really is; is this premature optimisation?