Apply rounded corners to arc created with UIBezierPath - objective-c

I am working on a circular progress bar that i've created using UIBezierPath. The progress bar looks like the following picture:
My question is: how to make the edge of the arc rounded and not rectangular?
The code i used to draw the arc looks like is:
// Draw the arc with bezier path
int radius = 100;
arc = [CAShapeLayer layer];
arc.path = [UIBezierPath bezierPathWithArcCenter:CGPointMake(100, 50) radius:radius startAngle:M_PI endAngle:M_PI/150 clockwise:YES].CGPath;
arc.position = CGPointMake(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
arc.fillColor = [UIColor clearColor].CGColor;
arc.strokeColor = [UIColor purpleColor].CGColor;
arc.cornerRadius = 0.5;
arc.lineWidth = 6;
[self.view.layer addSublayer:arc];
// Animation of the progress bar
drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = YES; // Remain stroked after the animation..
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:10.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[arc addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
I tried to use arc.cornerRadius but it seemed to return nothing.
Any help would be appreciated.
Thank you in advance!
Granit

Set lineCapStyle to kCGLineCapRound (On the bezier path) to draw the ends of the lines with a round edge.
You can also set the lineCap on the shape layer to do the same thing.

To make corner as round you can use this.
arc.lineCap = #"round";

The swift equivalent is:
let circlePath = UIBezierPath(…)
circlePath.lineCapStyle = .round

Related

Antialiasing on CAShapeLayers

I have a custom view for a loading indicator which is comprised of a number of CAShapeLayers which I perform various animations on to represent various states. In the init method I create a number of shape layers and add them as sublayers to the view's layer. The issue I'm having is the shapes that I'm drawing have very rough edges and don't seem to be antialiased. I've tried a number of things from similar answers on here but I can't seem to antialias the shapes. I've never had this problem before then drawing directly in drawRect.
Is there anything I'm missing or is there a better way to accomplish what I'm trying to do?
Update: Here's a comparison of how the shapes are being drawn:
On the left I'm drawing in layoutSubviews using this code:
CGFloat lineWidth = 2.5f;
CGFloat radius = (self.bounds.size.width - lineWidth)/2;
CGPoint center = CGPointMake(CGRectGetWidth(self.frame)/2, CGRectGetHeight(self.frame)/2);
CGFloat startAngle = - ((float)M_PI / 2);
CGFloat endAngle = (2 * (float)M_PI) + startAngle;
UIBezierPath *trackPath = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:startAngle
endAngle:endAngle
clockwise:YES];
self.trackLayer.path = trackPath.CGPath;
self.trackLayer.lineWidth = lineWidth;
On the right I'm calling a separate method from awakeFromNib:
-(void)awakeFromNib
{
....
self.trackLayer = [self trackShape]
[self.layer addSublayer:self.trackLayer];
}
-(CAShapeLayer*)trackShape
{
float start_angle = 2*M_PI*-M_PI_2;
float end_angle = 2*M_PI*1-M_PI_2;
float minSize = MIN(self.frame.size.width, self.frame.size.height);
float radius = (minSize-_strokeWidth)/2 -4;
CGPoint center = CGPointMake(self.frame.size.width/2,self.frame.size.height/2);
CAShapeLayer *circle = [CAShapeLayer layer];
circle.path = [UIBezierPath bezierPathWithArcCenter:center
radius:radius
startAngle:start_angle
endAngle:end_angle
clockwise:YES].CGPath;
circle.strokeColor = self.trackColor.CGColor;
circle.fillColor = nil;
circle.lineWidth = (_strokeWidth == -1.0) ? minSize * _strokeWidthRatio
: _strokeWidth;
circle.rasterizationScale = [[UIScreen mainScreen] scale];
return circle;
}
The picture you provided is exactly what happened to me. I got the chunky circle. In my case, the code was inside the drawRect method. The problem was that I was adding a CAShapeLayer, as a subLayer of my view, every time drawRect was called.
So to anyone who stumbles upon something similar, keep track of the CAShapeLayer you are adding or clear everything before drawing again in the drawRect.

Incorrect work of SKPhysicsBody with moved SKSpriteNode anchorPoint?

Here is my code:
// setting background
_ground = [[SKSpriteNode alloc]
initWithColor:[SKColor greenColor]
size:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.name = #"ground";
_ground.zPosition = 1;
_ground.anchorPoint = CGPointMake(0, 0);
_ground.position = CGPointMake(0, 0);
_ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.physicsBody.dynamic = NO;
_ground.physicsBody.affectedByGravity = NO;
_ground.physicsBody.categoryBitMask = groundCategory;
_ground.physicsBody.collisionBitMask = elementCategory;
_ground.physicsBody.contactTestBitMask = elementCategory;
Green rectangle positioning is ok, but SKPhysicsBody is not representing this rectangle correctly. It looks like SKPhysicsBody is not "moving" its body according to sprite position and anchorPoint. SKPhysicsBody is moved left for _ground.width points.
What am I doing wrong?
PS.
Changing to this (code) solved my problem, but I really dont like this solution, it seems an ugly way to position an element.
_ground.position = CGPointMake([Helpers landscapeScreenSize].width / 2, 0);
+removing:
_ground.position = CGPointMake(0, 0);
I had exactly the same issue yesterday ;)
For me I solved this by using the default sprite positioning by SpriteKit. I just set the position and leave the anchor point at it's default. Then you set the physicsbody to the size of the sprite.
Change your code to the following and check if it works:
_ground = [[SKSpriteNode alloc]
initWithColor:[SKColor greenColor]
size:CGSizeMake([Helpers landscapeScreenSize].width, 50)];
_ground.position = CGPointMake([Helpers landscapeScreenSize].width / 2, 0);
_ground.physicsBody = [SKPhysicsBody bodyWithRectangleOfSize:_ground.size];
If you still have no luck with this you can also try to use another initialisation for your sprite (this works in my project). Try it with a texture:
[[SKSpriteNode alloc] initWithTexture:[SKTexture textureWithCGImage:[UIImage imageWithColor:[UIColor whiteColor]].CGImage] color:[UIColor whiteColor] size:CGSizeMake(PLAYER_WIDTH, PLAYER_HEIGHT)];
don't forget to implement the (UIImage*)imageWithColor: method to create a 1x1 pixel image of your desired color for you (see here).

Circular-slider weird behaviour while animating with CA

I wanted to make a circular Slider which is draggable and animatable. So far I've managed to build the slider and use the drag handle to move it around and even animate it. Sometimes animation goes wrong (wrong direction or shortest direction. I've subclassed a UIView (Will be a UIControl soon, just wanted to get the animation right first) added a PanGestureRecognizer and several layers for the drawing.
So how do I fix this weird behaviour? I've someone could help me here, I'd be thankful :)
Here's the sample project -> http://cl.ly/2l0O3b1I3U0X
Thanks a lot!
EDIT:
Here's the drawing code:
CALayer *aLayer = [CALayer layer];
aLayer.bounds = CGRectMake(0, 0, 170, 170);
aLayer.position = self.center;
aLayer.transform = CATransform3DMakeRotation(M_PI_4, 0, 0, 1);
self.handleHostLayer = [CALayer layer];
self.handleHostLayer.bounds = CGRectMake(0, 0, 170, 170);
self.handleHostLayer.position = CGPointMake(CGRectGetMaxX(aLayer.bounds) - 170/2.0, CGRectGetMaxY(aLayer.bounds) - 170/2.0);
[aLayer addSublayer:self.handleHostLayer];
[self.layer addSublayer:aLayer];
self.handle = [CALayer layer];
self.handle.bounds = CGRectMake(0, 0, 50, 50);
self.handle.cornerRadius = 25;
self.handle.backgroundColor = [UIColor whiteColor].CGColor;
self.handle.masksToBounds = NO;
self.handle.shadowOffset = CGSizeMake(3.0, 0.0);
self.handle.shadowRadius = 0;
self.handle.shadowOpacity = .15;
self.handle.shadowColor = [UIColor blackColor].CGColor;
[self.handleHostLayer addSublayer:self.self.handle];
Here's the animation code:
CGFloat handleTarget = ToRad(DEG);
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotationAnimation.fromValue = #([[self.handleHostLayer valueForKeyPath:#"transform.rotation"] floatValue]);
rotationAnimation.toValue = #(handleTarget);
rotationAnimation.duration = .5;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.cumulative = YES;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[self.handleHostLayer addAnimation:rotationAnimation forKey:#"transform.rotation"];
OK, I looked at your project. Your problem is that the to and from angles don't both fall in the 0≤ɸ<2π range.
You can make sure that they do by adding and removing 2π until they both are within that range.
CGFloat fromAngle = [[self.handleHostLayer valueForKeyPath:#"transform.rotation"] floatValue];
CGFloat toAngle = handleTarget;
while (fromAngle >= 2.0*M_PI) { fromAngle -= 2*M_PI; }
while (toAngle >= 2.0*M_PI) { toAngle -= 2*M_PI; }
while (fromAngle < 0.0) { fromAngle += 2*M_PI; }
while (toAngle < 0.0) { toAngle += 2*M_PI; }
CABasicAnimation *rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
rotationAnimation.fromValue = #(fromAngle);
rotationAnimation.toValue = #(toAngle);
// As before...
Another things you could change is the misuse of removeOnCompletion. I did a long explanation in this answer about why it's bad to do so.
In short: you are not animating from the value that you think you are animating since you inadvertently introduce a difference between the value of the layers property and what you see on screen.
Skip that line all together. You can also skip the skip the cumulative line since you are not repeating the animation. Now, if your animations doesn't stick: set the model value to its final value before adding the animation to it.

How do I further animate an image that's on a bezier path

Maybe someone can point me in the right direction. I tried Google but dont really know how to define my question in searchable terms.
I am animating an image along a UIBezierPath like this:
UIBezierPath *trackPath = [UIBezierPath bezierPath];
[trackPath moveToPoint:P(8, 250)];
[trackPath addCurveToPoint:P(318, 250)
controlPoint1:P(156, 86)
controlPoint2:P(166, 412)];
[trackPath addCurveToPoint:P(652, 254)
controlPoint1:P(488, 86)
controlPoint2:P(512, 412)];
[trackPath addCurveToPoint:P(992, 254)
controlPoint1:P(818, 96)
controlPoint2:P(872, 412)];
CALayer *bee = [CALayer layer];
bee.bounds = CGRectMake(0, 0, 69, 71);
bee.position = P(160, 25);
bee.contents = (id)([UIImage imageNamed:#"bee3.png"].CGImage);
[self.view.layer addSublayer:bee];
What I want to do is animate the image itself as it moving along the path. I can animate the image when its b itself with like this:
NSArray *blueArray = [NSArray array];
blueArray = [[NSArray alloc] initWithObjects:
[UIImage imageNamed:#"blue1.png"],
[UIImage imageNamed:#"blue2.png"], nil];
blueFly.animationImages = blueArray;
blueFly.animationDuration = 0.20;
blueFly.animationRepeatCount = -1;
[blueFly startAnimating];
How can I combine the two, so to speak?
Additionally I know how to draw the path itself onto the screen like this:
CAShapeLayer *beeline = [CAShapeLayer layer];
beeline.path = trackPath.CGPath;
beeline.strokeColor = [UIColor whiteColor].CGColor;
beeline.fillColor = [UIColor clearColor].CGColor;
beeline.lineWidth = 2.0;
beeline.lineDashPattern = [NSArray arrayWithObjects:[NSNumber numberWithInt:6], [NSNumber numberWithInt:2], nil];
[self.view.layer addSublayer:beeline];
But how can I draw the line only behind the image as its being animated along the path?
You can use a CAShapeLayer to display the path. A shape layer provides two properties strokeStart and strokeEnd that have values between 0 and 1 and determine the fraction of the path that is visible. By animating strokeEnd you can create the impression that the path is "drawn" on the screen. See http://oleb.net/blog/2010/12/animating-drawing-of-cgpath-with-cashapelayer for an example that animates a pen that draws a line.
In order to change the image of the bee you can simply add an animation that changes the contents of your bee layer. For example, to change the image of the pen in the CAShapeLayer project above, we can add the following code to the startAnimation method.
CAKeyframeAnimation *imageAnimation = [CAKeyframeAnimation animationWithKeyPath:#"contents"];
imageAnimation.duration = 1.0;
imageAnimation.repeatCount = HUGE_VALF;
imageAnimation.calculationMode = kCAAnimationDiscrete;
imageAnimation.values = [NSArray arrayWithObjects:
(id)[UIImage imageNamed:#"noun_project_347_1.png"].CGImage,
(id)[UIImage imageNamed:#"noun_project_347_2.png"].CGImage,
nil];
[self.penLayer addAnimation:imageAnimation forKey:#"contents"];
This simply adds a key animation to the contents property of the layer that displays the pen. Note that the image "noun_project_347_1.png" is not part of the project so you would have to add an image to see the effect. I have simply horizontally flipped the image that comes with the project. By setting calculationMode to kCAAnimationDiscrete the animation does not interpolate between two images by replaces one by the other.
As long as you can get the exact path that you want to animate the image align you can do it using Core Animation and CAKeyframeAnimation.
Refer how-to-animate-one-image-over-the-unusual-curve-path-in-my-ipad-application) link.

Core Animation: Make content follow shadow path

I have an animated shadow path of a CALayer:
CABasicAnimation* shadowAnimation = [CABasicAnimation animationWithKeyPath: #"shadowPath"];
shadowAnimation.duration = duration;
shadowAnimation.timingFunction = function;
shadowAnimation.fromValue = (id) panelView.layer.shadowPath;
panelView.layer.shadowPath = [UIBezierPath bezierPathWithRoundedRect: CGRectSetOriginM20 (panelView.bounds, CGPointZero) cornerRadius: cornerRadius].CGPath;
[panelView.layer addAnimation: shadowAnimation forKey: #"shadow"];
panelView.layer.shadowOpacity = 1.0;
I need to recreate the same effect using an asset by settings the CALayer's content property to a UIImage. Is there a way to make the bounds follow the same animation as the shadow?
No, you can't make the content follow the shadow path.
Instead you have to animate the shadow path and the bounds together. This can be done in an animation group so that you only can configure the timing function and duration on the group.
CAAnimationGroup* shadowAndBounds = [CAAnimationGroup animation];
shadowAndBounds.duration = duration;
shadowAndBounds.timingFunction = function;
CABasicAnimation* shadowAnimation = [CABasicAnimation animationWithKeyPath: #"shadowPath"];
// Set to and from value for the shadow path ...
CABasicAnimation* boundsAnimation = [CABasicAnimation animationWithKeyPath: #"bounds"];
// Set to and from value for the bounds ...
shadowAndBounds.animations = #[ shadowAnimation, boundsAnimation ];
[panelView.layer addAnimation: shadowAndBounds forKey: #"shadowAndBounds"];