I am working on a circular progress view. I am adding slices or ARCs of a circle with unselected and selected colours that indicate progress of some task.
I am working in CALayers to draw Arcs and add animations. Everything is working fine for all slices with out gradient. If I add gradient in any slice / Arc it does not animate while drawing immediately.
Please help me in this issue to add animation in CAShapeLayer having CAGradientLayer.
Below is my code and my tried approaches...
NOTE: I have tried three approaches to add animation. Please check the commented code also
- (void)drawArcAnimation:(CGRect)rect {
if (self.itemIndex >= self.sliceItems.count) {
return;
}
float total = [self calculateTotal];
SliceItem *item = self.sliceItems[self.itemIndex];
UIBezierPath* path = [UIBezierPath bezierPath];
float angle = (item.itemValue/total) * 2 * M_PI;
float endAngle = self.startAngle + angle;
totalProgress += item.itemValue;
if (item.shouldGradient) {
// ***** This code will be used if we go for second approach of animation ****
UIBezierPath *rectToAnimateFrom = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) radius:rect.size.width/2 - self.lineWidth startAngle:self.startAngle endAngle:self.startAngle clockwise:NO];
UIBezierPath *rectToAnimateTo = [UIBezierPath bezierPathWithArcCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2) radius:rect.size.width/2 - self.lineWidth startAngle:self.startAngle endAngle:endAngle clockwise:NO];
// ***** END ****
CGMutablePathRef arc = CGPathCreateMutable();
CGPathAddArc(arc, NULL,
(rect.size.width / 2), (rect.size.height / 2),
rect.size.width/2 - self.lineWidth,
self.startAngle,
endAngle,
NO);
self.startAngle = endAngle;
CGFloat lineWidth = self.lineWidth;
CGPathRef strokedArc =
CGPathCreateCopyByStrokingPath(arc, NULL,
lineWidth,
kCGLineCapButt,
kCGLineJoinMiter, // the default
10); // 10 is default miter limit
CAShapeLayer *segment = [CAShapeLayer layer];
segment.fillColor = item.itemColor.CGColor;
segment.strokeColor = [UIColor clearColor].CGColor;
segment.path = strokedArc;
// [self.baseLayer addSublayer:segment];
CAGradientLayer *gradient = [CAGradientLayer layer];
if (totalProgress > 50) {
gradient.colors = #[(id)item.itemSecondaryColor.CGColor, (id)item.itemColor.CGColor,(id)item.itemColor.CGColor,(id)item.itemColor.CGColor];
}else {
gradient.colors = #[(id)item.itemColor.CGColor, (id)item.itemColor.CGColor, (id)item.itemColor.CGColor,(id)item.itemSecondaryColor.CGColor];
}
gradient.frame = CGPathGetBoundingBox(segment.path);
CAShapeLayer *mask = [CAShapeLayer layer];
CGAffineTransform translation = CGAffineTransformMakeTranslation(-CGRectGetMinX(gradient.frame),
-CGRectGetMinY(gradient.frame));
mask.path = CGPathCreateCopyByTransformingPath(segment.path,
&translation);
gradient.mask = mask;
[self.baseLayer addSublayer:gradient];
if (_duration == 0.0) {
self.itemIndex++;
[self drawArcAnimation:rect];
}else {
// ******* First Approach to animate *******
[CATransaction begin];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = _duration*(angle/(2 * M_PI));
drawAnimation.repeatCount = 1.0;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[CATransaction setCompletionBlock:^{
self.itemIndex++;
[self drawArcAnimation:rect];
}];
[gradient addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction commit];
// ******* Second Approach to animate *******
// [CATransaction begin];
// CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"path"];
// animation.fromValue = (__bridge id _Nullable)(rectToAnimateFrom.CGPath);
// animation.toValue = (__bridge id _Nullable)(rectToAnimateTo.CGPath);
// animation.duration = 3;
// animation.repeatCount = 1;
// animation.removedOnCompletion = false;
// animation.fillMode = kCAFillModeForwards;
//
// [CATransaction setCompletionBlock:^{
// self.itemIndex++;
// [self drawArcAnimation:rect];
// }];
//
// [gradient addAnimation:animation forKey:#"fill animation"];
// [CATransaction commit];
// ******* Third Approach to animate *******
// NSArray *fromColors = #[(id)UIColor.whiteColor.CGColor, (id)UIColor.clearColor.CGColor];
// NSArray *toColors = gradient.colors;
// [gradient setColors:toColors];
//
// [CATransaction begin];
// CABasicAnimation *animation1 = [CABasicAnimation animationWithKeyPath:#"colors"];
// animation1.fromValue = fromColors;
// animation1.toValue = toColors;
// animation1.duration = _duration;
// animation1.removedOnCompletion = YES;
// animation1.fillMode = kCAFillModeForwards;
// animation1.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// [CATransaction setCompletionBlock:^{
// self.itemIndex++;
// [self drawArcAnimation:rect];
// }];
// [gradient addAnimation:animation1 forKey:#"animateGradient"];
// [CATransaction commit];
}
}else {
[path addArcWithCenter:CGPointMake(rect.size.width / 2, rect.size.height / 2)
radius:rect.size.width/2 - self.lineWidth
startAngle:self.startAngle
endAngle:endAngle
clockwise:YES];
self.startAngle = endAngle;
CAShapeLayer *layer = [CAShapeLayer layer];
layer.path = path.CGPath;
CGRect frame = path.bounds;
frame.size.height = rect.size.height;
frame.size.width = frame.size.width * 30;
layer.strokeColor = item.itemColor.CGColor;
layer.fillColor = nil;
layer.lineWidth = self.lineWidth;
layer.strokeStart = 0;
layer.strokeEnd = 1.0;
[self.baseLayer addSublayer:layer];
if (_duration == 0.0) {
self.itemIndex++;
[self drawArcAnimation:rect];
}else {
[CATransaction begin];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = _duration*(angle/(2 * M_PI));
animation.fromValue = #(0.0);
animation.toValue = #(1.0);
[CATransaction setCompletionBlock:^{
self.itemIndex++;
[self drawArcAnimation:rect];
}];
[layer addAnimation:animation forKey:nil];
[CATransaction commit];
}
}
}
In above code you can a condition if (item.shouldGradien) and its else part. Animations are working fine in else part of CALayers but no in gradient part.
What I am doing wrong here? Any help... ???
I have implemented a basic line drawing animation for a progress bar.The line has a round cap edge.When I moving the slider the line drawing the ending of line is having a rounded cap but in the starting its a square. below is the code I am using am i missing anything??
Image Attached: expected output
Below Code I tried
#interface ViewController ()
{
CGPoint center;
CGFloat radius;
}
#end
#implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
center = CGPointMake(200.0, 200.0);
self.view.layer.backgroundColor = [[UIColor grayColor] CGColor];
}
- (IBAction)greenSlider:(UISlider *)sender {
[self.view.layer addSublayer:[self drawArcAtCenter:center startAngle:sender.minimumValue endAngle:sender.value radius:100.0 withColor:[UIColor greenColor]]];
}
- (IBAction)blueSlider:(UISlider *)sender {
int value = (int)sender.value;
if (value > 360) {
value = value % 90;
}
[self.view.layer addSublayer:[self drawArcAtCenter:center startAngle:sender.minimumValue endAngle:(float)value radius:100 withColor:[UIColor blueColor]]];
}
- (CAShapeLayer *) drawArcAtCenter:(CGPoint)newCenter startAngle:(CGFloat)sAngle endAngle:(CGFloat)bAngle radius:(CGFloat) radious withColor:(UIColor*) color
{
CGFloat begAngle = sAngle * 3.1459 / 180.0;
CGFloat endAngle = bAngle * 3.1459 / 180.0;
CAShapeLayer *layer = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPath];
[path addArcWithCenter:newCenter radius:radious startAngle:begAngle endAngle:endAngle clockwise:YES];
layer.path = [path CGPath];
layer.strokeColor = [color CGColor];
layer.fillColor = [[UIColor clearColor] CGColor];;
layer.lineWidth = 50.0;
layer.strokeEnd = 50.0f;
layer.lineCap = kCALineCapRound;
layer.cornerRadius = 30.0f;
return layer;
}
The code below is drawing me a circle, how can I modify the existing code to draw a triangle instead?
_colorDotLayer = [CALayer layer];
CGFloat width = self.bounds.size.width-6;
_colorDotLayer.bounds = CGRectMake(0, 0, width, width);
_colorDotLayer.allowsGroupOpacity = YES;
_colorDotLayer.backgroundColor = self.annotationColor.CGColor;
_colorDotLayer.cornerRadius = width/2;
_colorDotLayer.position = CGPointMake(self.bounds.size.width/2, self.bounds.size.height/2);
While there are shown several Core Graphics solution, I want to add a Core Animation based solution.
- (void)viewDidLoad
{
[super viewDidLoad];
UIBezierPath* trianglePath = [UIBezierPath bezierPath];
[trianglePath moveToPoint:CGPointMake(0, view3.frame.size.height-100)];
[trianglePath addLineToPoint:CGPointMake(view3.frame.size.width/2,100)];
[trianglePath addLineToPoint:CGPointMake(view3.frame.size.width, view2.frame.size.height)];
[trianglePath closePath];
CAShapeLayer *triangleMaskLayer = [CAShapeLayer layer];
[triangleMaskLayer setPath:trianglePath.CGPath];
UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0,0, size.width, size.height)];
view.backgroundColor = [UIColor colorWithWhite:.75 alpha:1];
view.layer.mask = triangleMaskLayer;
[self.view addSubview:view];
}
code based on my blog post.
#implementation TriangleView {
CAShapeLayer *_colorDotLayer;
}
- (void)layoutSubviews {
[super layoutSubviews];
if (_colorDotLayer == nil) {
_colorDotLayer = [CAShapeLayer layer];
_colorDotLayer.fillColor = self.annotationColor.CGColor;
[self.layer addSublayer:_colorDotLayer];
}
CGRect bounds = self.bounds;
CGFloat radius = (bounds.size.width - 6) / 2;
CGFloat a = radius * sqrt((CGFloat)3.0) / 2;
CGFloat b = radius / 2;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(0, -radius)];
[path addLineToPoint:CGPointMake(a, b)];
[path addLineToPoint:CGPointMake(-a, b)];
[path closePath];
[path applyTransform:CGAffineTransformMakeTranslation(CGRectGetMidX(bounds), CGRectGetMidY(bounds))];
_colorDotLayer.path = path.CGPath;
}
- (void)awakeFromNib {
[super awakeFromNib];
self.annotationColor = [UIColor redColor];
}
#end
Result:
I think is more easy than this solutions:
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){20, 0}];
[path addLineToPoint:(CGPoint){40, 40}];
[path addLineToPoint:(CGPoint){0, 40}];
[path addLineToPoint:(CGPoint){20, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewTriangleCallout.bounds;
mask.path = path.CGPath;
This is my white triangle:
You can change points or rotate if you want:
UIBezierPath *path = [UIBezierPath new];
[path moveToPoint:(CGPoint){0, 0}];
[path addLineToPoint:(CGPoint){40, 0}];
[path addLineToPoint:(CGPoint){20, 20}];
[path addLineToPoint:(CGPoint){0, 0}];
// Create a CAShapeLayer with this triangular path
// Same size as the original imageView
CAShapeLayer *mask = [CAShapeLayer new];
mask.frame = self.viewTriangleCallout.bounds;
mask.path = path.CGPath;
Example code, it is based on this SO Answer which draws stars:
#implementation TriangleView
-(void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
int sides = 3;
double size = 100.0;
CGPoint center = CGPointMake(160.0, 100.0);
double radius = size / 2.0;
double theta = 2.0 * M_PI / sides;
CGContextMoveToPoint(context, center.x, center.y-radius);
for (NSUInteger k=1; k<sides; k++) {
float x = radius * sin(k * theta);
float y = radius * cos(k * theta);
CGContextAddLineToPoint(context, center.x+x, center.y-y);
}
CGContextClosePath(context);
CGContextFillPath(context); // Choose for a filled triangle
// CGContextSetLineWidth(context, 2); // Choose for a unfilled triangle
// CGContextStrokePath(context); // Choose for a unfilled triangle
}
#end
Use UIBezeierPaths
CGFloat radius = 20;
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, (center.x + bottomLeft.x) / 2, (center.y + bottomLeft.y) / 2);
CGPathAddArcToPoint(path, NULL, bottomLeft.x, bottomLeft.y, bottomRight.x, bottomRight.y, radius);
CGPathAddArcToPoint(path, NULL, bottomRight.x, bottomRight.y, center.x, center.y, radius);
CGPathAddArcToPoint(path, NULL, center.x, center.y, bottomLeft.x, bottomLeft.y, radius);
CGPathCloseSubpath(path);
UIBezierPath *bezierPath = [UIBezierPath bezierPathWithCGPath:path];
CGPathRelease(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...
I have created 2 CALayers of same size and am passing these to the method below. However, the two layers runs together. How can I seperate these so that both are visible?
- (void) myAnimation : (CALayer *) sublayer {
UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(30, 100, 270, 270)];
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = aPath.CGPath;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration =35.0;
[sublayer addAnimation:anim forKey:#"race"];
}
Your path begins and ends at the same point. Assuming both beginning times are the same and duration is the same, your layers will precisely overlap. You can either shift the beginning time or rotate your UIBezierPath* aPath Here's an example of rotating your UIBezierPath* aPath and altering the duration. It should give you an idea how you could alter duration, begin time, rotation, etc.
- (void)viewDidLoad
{
[super viewDidLoad];
[self.view setBackgroundColor:[UIColor blackColor]];
CALayer * layer1 = [CALayer layer];
CALayer * layer2 = [CALayer layer];
[layer1 setFrame:CGRectMake(0, 0, 5, 50)];
[layer2 setFrame:CGRectMake(0, 0, 5, 100)];
layer1.backgroundColor = [[UIColor colorWithRed:.1 green:.5 blue:1 alpha:.35] CGColor];
layer2.backgroundColor = [[UIColor colorWithRed:.9 green:.2 blue:.5 alpha:.35] CGColor];
[self.view.layer addSublayer:layer1];
[self.view.layer addSublayer:layer2];
[self myAnimation:layer1 andRotation:0 andDuration:35.0];
[self myAnimation:layer2 andRotation:10 andDuration:10.0];
}
- (void) myAnimation:(CALayer *)sublayer andRotation:(CGFloat)rot andDuration:(CFTimeInterval)dur {
CGRect rect = CGRectMake(30, 100, 270, 270);
UIBezierPath* aPath = [UIBezierPath bezierPathWithOvalInRect:rect];
// Creating a center point around which we will transform the path
CGPoint center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect));
CGAffineTransform transform = CGAffineTransformIdentity;
transform = CGAffineTransformTranslate(transform, center.x, center.y);
transform = CGAffineTransformRotate(transform, rot);
// Recenter the transform
transform = CGAffineTransformTranslate(transform, -center.x, -center.y);
// This is the new path we will use.
CGPathRef rotated = CGPathCreateCopyByTransformingPath(aPath.CGPath, &transform);
CAKeyframeAnimation *anim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
anim.path = rotated;
anim.rotationMode = kCAAnimationRotateAuto;
anim.repeatCount = HUGE_VALF;
anim.duration =dur;
[sublayer addAnimation:anim forKey:#"race"];
}