iOS "erase" drawing with CAShapeLayer animated - ios

I'm trying to implement a circular indicator that draws itself with animation as values change.
I use CAShapeLayer to draw with animation. So far I got it to draw itself from the beginning each time, it works as intended. Now I want to make it more sleek and draw it forward and "erase" depending on if the new value is greater or lower than previous. I'm not sure how to implement the erasing part. There's a background so I cant just draw on top with white colour. Here's an image to better understand how it looks.
Any ideas how erasing part could be achieved? There's some solutions here on SO on similar problems, but those involve no animation.
I use this code to draw:
- (void)drawCircleAnimatedWithStartAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
color:(UIColor *)color
radius:(int)radius
lineWidth:(int)lineWidth {
CAShapeLayer *circle = [CAShapeLayer layer];
circle.name = #"circleLayer";
circle.path = ([UIBezierPath bezierPathWithArcCenter:CGPointMake(radius, radius) radius:radius-1 startAngle:startAngle endAngle:endAngle clockwise:YES].CGPath);
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
circle.strokeColor = [UIColor colorWithRed:19.0/255 green:167.0/255 blue:191.0/255 alpha:1].CGColor;
circle.lineWidth = lineWidth;
// Add to parent layer
for (CALayer *layer in self.circleView.layer.sublayers) {
if ([layer.name isEqualToString:#"circleLayer"]) {
[layer removeFromSuperlayer];
break;
}
}
[self.circleView.layer addSublayer:circle];
circle.zPosition = 1;
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 0.5;
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:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
// Add the animation to the circle
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}

I was able to do this in a slightly different way from what you're doing. Instead of drawing an arc of a certain length, I made my path a full circle, but only animate it part way. You will notice that I use the presentation layer's strokeEnd to get the toValue, so that if an animation is in progress when you need to change the value of the final strokeEnd, it will start from where the path is currently drawn. Here is the code in my view controller; outline layer draws the gray circle, similar to what you have in your image. I added 5 buttons to my view (for testing) whose tags are used to change the final stroke value of the animation,
#implementation ViewController {
UIView *circleView;
RDShapeLayer *circle;
}
- (void)viewDidLoad {
[super viewDidLoad];
circleView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 200, 200)];
CALayer *outlineLayer = [CALayer new];
outlineLayer.frame = circleView.bounds;
outlineLayer.borderWidth = 2;
outlineLayer.borderColor = [UIColor lightGrayColor].CGColor;
outlineLayer.cornerRadius = circleView.frame.size.width/2.0;
[circleView.layer addSublayer:outlineLayer];
[self.view addSubview:circleView];
circle = [[RDShapeLayer alloc] initWithFrame:circleView.bounds color:[UIColor blueColor].CGColor lineWidth:4];
[circleView.layer addSublayer:circle];
}
-(void)animateToPercentWayAround:(CGFloat) percent {
circle.strokeEnd = percent/100.0;
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 1.0;
drawAnimation.fromValue = #([circle.presentationLayer strokeEnd]);
drawAnimation.toValue = #(percent/100.0);
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
- (IBAction)changeStroke:(UIButton *)sender {
[self animateToPercentWayAround:sender.tag];
}
This is the code for the subclassed CAShapeLayer,
-(instancetype)initWithFrame:(CGRect) frame color:(CGColorRef) color lineWidth:(CGFloat) lineWidth {
if (self = [super init]) {
self.path = [UIBezierPath bezierPathWithOvalInRect:CGRectInset(frame, 1, 1)].CGPath;
self.fillColor = [UIColor clearColor].CGColor;
self.strokeColor = color;
self.lineWidth = lineWidth;
self.strokeStart = 0;
self.strokeEnd = 0;
}
return self;
}

Related

CAShapeLayer rendering issue

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.

How to make round animation in ios

If I click the Workout button, I want to show the red color round animation and round the images. And I want to move another class in Storyboard. How can I accomplish this?
Using UIBezierPath or CAShapeLayer, you can achieve this. I tested the following code in one of my projects and found it working :
- (void)viewDidLoad
{
[super viewDidLoad];
int radius = 50;
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 = 5;
// Add to parent layer
[self.view.layer addSublayer:circle];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0;
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:1.0f];
// 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:#"draw"];
// Do any additional setup after loading the view, typically from a nib.
}
The output is as follows:

Why when I add my animation with Core Animation does it immediately snap back to what it was at the end? [duplicate]

This question already has an answer here:
Animation Snaps back when the UIImage is changed
(1 answer)
Closed 8 years ago.
I'm trying to build a simple pie-chart style progress indicator. It animates well, but after the animation concludes, it snaps right back to the previous value before the animation.
My code is as follows:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
// Create a white ring that fills the entire frame and is 2 points wide.
// Its frame is inset 1 point to fit for the 2 point stroke width
CGFloat radius = CGRectGetWidth(self.frame) / 2;
CGFloat inset = 1;
CAShapeLayer *ring = [CAShapeLayer layer];
ring.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
ring.fillColor = [UIColor clearColor].CGColor;
ring.strokeColor = [UIColor whiteColor].CGColor;
ring.lineWidth = 2;
// Create a white pie-chart-like shape inside the white ring (above).
// The outside of the shape should be inside the ring, therefore the
// frame needs to be inset radius/2 (for its outside to be on
// the outside of the ring) + 2 (to be 2 points in).
self.innerPie = [CAShapeLayer layer];
inset = radius/2; // The inset is updated here
self.innerPie.path = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(self.bounds, inset, inset)
cornerRadius:radius-inset].CGPath;
self.innerPie.fillColor = [UIColor clearColor].CGColor;
self.innerPie.strokeColor = [UIColor whiteColor].CGColor;
self.innerPie.lineWidth = (radius-inset)*2;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
self.innerPie.hidden = YES;
}
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
self.innerPie.hidden = NO;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.duration = 3.0;
pathAnimation.fromValue = [NSNumber numberWithFloat:_progress];
pathAnimation.toValue = [NSNumber numberWithFloat:progress];
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
if (_progress != progress) {
_progress = progress;
}
}
I simply set it up in drawRect then have a method to set the progress with an animation. What am I doing wrong here?
That's how core Animation works. What you want to do is to set the property to it's final value just before adding the animation to the layer. Then the animation hides the fact that you changed the property, and when the animation is removed after it's finished, the property is at it's end value.

