CAShapeLayer animation doesn't stay on screen but disappears - ios

I'm trying to draw a animated circle but every segment needs to have another color. Now everything works except that my piece that is just drawed before I call the method again disappears so only the last part stays. I don't want that, I want that after 4 times a whole circle is drawn in different color strokes.
How can I fix this that it doesn't disappears?
This is my init code:
- (void)_initCircle {
_circle = [CAShapeLayer layer];
_radius = 100;
// 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);
_circle.fillColor = [UIColor clearColor].CGColor;
_circle.lineWidth = 5;
// Add to parent layer
[self.view.layer addSublayer:_circle];
}
This is my draw piece:
- (void)_drawStrokeOnCircleFrom:(float)start to:(float)end withColor:(UIColor *)color {
// Configure the apperence of the circle
_circle.strokeColor = color.CGColor;
_circle.strokeStart = start;
_circle.strokeEnd = end;
[CATransaction begin];
// Configure animation
CABasicAnimation *drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 2.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:start];
drawAnimation.toValue = [NSNumber numberWithFloat:end];
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[CATransaction setCompletionBlock:^{
NSLog(#"Complete animation");
_index++;
if(_index < _votes.count){
_startStroke = _endStroke;
_endStroke += [_votes[_index] floatValue] / _totalListens;
[self _drawStrokeOnCircleFrom:_startStroke to:_endStroke withColor:_colors[_index]];
}
}];
// Add the animation to the circle
[_circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];
[CATransaction commit];
}

Try setting these values on your CABasicAnimation:
drawAnimation.removedOnCompletion = NO;
drawAnimation.fillMode = kCAFillModeForwards;

That worked for me using "kCAFillModeBoth", both statements are necessary:
//to keep it after finished
animation.removedOnCompletion = false;
animation.fillMode = kCAFillModeBoth;

