Glitches when queuing CAAnimations - ios

I have a CAShapeLayer where I animate a circle. The animation is to first "undraw" the circle clockwise and then redraw the circle clockwise. Sort of a "rotating circle". Another way to put it: Move path stroke end point to start, then move the start point to the end.
The animation itself works, but it produces glitches now and then. It manifests in a short glimpse of the entire circle when it is supposed to be "undrawn".
Why does this occur and how can you fix it?
Thanks,
// Shape creation
layer.path = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, self.width - 2 * OUTER_BORDER_WIDTH, self.width - 2* OUTER_BORDER_WIDTH)].CGPath;
// Animation queuing
-(void) applyNextAnimation
{
CABasicAnimation* animation;
if (self.animatingOpening)
{
animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = NO;
}
else
{
animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = YES;
}
animation.duration = 1.0f;
animation.autoreverses = NO;
animation.delegate = self;
animation.removedOnCompletion = YES;
[self.outerCircleLayer addAnimation:animation forKey:#"stroke"];
}
// Animation stop callback
-(void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if (self.isAnimating)
{
[self applyNextAnimation];
}
}

It blinks becuase you are not setting the corresponding property on the layer. So when the animation completes, the layer's model is still in the pre-animated state and that is what you see momentarily between the two animations.
This will get you towards what you want...
if (self.animatingOpening)
{
self.outerCircleLayer.strokeStart = 0.0;
animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = NO;
}
else
{
self.outerCircleLayer.strokeStart = 1.0;
animation = [CABasicAnimation animationWithKeyPath:#"strokeStart"];
animation.fromValue = [NSNumber numberWithFloat:0.0f];
animation.toValue = [NSNumber numberWithFloat:1.0f];
self.animatingOpening = YES;
}
animation.duration = 1.0f;
animation.autoreverses = NO;
that almost works, but you will notice a more subtle glitch as you transition from the undrawn state to start animating the drawing state. The beginning of the circle has a small reverse animation as it starts. This is an implicit animation triggered by setting strokeStart from 1.0 to 0.0: which you need to get rid of so that all of the animations effects are under your control. You can achieve that most simply by setting disableActions to YES on CATransaction:
[CATransaction setDisableActions:YES];
( add it just above if (self.animatingOpening))

Related

Animation is not working as expected

I implemented corePlot in xcode and I'm using the pie chart. I'm trying to create a 3d flip animation while the chart reloads. Here is the code:
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:0.5];
scaleAnimation.duration = 1.0f;
scaleAnimation.removedOnCompletion = NO;
[self.pieChart addAnimation:scaleAnimation forKey:#"scale"];
[self.pieChart reloadData];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
animation.fromValue = [NSNumber numberWithFloat:0.5];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.duration = 1.0f;
[self.pieChart addAnimation:animation forKey:#"scale"];
I didn't get desirable effects. I think what's happening is, both animations are happening at once. (Though I'm not sure that is what's happening.)
Also, is it possible to add z-depth? If so, how?
Update
I tried the follwoing:
CABasicAnimation *currentAnition = (CABasicAnimation *)anim;
if (currentAnition == self.scaleAnimation {...}
And it didn't work.
CAAnimation is KVC compliant, so we can store whether you're starting or finishing for lookup in the delegate methods:
{
CABasicAnimation *scaleAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
[scaleAnimation setValue:#(YES) forKey:#"scaleAnimation"];
// set the delegate
scaleAnimation.delegate = self;
scaleAnimation.fromValue = [NSNumber numberWithFloat:1.0];
scaleAnimation.toValue = [NSNumber numberWithFloat:0.5];
scaleAnimation.duration = 1.0f;
scaleAnimation.removedOnCompletion = NO;
[self.pieChart addAnimation:scaleAnimation forKey:#"scale"];
[self.pieChart reloadData];
}
- (void)runSecondAnimation
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.scale.x"];
[animation setValue:#(YES) forKey:#"secondAnimation"];
animation.delegate = self;
animation.fromValue = [NSNumber numberWithFloat:0.5];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.duration = 1.0f;
[self.pieChart addAnimation:animation forKey:#"scale"];
}
/* Called when the animation either completes its active duration or
* is removed from the object it is attached to (i.e. the layer). 'flag'
* is true if the animation reached the end of its active duration
* without being removed. */
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
BOOL scaleAnimation = [[anim valueForKey:#"scaleAnimation"] boolValue];
if (scaleAnimation) {
[self runSecondAnimation];
}
BOOL secondAnimation = [[anim valueForKey:#"secondAnimation"] boolValue];
if (secondAnimation) {
[self runThirdAnimation];
}
}
And remember you have also:
/* Called when the animation begins its active duration. */
- (void)animationDidStart:(CAAnimation *)anim;

IOS, how to remove the layer one time only on CABasicAnimation

i have modified this code to the code as shown below. I am using touch events to trigger the function below to run the animation. The results I am trying to achieve is: every time I touch 1 time, the image layer remove just processes 1 time. The problem I am facing is when I set removedOnCompletion to YES, whatever the layer just feels like it has removed 1 time but it is never removed, so the program is stuck on same layer. If I keep removedOnCompletion as NO, then image layer will auto remove all the layers.
So, how I can make sure that every time the process removes just 1 Image layer? Any help is appreciated.
-(void)animate:(id)sender {
//Dont implicitly animate the delay change
[CATransaction setDisableActions:YES];
//Reset the replicator delays to their origonal values
imageLayer.instanceDelay = X_TIME_DELAY;
//Re-enable the implicit animations
[CATransaction setDisableActions:NO];
//Create the transform matrix for the animation
//Move forward 1000 units along z-axis
CATransform3D t = CATransform3DMakeTranslation( -500, -500, 1000);
//Rotate Pi radians about the axis (0.7, 0.3, 0.0)
t = CATransform3DRotate(t, M_PI, 0.7, 0.3, 0.0);
//Scale the X and Y dimmensions by a factor of 3
t = CATransform3DScale(t, 0.5, 0.5, 1);
//Transform Animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.fromValue = [NSValue valueWithCATransform3D: CATransform3DIdentity];
animation.toValue = [NSValue valueWithCATransform3D: t];
animation.duration = 1.0;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeBoth;
[subLayer addAnimation:animation forKey:#"transform"];
//Opacity Animation
animation = [CABasicAnimation animation];
animation.fromValue = [NSNumber numberWithFloat:1.0];
animation.toValue = [NSNumber numberWithFloat:0.0];
animation.duration = 1.0;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeBoth;
[subLayer addAnimation:animation forKey:#"opacity"];
stuckfile = NO;
}

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.

How do I make a layer maintain the final value of a CABasicAnimation

Consider the following animation:
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.removedOnCompletion = NO;
pathAnimation.delegate = self;
This will essentially animate the drawing of the layer from one end to the next. The problem is that once the animation completes, the strokeEnd property resets back to 0 (where it was initially set). How do I make the final value "stick"?
I have attempted to change this in the animationDidStop delegate method. This mostly works, but can cause a flash of strokeEnd at 0 briefly, even when put inside a CATransaction to disable animations. I have also played with the additive and cumulative properties to no avail. Any suggestions?
You simply set the strokeEnd property to its final value yourself! Like this:
// your current code (stripped of unnecessary stuff)
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1.0;
pathAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
// just add this to it:
theLayer.strokeEnd = 1;
// and now add the animation to theLayer
If strokeEnd were implicitly animatable (so that that line would itself cause animation) we would turn off implicit animation first:
[CATransaction setDisableActions:YES];
theLayer.strokeEnd = 1;
Note that your pathAnimation.removedOnCompletion = NO; is wrong, and so is the other answer's kCAFillModeForwards. Both are based on misunderstandings of how Core Animation works.

What is the maximum duration value (CFTimeInterval) for a CAAnimationGroup?

I have two rotation animations in my CAAnimationGroup, one that starts from zero and another that repeats and autoreverses from that state:
- (void)addWobbleAnimationToView:(UIView *)view amount:(float)amount speed:(float)speed
{
NSMutableArray *anims = [NSMutableArray array];
// initial wobble
CABasicAnimation *startWobble = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
startWobble.toValue = [NSNumber numberWithFloat:-amount];
startWobble.duration = speed/2.0;
startWobble.beginTime = 0;
startWobble.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
[anims addObject:startWobble];
// rest of wobble
CABasicAnimation *wobbleAnim = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
wobbleAnim.fromValue = [NSNumber numberWithFloat:-amount];
wobbleAnim.toValue = [NSNumber numberWithFloat:amount];
wobbleAnim.duration = speed;
wobbleAnim.beginTime = speed/2.0;
wobbleAnim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
wobbleAnim.autoreverses = YES;
wobbleAnim.repeatCount = INFINITY;
[anims addObject:wobbleAnim];
CAAnimationGroup *wobbleGroup = [CAAnimationGroup animation];
wobbleGroup.duration = DBL_MAX; // this stops it from working
wobbleGroup.animations = anims;
[view.layer addAnimation:wobbleGroup forKey:#"wobble"];
}
Since CFTimeInterval is defined as a double, I try setting the duration of the animation group to DBL_MAX, but that stops the animation group from running. However, If I set it to a large number, such as 10000, it runs fine. What is the largest number I can use for a duration of a CAAnimationGroup, to ensure it runs for as near to infinity as possible?
UPDATE: It appears that if I put in a very large value such as DBL_MAX / 4.0 then it freezes for a second, then starts animating. If I put in the value DBL_MAX / 20.0 then the freeze at the beginning is a lot smaller. It seems that having such a large value for the duration is causing it to freeze up. Is there a better way of doing this other than using a very large value for the duration?
I am faced with the exact same issue right now... I hope someone proves me wrong, but the only reasonable way I see to handle this situation is by moving the first animation to a CATransaction, and chaining that with the autoreverse animation using:
[CATransaction setCompletionBlock:block];
It's not ideal, but gets the job done.
Regarding your question about the animations being paused when coming back from background, that's a classic limitation of the CoreAnimation framework, many solutions have been proposed for it. The way I solve it is by simply reseting the animations at a random point of the animation, by randomizing the timeOffset property. The user can't tell exactly what the animation state should be, since the app was in the background. Here is some code that could help (look for the //RANDOMIZE!! comment):
[[NSNotificationCenter defaultCenter] addObserver:self
selector:#selector(startAnimating)
name:UIApplicationWillEnterForegroundNotification
object:[UIApplication sharedApplication]];
...
for (CALayer* layer in _layers)
{
// RANDOMIZE!!!
int index = arc4random()%[levels count];
int level = ...;
CGFloat xOffset = ...;
layer.position = CGPointMake(xOffset, self.bounds.size.height/5.0f + yOffset * level);
CGFloat speed = (1.5f + (arc4random() % 40)/10.f);
CGFloat duration = (int)((self.bounds.size.width - xOffset)/speed);
NSString* keyPath = #"position.x";
CABasicAnimation* anim = [CABasicAnimation animationWithKeyPath:keyPath];
anim.fromValue = #(xOffset);
anim.toValue = #(self.bounds.size.width);
anim.duration = duration;
anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// RANDOMIZE!!
anim.timeOffset = arc4random() % (int) duration;
anim.repeatCount = INFINITY;
[layer removeAnimationForKey:keyPath];
[layer addAnimation:anim forKey:keyPath];
[_animatingLayers addObject:layer];
}
It is much simpler to use a single keyframe animation instead of a group of two separate animations.
- (void)addWobbleAnimationToView:(UIView *)view amount:(CGFloat)amount speed:(NSTimeInterval)speed {
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"transform.rotation.z"];
animation.duration = 2 * speed;
animation.values = #[ #0.0f, #(-amount), #0.0f, #(amount), #0.0f ];
animation.keyTimes = #[ #0.0, #0.25, #0.5, #0.75, #1.0 ];
CAMediaTimingFunction *easeOut =[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
CAMediaTimingFunction *easeIn =[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseIn];
animation.timingFunctions = #[ easeOut, easeIn, easeOut, easeIn ];
animation.repeatCount = HUGE_VALF;
[view.layer addAnimation:animation forKey:animation.keyPath];
}

Resources