Adding an animating circle to my UITableViewCell AccesoryView

I'd like to display an animating circle in my UITableViewCell. I've gotten the following code to work for self.view, but when I try to add it to cell.accessoryview it never draws. Anybody have any ideas on why it's not showing up?
- (void)makeCircleAtIndexPath:(NSIndexPath *)path {
UITableViewCell *cell = [_permanentSoundTableView cellForRowAtIndexPath:path];
// Set up the shape of the circle
int radius = 10;
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(cell.frame.size.width-10,cell.frame.size.height-5);
// Configure the apperence of the circle
circle.fillColor = [UIColor clearColor].CGColor;
UIColor *transparentBlack = [UIColor colorWithRed:250.0f green:250.0f blue:250.0f alpha:.5];
circle.strokeColor = transparentBlack.CGColor;
circle.lineWidth = 5;
// Add to parent layer
[cell.accessoryView.layer addSublayer:circle];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 8.0; // "animate over 10 seconds or so.."
drawAnimation.repeatCount = 1.0; // Animate only once..
drawAnimation.removedOnCompletion = NO; // Remain stroked after the animation..
// 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.0f];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add the animation to the circle
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
}
Try adding your animation layer inside the cellForRowAtIndexPath: method, and make sure accessoryView of the cell is not nil. If it's nil, then add empty UIView with your animation layer's size, and then adding the layer. Good Luck!

How to draw a rectangle with an animated stroke

I know how to draw a rectangle outline to the screen with something like this:
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGPathRef path = CGPathCreateWithRect(rect, NULL);
[[UIColor greenColor] setStroke];
CGContextAddPath(context, path);
CGContextDrawPath(context, kCGPathFillStroke);
CGPathRelease(path);
}
But what I want is to have the "pen" start at the top center of the rectangle and draw around the edges at some variable speed, so that you can actually see the rectangle getting drawn as the "pen" moves. Is this possible? How?
You could easily use a CALayerShape with a Pen Image and do it like this (I added a button to the view which triggered the drawing):
#import "ViewController.h"
#interface ViewController () {
UIBezierPath *_drawPath;
CALayer *_pen;
UIBezierPath *_penPath;
CAShapeLayer *_rectLayer;
}
#end
#implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
UIImage *penImage = [UIImage imageNamed:#"pen"];
_drawPath = [UIBezierPath bezierPathWithRect:self.view.frame];
_penPath = [UIBezierPath bezierPath];
[_penPath moveToPoint:CGPointMake(penImage.size.width/2.f, penImage.size.height/2.f)];
[_penPath addLineToPoint:CGPointMake(self.view.frame.size.width - penImage.size.width/2.f, penImage.size.height/2.f)];
[_penPath addLineToPoint:CGPointMake(self.view.frame.size.width - penImage.size.width/2.f, self.view.frame.size.height - penImage.size.height/2.f)];
[_penPath addLineToPoint:CGPointMake(penImage.size.width/2.f, self.view.frame.size.height - penImage.size.height/2.f)];
[_penPath addLineToPoint:CGPointMake(penImage.size.width/2.f, penImage.size.height/2.f)];
_rectLayer = [[CAShapeLayer alloc] init];
_rectLayer.path = _drawPath.CGPath;
_rectLayer.strokeColor = [UIColor greenColor].CGColor;
_rectLayer.lineWidth = 5.f;
_rectLayer.fillColor = [UIColor clearColor].CGColor;
_rectLayer.strokeEnd = 0.f;
[self.view.layer addSublayer:_rectLayer];
_pen = [CALayer layer];
_pen.bounds = CGRectMake(0, 0, 25.f, 25.f);
_pen.position = CGPointMake(penImage.size.width/2.f, penImage.size.height/2.f);
_pen.contents = (id)(penImage.CGImage);
_pen.position = CGPointMake(penImage.size.width/2.f, penImage.size.height/2.f);
[self.view.layer addSublayer:_pen];
}
- (IBAction)drawRectangle:(id)sender
{
_rectLayer.strokeEnd = 1.f;
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
anim.fromValue = (id)[NSNumber numberWithFloat:0];
anim.toValue = (id)[NSNumber numberWithFloat:1.f];
anim.duration = 5.f;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
[_rectLayer addAnimation:anim forKey:#"drawRectStroke"];
CAKeyframeAnimation *penMoveAnimation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
penMoveAnimation.path = _penPath.CGPath;
penMoveAnimation.rotationMode = kCAAnimationRotateAuto;
penMoveAnimation.duration = 5.0;
penMoveAnimation.calculationMode = kCAAnimationPaced;
[_pen addAnimation:penMoveAnimation forKey:#"followStroke"];
}
EDIT: Added code for pen to follow stroke, heres the 2x image used: http://cl.ly/image/173J271Y003B
Note: The only problem is that when the pen gets closer to the rotating point or corner its still paced with the stroke therefore the pen looks like its behind the stroke a bit until it flips, a simple solution might be to arc the curve, but Im not sure what your overall goal is.

Resources