I solved the problem with extra layers like this:
- (void)_drawStrokeOnCircle {
[self _initCircle];
[CATransaction begin];
for (NSUInteger i = 0; i < 4; i++)
{
CAShapeLayer* strokePart = [[CAShapeLayer alloc] init];
strokePart.fillColor = [[UIColor clearColor] CGColor];
strokePart.frame = _circle.bounds;
strokePart.path = _circle.path;
strokePart.lineCap = _circle.lineCap;
strokePart.lineWidth = _circle.lineWidth;
// Configure the apperence of the circle
strokePart.strokeColor = ((UIColor *)_colors[i]).CGColor;
strokePart.strokeStart = [_segmentsStart[i] floatValue];
strokePart.strokeEnd = [_segmentsEnd[i] floatValue];
[_circle addSublayer: strokePart];
// Configure animation
CAKeyframeAnimation* drawAnimation = [CAKeyframeAnimation animationWithKeyPath: #"strokeEnd"];
drawAnimation.duration = 4.0;
// Animate from no part of the stroke being drawn to the entire stroke being drawn
NSArray* times = #[ #(0.0),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(1.0) ];
NSArray* values = #[ #(strokePart.strokeStart),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(strokePart.strokeEnd) ];
drawAnimation.keyTimes = times;
drawAnimation.values = values;
drawAnimation.removedOnCompletion = NO;
drawAnimation.fillMode = kCAFillModeForwards;
// Experiment with timing to get the appearence to look the way you want
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
[strokePart addAnimation: drawAnimation forKey: #"drawCircleAnimation"];
}
[CATransaction commit];
}
- (void)_initCircle {
[_circle removeFromSuperlayer];
_circle = [CAShapeLayer layer];
_radius = 100;
// 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);
_circle.fillColor = [UIColor clearColor].CGColor;
_circle.lineWidth = 5;
// Add to parent layer
[self.view.layer addSublayer:_circle];
}

SWIFT 4, Xcode 11:
let strokeIt = CABasicAnimation(keyPath: "strokeEnd")
let timeLeftShapeLayer = CAShapeLayer()
and in your viewDidLoad
override func viewDidLoad() {
super.viewDidLoad()
drawTimeLeftShape()
strokeIt.fillMode = CAMediaTimingFillMode.forwards
strokeIt.isRemovedOnCompletion = false
strokeIt.fromValue = 0
strokeIt.toValue = 1
strokeIt.duration = 2
timeLeftShapeLayer.add(strokeIt, forKey: nil)
}
and by UIBezierPathcreate function to draw circle:
func drawTimeLeftShape() {
timeLeftShapeLayer.path = UIBezierPath(
arcCenter: CGPoint(x: myView.frame.midX , y: myView.frame.midY),
radius: myView.frame.width / 2,
startAngle: -90.degreesToRadians,
endAngle: 270.degreesToRadians,
clockwise: true
).cgPath
timeLeftShapeLayer.strokeColor = UIColor.red.cgColor
timeLeftShapeLayer.fillColor = UIColor.clear.cgColor
timeLeftShapeLayer.lineWidth = 1
timeLeftShapeLayer.strokeEnd = 0.0
view.layer.addSublayer(timeLeftShapeLayer)
}

Related

Circular Progress Bar with CAShapeLayer and CABasicAnimation

I am trying to make Circular Progress View using the idea from here
This is my code for the CircleView:
#import "CircleView.h"
#interface CircleView()
#property (nonatomic, strong) CAShapeLayer *circleLayer;
#end
#implementation CircleView
-(instancetype)initWithFrame:(CGRect)frame {
self = [super initWithFrame:frame];
if (self) {
self.backgroundColor = UIColor.clearColor;
UIBezierPath *circlePath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(self.frame.size.width / 2., self.frame.size.height / 2.0) radius: (self.frame.size.width - 10) / 2 startAngle:0.0 endAngle:M_PI * 2 clockwise:YES];
CAShapeLayer *downLayer = [[CAShapeLayer alloc] init];
downLayer.path = circlePath.CGPath;
downLayer.fillColor = UIColor.clearColor.CGColor;
downLayer.strokeColor = UIColor.lightGrayColor.CGColor;
downLayer.lineWidth = 10.0;
downLayer.strokeEnd = 1.0;
self.circleLayer = [[CAShapeLayer alloc] init];
self.circleLayer.path = circlePath.CGPath;
self.circleLayer.fillColor = UIColor.clearColor.CGColor;
self.circleLayer.strokeColor = UIColor.redColor.CGColor;
self.circleLayer.lineWidth = 10.0;
self.circleLayer.strokeEnd = 0.0;
[self.layer addSublayer:downLayer];
[self.layer addSublayer: self.circleLayer];
}
return self;
}
-(void)animateCircle:(NSTimeInterval)duration fromPart:(CGFloat)fromPart toPart: (CGFloat)toPart {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = duration;
animation.fromValue = #(fromPart);
animation.toValue = #(toPart);
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
self.circleLayer.strokeEnd = toPart;
[self.circleLayer addAnimation:animation forKey:#"animateCircle"];
}
#end
The problem is when I try to make this animation more than once. Then it shows only the animation from the last call and as if the previous calls have already finished animating. For example if I make the following two calls:
[self.circleView animateCircle:1 fromPart:0.0 toPart:0.25];
[self.circleView animateCircle:1 fromPart:0.25 toPart:0.5];
It will show only animating the colouring from 0.25 percentage of the circle to 0.5 percentage of the circle. Can I change that? I need to show all animations one after another. (Need to show the progress of a download)
You need to remove these 2 below lines and it will work as expected.
animation.fromValue = #(fromPart);
animation.toValue = #(toPart);
Because you use CircleView to display progress of a download so your method don't need fromPart value when animating circle. You can update animateCircle method like below code.
-(void)animateCircle:(NSTimeInterval)duration toPart: (CGFloat)toPart {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = duration;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
self.circleLayer.strokeEnd = toPart;
[self.circleLayer addAnimation:animation forKey:#"animateCircle"];
}
Result

How to Animate a Path Forwards and Backwards?

I have animated a UIBezier Path to go around a circle and change its stroke color whilst doing so. When it goes all the way around, I would like it go all the way around in the opposite direction.
Here is the code that makes it go forwards:
[CATransaction begin];
{
[CATransaction setAnimationDuration: 3.0];//Dynamic Duration
[CATransaction setCompletionBlock:^{
[tutorialCircle removeFromSuperlayer];
tutorialCircle.opacity = 0.0;
strokePart.opacity = 0.0;
[strokePart removeFromSuperlayer];
}];
const double portion = 1.0 / ((double)3);
for (NSUInteger i = 0; i < 3; i++)
{
strokePart = [[CAShapeLayer alloc] init];
strokePart.fillColor = [[UIColor clearColor] CGColor];
strokePart.frame = tutorialCircle.bounds;
strokePart.path = tutorialCircle.path;
strokePart.lineCap = tutorialCircle.lineCap;
strokePart.lineWidth = tutorialCircle.lineWidth;
if (i == 0) {
strokePart.strokeColor = [UIColor greenColor].CGColor;
}
else if (i == 1) {
strokePart.strokeColor = [UIColor orangeColor].CGColor;
}
else if (i == 2) {
strokePart.strokeColor = [UIColor redColor].CGColor;
}
strokePart.strokeStart = i * portion;
strokePart.strokeEnd = (i + 1) * portion;
[tutorialCircle addSublayer: strokePart];
animation = [CAKeyframeAnimation animationWithKeyPath: #"strokeEnd"];
NSArray* times = #[ #(0.0), // Note: This works because both the times and the stroke start/end are on scales of 0..1
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(1.0) ];
NSArray* values = #[ #(strokePart.strokeStart),
#(strokePart.strokeStart),
#(strokePart.strokeEnd),
#(strokePart.strokeEnd) ];
animation.keyTimes = times;
animation.values = values;
animation.removedOnCompletion = YES;
animation.fillMode = kCAFillModeForwards;
[animation setDelegate:self];
[strokePart addAnimation: animation forKey: #"whatever"];
}
}
[CATransaction commit];
So in my animationDidStop delegate method for the first animation, I know I'd be calling the second animation to go backwards, however, I am struggling to create the animation for this to go backwards.
Have you tried?
animation.autoreverses = YES

How do I smoothly update the toValue/end point of an animation in Core Animation?

I'm trying to emulate the effect Apple has as a progress indicator when downloading an app in iOS 7, somewhat of a pie chart:
My code's coming along pretty well. I can give it a value to go to and it will update to that.
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
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;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
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 {
if (animated) {
self.innerPie.hidden = NO;
self.innerPie.strokeEnd = progress;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
pathAnimation.duration = 0.5;
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
pathAnimation.toValue = [NSNumber numberWithFloat:progress];
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEnd"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.hidden = NO;
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
self.progress = progress;
}
My issue lies in updating the progress while in the middle of an animation. For example, if I'm downloading a file and the progress jumps from 0.1 to 0.2 (10% to 20%), I want to animate that. Say I give it a 0.5 second animation. What if, 0.2 seconds into the 0.5 second animation it jumps to 0.4 (40%)?
With my current code, it jumps to where it should have ended (when it should have animated) in the first animation (20%) and then starts animating again toward 40%.
What I'd like is for it to almost update the toValue to 0.4. At least that's what I think I want. Conceptually I'd like it to continue the animation toward the new value without interrupting the previous value. Smoothness, to put it simply.
How would I accomplish this?
I know there's libraries to do this. I want to do it myself for learning purposes.
You should replace
pathAnimation.fromValue = [NSNumber numberWithFloat:self.progress];
with :
pathAnimation.fromValue = [NSNumber numberWithFloat:[self.innerPie.presentationLayer strokeEnd]];
Initially, [self.innerPie.presentationLayer strokeEnd]]will be 1, so you will need to set the initial value to 0. Add these 2 lines to place where you create your 'innerPie' :
self.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
I've tested and it works.

Why when I set a low duration value for my CABasicAnimation does it jump?

Sample Project: http://cl.ly/1W3V3b0D2001
I'm using CABasicAnimation to create a progress indicator that is like a pie chart. Similar to the iOS 7 app download animation:
The animation is set up as follows:
- (void)drawRect:(CGRect)rect
{
[super drawRect:rect];
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;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
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.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
}
The animation is triggered by setting the progress of the view:
- (void)setProgress:(CGFloat)progress animated:(BOOL)animated {
self.progress = progress;
if (animated) {
CGFloat totalDurationForFullCircleAnimation = 0.25;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
self.innerPie.strokeEnd = progress;
pathAnimation.delegate = self;
pathAnimation.fromValue = #([self.innerPie.presentationLayer strokeEnd]);
pathAnimation.toValue = #(progress);
pathAnimation.duration = totalDurationForFullCircleAnimation * ([pathAnimation.toValue floatValue] - [pathAnimation.fromValue floatValue]);
[self.innerPie addAnimation:pathAnimation forKey:#"strokeEndAnimation"];
}
else {
[CATransaction setDisableActions:YES];
[CATransaction begin];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
}
}
However, in cases where I set the progress to something small, such as 0.25, there's a jump in the animation. It goes a little forward clockwise, jumps back, then keeps going forward as normal. It's worth nothing that this does not happen if the duration or progress is set higher.
How do I stop the jump? This code works well in every case except when the progress is very low. What am I doing wrong?
Ah! We should have seen this earlier. The problem is this line in your if (animated) block:
self.innerPie.strokeEnd = progress;
Since innerPie is a Core Animation layer, this causes an implicit animation (most property changes do). This animation is fighting with your own animation. You can prevent this from happening by disabling implicit animation while setting the strokeEnd:
[CATransaction begin];
[CATransaction setDisableActions:YES];
self.innerPie.strokeEnd = progress;
[CATransaction commit];
(Note how setDisableActions: is within the begin/commit.)
Alternatively, you can remove your own animation and just use the automatic one, using something like setAnimationDuration: to change its length.
Original Suggestions:
My guess is that your drawRect: is being called, which resets your strokeEnd value. Those layers should probably not be set up in drawRect: anyway. Try moving that setup to an init method or didMoveToWindow: or similar.
If that's not effective, I would suggest adding log statements to track the value of progress and [self.innerPie.presentationLayer strokeEnd] each time the method is called; perhaps they're not doing what you expect.
Why are you using drawRect:? There should be no drawRect: method in your class.
You should not be using drawRect if you are also using self.layer. Use one or the other, never both.
Change it to something like:
- (id)initWithFrame:(CGRect)frame
{
if (!(self = [super initWithFrame:frame])
return nil;
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;
self.innerPie = [CAShapeLayer layer];
inset = radius/2;
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.innerPie.strokeStart = 0;
self.innerPie.strokeEnd = 0;
[self.layer addSublayer:ring];
[self.layer addSublayer:self.innerPie];
self.progress = 0.0;
return self;
}
When travelling within same distance, the smaller the duration, the higher the speed. When you set the duration too small, the speed will be very high, and that's why it looks like jumping.

CABasicAnimation lags (iOS5)

i got some simple animation that works perfect on iOS 6, but on iOS5 its lagging and often freezes screen without any reason
i need help really, because i dont have any ideas what could be wrong here. By the way i tried to turn off each of the animations, and looks like the problem is in "transform" animation
here is the code:
-(void)drawDotAtPointAnimated:(CGPoint)p withRadius:(CGFloat)radius andColor:(UIColor *)color
{
CGRect circleFrame = CGRectMake(p.x - radius*self.scale, p.y - radius*self.scale, radius*2*self.scale, radius*2*self.scale);
CGPoint circleAnchor = CGPointMake(0.5, 0.5);
NSLog(#"animating dot with x = %f and y = %f", p.x, p.y);
NSLog(#"=====mid x = %f, mid y = %f", CGRectGetMidX(circleFrame), CGRectGetMidY(circleFrame));
NSLog(#"=====max x = %f, max y = %f", CGRectGetMaxX(circleFrame), CGRectGetMaxY(circleFrame));
self.shapeLayer = [CAShapeLayer layer];
UIBezierPath *path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, circleFrame.size.width, circleFrame.size.height)];
self.shapeLayer.path = path.CGPath;
self.shapeLayer.anchorPoint = circleAnchor;
self.shapeLayer.frame = circleFrame;
self.shapeLayer.fillColor = color.CGColor;
[self.layer addSublayer:self.shapeLayer];
CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.0, 0.0, 0)];
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(4.0, 4.0, 1.0)];
if(self.mode == GameModeOffline) {
animation.repeatCount = OFFLINE_GAME_DOT_ANIMATION_REPEAT_COUNT;
} else {
animation.repeatCount = ONLINE_GAME_DOT_ANIMATION_REPEAT_COUNT;
}
animation.duration = 1;
animation.delegate = self;
[self.shapeLayer addAnimation:animation forKey:#"transform"];
CABasicAnimation* fadeAnim = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnim.fromValue = [NSNumber numberWithFloat:1.0];
fadeAnim.toValue = [NSNumber numberWithFloat:0.0];
fadeAnim.duration = 1.0;
if(self.mode == GameModeOffline) {
fadeAnim.repeatCount = OFFLINE_GAME_DOT_ANIMATION_REPEAT_COUNT;
} else {
fadeAnim.repeatCount = ONLINE_GAME_DOT_ANIMATION_REPEAT_COUNT;
}
[self.shapeLayer addAnimation:fadeAnim forKey:#"opacity"];
self.shapeLayer.transform = CATransform3DMakeScale(4.0, 4.0, 1);
self.shapeLayer.opacity = 0.0;
any help would be appreciated
Scale by zero at start? Don't you want to scale by {1, 1, 1} ? (ie. identity transform)
(It seems this has simply solved the problem, so I'm posting my comment as an answer).

Resources