I am trying to animate loading animation along the path of rounded rect,using following code. (I need to animate border of square as loader ,partial border.) It does not happen to be smooth ,as soon as it reaches the end of path it jumps to the starting start stroke.
shapeLayer = [CAShapeLayer layer];
CGRect shapeRect = _border.bounds;//CGRectMake(0.0f, 0.0f, 200.0f, 100.0f);
[shapeLayer setBounds:shapeRect];
[shapeLayer setContents:_border.image];
shapeLayer.contentsGravity = kCAGravityCenter;
NSLog(#"center of border %#",NSStringFromCGPoint(_border.center) );
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:shapeLayer.bounds cornerRadius:BORDER_ANGLE];
[shapeLayer setPath:path.CGPath];
shapeLayer.position = _border.center;
[[self.view layer] addSublayer:shapeLayer];
[[self.view layer] insertSublayer:shapeLayer above:_border.layer];
[shapeLayer setFillColor:[[UIColor clearColor] CGColor]];
[shapeLayer setStrokeColor:[[UIColor whiteColor] CGColor]];
[shapeLayer setLineWidth:2.5f];
shapeLayer.strokeStart = 0;
shapeLayer.strokeEnd = 0.1;
UIImageView * image = [[UIImageView alloc] initWithImage:[UIImage imageNamed:#"ball"]];
image.center = CGPointMake(187, 220.5);
startAnim = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
startAnim.duration = SPEED;
startAnim.delegate = self;
startAnim.repeatCount=200;
startAnim.removedOnCompletion = FALSE;
startAnim.toValue = [NSNumber numberWithFloat:0.3];
endAnim = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
endAnim.toValue = [NSNumber numberWithFloat:0.99];
endAnim.duration = SPEED;
endAnim.repeatCount = 200;
endAnim.removedOnCompletion = NO;
endAnim.cumulative = NO;
[shapeLayer addAnimation:startAnim forKey:nil];
[shapeLayer addAnimation:endAnim forKey:nil];
But above code jitters after reaching at endAnim's tovalue and starts again,its not smooth.
Please help me know which parts going wrong. I saw many tutorials for loader but all those were for circular path not square.
[enter image description here][1]
! [1]: https://i.stack.imgur.com/FwT01.png
just want to fill 50% of border and rotate it continuously
Okay. So do not use animation of a shape layer. Draw the square. Now make a mask that is a filled square with half of it empty. Place the mask on your square. So now we see only 50% of your square because the rest is masked out. Got it?
Now rotate the mask.
Related
I want to approach the following scenario:
If I draw a rectangle using CGBezierPath and CAShapeLayer like the following:
CAShapeLayer *layerX = [CAShapeLayer layer];
layerX.path = path.CGPath;
layerX.lineWidth = 3;
layerX.strokeColor = [[UIColor whiteColor] CGColor];
layerX.fillColor = [[UIColor clearColor] CGColor];
[self.view.layer addSublayer: layerX];
And I want to add animation like the image I attached, this cleared area keeps moving around the rectangle path so it gives visual effect of the rectangle being drawn over and over. [Snake movement in old snake games]
I tried animating using CABasicAnimation but I literally couldn't achieve anything, thanks in advance.
Try this code may be helpful.
UIBezierPath *drawablePath = [UIBezierPath bezierPathWithRect:CGRectMake(100.0, 100.0, 150.0, 100.0)];
CAShapeLayer *squareLayer = [[CAShapeLayer alloc] init];
squareLayer.path = drawablePath.CGPath;
squareLayer.strokeColor = [UIColor redColor].CGColor;
squareLayer.lineWidth = 5.f;
squareLayer.fillColor = [UIColor clearColor].CGColor;
squareLayer.strokeEnd = 1.f;
//squareLayer.strokeEnd = 0.9; // this line use draw half line but some animation issue.
[self.view.layer addSublayer:squareLayer];
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.fromValue = (id)[NSNumber numberWithFloat:0.10];
drawAnimation.toValue = (id)[NSNumber numberWithFloat:1.f];
drawAnimation.duration = 5.f;
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[squareLayer addAnimation:drawAnimation forKey:#"drawRectStroke"];
This code is use to draw rectangle with animation.
You have to animate both: strokeStart and strokeEnd and use a CAAnimationGroup to make them happen at the same time.
I found this link which does something similar but in swift. I think it might help.
I will try to implement it if you can't do it. But I can't right now.
Happy coding.
let drawablePath = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 150, height: 150))
let squareLayer = CAShapeLayer()
squareLayer.path = drawablePath.cgPath;
squareLayer.strokeColor = UIColor.red.cgColor;
squareLayer.lineWidth = 5;
squareLayer.fillColor = UIColor.clear.cgColor;
squareLayer.strokeEnd = 1;
//squareLayer.strokeEnd = 0.9; // this line use draw half line but some animation issue.
flowerView.layer.addSublayer(squareLayer)
let drawAnimation = CABasicAnimation(keyPath: "strokeEnd")
drawAnimation.fromValue = 0.1
drawAnimation.toValue = 1.0
drawAnimation.duration = 5
drawAnimation.timingFunction = CAMediaTimingFunction(name:.linear)
squareLayer.add(drawAnimation, forKey:"drawRectStroke")
Not really an answer to the original question but here's a Swift 5 version of Dixit Akabari's answer, which was useful in itself in that it demonstrates how to animate drawing a UIBezierPath by animating strokeEnd.
I am using this code to create the slide to unlock like animation but I am unable to make it animate from right to left.
How can I make it start from right and animate to left?
-(void)slideToCancel {
CALayer *maskLayer = [CALayer layer];
// Mask image ends with 0.15 opacity on both sides. Set the background color of the layer
// to the same value so the layer can extend the mask image.
maskLayer.backgroundColor = [[UIColor colorWithRed:0.0f green:0.0f blue:0.0f alpha:0.50f] CGColor];
maskLayer.contents = (id)[[UIImage imageNamed:#"slidetocancel.png"] CGImage];
// Center the mask image on twice the width of the text layer, so it starts to the left
// of the text layer and moves to its right when we translate it by width.
maskLayer.contentsGravity = kCAGravityCenter;
maskLayer.frame = CGRectMake(_slideToCancelLbl.frame.size.width * -1, 0.0f, _slideToCancelLbl.frame.size.width * 2, _slideToCancelLbl.frame.size.height);
// Animate the mask layer's horizontal position
CABasicAnimation *maskAnim = [CABasicAnimation animationWithKeyPath:#"position.x"];
maskAnim.byValue = [NSNumber numberWithFloat:_slideToCancelLbl.frame.size.width];
maskAnim.repeatCount = 1e100f;
maskAnim.duration = 1.5f;
[maskLayer addAnimation:maskAnim forKey:#"slideAnim"];
_slideToCancelLbl.layer.mask = maskLayer;
}
You should be able to simply use a negative byValue. However, the code you've posted is a canned animation, not one that responds to a slide gesture.
I am working on one of the school application. In which, based on the percentage achieved by student respective piechart should be drawn with an animation.
For example, if student got only 50%, the pie chart needs to be filled with 50% with an animation.
Can you please help me on this. I used the below code,
// Set up the shape of the circle
int radius = 60;
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(CGRectGetMidX(self.view.frame)-radius,
CGRectGetMidY(self.view.frame)-radius);
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor redColor].CGColor;
circle.lineWidth = 10;
// Add to parent layer
[self.view.layer addSublayer:circle];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 10.0; // "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.75f];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
// Add the animation to the circle
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
I would like to stop the animation at 50%.
Please use the circle.strokeEnd = 0.5f;
It will automatically stops at 50%,
if you give 1.0f it goes to 100%
I'm trying to create circle animation by adding CAShapeLayers with different stroke colors:
UIView *view = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
[self addCircleAnimationTo:view direction:YES];
- (void)addCircleAnimationTo:(UIView *)view direction:(BOOL)direction {
CGFloat animationTime = 3;
// Set up the shape of the circle
int radius = view.frame.size.width/2;
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 view
circle.position = CGPointMake(radius,
radius);
circle.bounds = view.bounds;
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
// switch color
circle.strokeColor = (direction) ? [UIColor blueColor].CGColor : [UIColor whiteColor].CGColor;
circle.lineWidth = 5;
circle.lineJoin = kCALineJoinBevel;
circle.strokeStart = .0;
circle.strokeEnd = 1.;
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = animationTime;
drawAnimation.repeatCount = 1;
// 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:1.];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add to parent layer
[view.layer addSublayer:circle];
// Add the animation to the circle
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animationTime-0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addCircleAnimationTo:view direction:!direction];
});
}
I'm wondering why I can see blue when white layer animates and how to get rid of this weird blue color border:
You know that you are adding more and more shape layers after each call to addCircleAnimationTo ?
While you animate the new layer the old shape layer reminds below the new one. That is why you see it.
[EDIT 2] SHORT EXPLANATION
We create two layers first with blue path colour, second with white path colour.
To first layer we add fill animation - change of layer.strokeEnd property, we animate it from 0 to 1 and from 1 to 1. Half of duration from 0 to 1 and half of duration from 1 to 1 (visually nothing happens, but is needed).
We add clear animation - change of layer.strokeStart, we animate it from 0 to 1. Duration of this animation is half of duration of fill animation. Begin time is also half of fill animation because we want to move beginning of stroke when end stroke is still.
We add the same animations to second, white layer but with appropriate begin time offset, thanks to that white path unwinds and blue path winds.
[EDIT] ADDED REAL ANSWER
Here is my suggestion to solve your animation problem. Hope it helps ... :)
- (void)viewDidLoad {
[super viewDidLoad];
UIView *view = [[UIButton alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
[self.view addSubview:view];
view.backgroundColor = [UIColor redColor];
self.circle1 = [self circle:view.bounds];
self.circle1.strokeColor = [UIColor blueColor].CGColor;
self.circle2 = [self circle:view.bounds];
self.circle2.strokeColor = [UIColor whiteColor].CGColor;
[view.layer addSublayer:self.circle1];
[view.layer addSublayer:self.circle2];
[self.circle1 addAnimation:[self fillClearAnimation:0] forKey:#"fillclear"];
[self.circle2 addAnimation:[self fillClearAnimation:3] forKey:#"fillclear"];
}
- (CAAnimationGroup *)fillClearAnimation:(CFTimeInterval)offset
{
CGFloat animationTime = 3;
CAAnimationGroup *group = [CAAnimationGroup animation];
CAKeyframeAnimation *fill = [CAKeyframeAnimation animationWithKeyPath:#"strokeEnd"];
fill.values = #[ #0,#1,#1];
fill.duration = 2*animationTime;
fill.beginTime = 0.f;
CAKeyframeAnimation *clear = [CAKeyframeAnimation animationWithKeyPath:#"strokeStart"];
clear.values = #[ #0, #1];
clear.beginTime = animationTime;
clear.duration = animationTime;
group.animations = #[ fill, clear ];
group.duration = 2*animationTime;
group.beginTime = CACurrentMediaTime() + offset;
group.repeatCount = FLT_MAX;
return group;
}
- (CAShapeLayer *)circle:(CGRect)frame
{
CAShapeLayer *circle = [CAShapeLayer layer];
CGFloat radius = ceil(frame.size.width/2);
// Make a circular shape
circle.path = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, 2.0*radius, 2.0*radius)
cornerRadius:radius].CGPath;
// Center the shape in view
circle.position = CGPointMake(radius,
radius);
circle.frame = frame;
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.lineWidth = 5;
circle.lineJoin = kCALineJoinBevel;
circle.strokeStart = 0.f;
circle.strokeEnd = 0.f;
return circle;
}
The antialiasing is what's causing the problem
When the white CAShapeLayer gets drawn over the blue, the edge gets blended with the previous edge blending that the antialiasing created.
The only real solution is to clear the blue from underneath the white as you animate. You can do this by animating the strokeStart property of the previous layer from 0.0 to 1.0. You should be able to re-use your drawAnimation for this, by just changing the keyPath. For example, change your dispatch_after to:
- (void)addCircleAnimationTo:(UIView *)view direction:(BOOL)direction {
// Insert your previous animation code, where drawAnimation is defined and added to the circle layer.
drawAnimation.keyPath = #"strokeStart"; // Re-use the draw animation object, setting it to animate strokeStart now.
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)((animationTime-0.1) * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self addCircleAnimationTo:view direction:!direction];
[circle addAnimation:drawAnimation forKey:#"(un)drawCircleAnimation"]; // Sets the previous layer to animate out
});
}
That way the antialiasing edge blending gets cleared from the blue layer, allowing the white layer to get a nice & clean antialiasing. Just don't forget to remove the layer once you've animated it out!
Another potential solution is to increase the stroke width of the white layer, so it strokes over the blue layer's edge blending, however I would imagine it would create an undesired effect. You could also disable antialiasing, but that would look horrible on the circle.
I am currently using CAShapeLayer for one of my requirement. I was successful in implementing the fillcolor, strokecolor, etc... but I wanted to change the color of outer part of that CAShapeLayer. I tried doing it with backgroundcolor, & followed many answers from SO, but was unable to do it. Could anyone guide me with the solution. Screenshot attached
Edit 1: Code for creating the layer
// create layer mask for map
CAShapeLayer *maskLayer = [CAShapeLayer layer];
mapView.layer.mask = maskLayer;
// maskLayer.backgroundColor = [[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.7] CGColor];
self.maskLayer = maskLayer;
// create shape layer for circle we'll draw on top of map (the boundary of the circle)
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.lineWidth = 3.0;
circleLayer.fillColor = [[UIColor clearColor] CGColor];
circleLayer.strokeColor = [[UIColor blackColor] CGColor];
// circleLayer.borderColor = [[UIColor blackColor] CGColor];
// circleLayer.backgroundColor = [[UIColor colorWithRed:0.0 green:0.0 blue:0.0 alpha:0.7] CGColor];
[mapView.layer addSublayer:circleLayer];
self.circleLayer = circleLayer;
Edit 2: Xcode's Viewer's Debugger
In the above image I can see that I have set the background color to self.view. But I want it to be over my map view & the the color should be semi transparent so that map data outside circle should also be visible.
Have you tried using XCode views's debugger to know where this white background belongs in your view hierarchy ?
You're using your view's maskLayer as a circle. So my guess is that this white background must be OUTSIDE your view - everything out your mask gets clipped - so you should probably try to change your viewController's view backgroundColor (or any other view that is just above your custom rounded view)
[self.view.layer setBackgroundColor:[UIColor grayColor].CGColor];
UIBezierPath *circle = [UIBezierPath bezierPathWithArcCenter:self.view.center
radius:45.0
startAngle:0
endAngle:2.0*M_PI
clockwise:YES];
CAShapeLayer *circleLayer = [CAShapeLayer layer];
circleLayer.bounds = CGRectMake(0, 0, 2.0*(45.0), 2.0*(45.0));
circleLayer.path = circle.CGPath;
circleLayer.strokeColor = [UIColor orangeColor].CGColor;
circleLayer.fillColor = [UIColor whiteColor].CGColor;
circleLayer.lineWidth = 3.0; // your line width
[self.view.layer addSublayer:circleLayer];
See if it helps you.