iOS "CABasicAnimation" brings component to the original position after animation complete - iphone

I'm animating UIImageView using iOS core animation "CABasicAnimation", all works fine for me but the only problem is when i animate to the position, after completeing animation it comes back to original position where it was. how can i overcome this? i need to keep UIImageView in moved position.
NOTE : I've seen few questions with success answers regarding this, but i have no idea why mine is not working like they say.
After rotating a CALayer using CABasicAnimation the layer jumps back to it's unrotated position
Here is my sample code,
CGPoint endPt = CGPointMake(160, 53);
CABasicAnimation *anim5 = [CABasicAnimation animationWithKeyPath:#"position"];
[anim5 setBeginTime:CACurrentMediaTime()+0.4];
anim5.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
anim5.fromValue = [NSValue valueWithCGPoint:imgRef.layer.position];
anim5.toValue = [NSValue valueWithCGPoint:endPt];
anim5.duration = 1.5;
anim5.speed = 2;
[imgRef.layer addAnimation:anim5 forKey:#"position"];
//this is what other answers ask to do
[anim5 setFillMode:kCAFillModeForwards];
[anim5 setRemovedOnCompletion:NO];
BTW [imgRef.layer setPosition:CGPointMake(160, 53)]; won't help me since i'm delaying animation with 4 milliseconds.

The root cause is that the animation just transitions the property between two values, it doesn't actually change the ending value. You need to change the ending value when the animation completes, there are three ways to do that.
1) Use the delegate property on the CAAnimation superclass to be notified of when the animation completes. At that point you can set the property to it's end value. See: https://developer.apple.com/library/mac/#documentation/GraphicsImaging/Reference/CAAnimation_class/Introduction/Introduction.html#//apple_ref/occ/cl/CAAnimation The animationDidStop:finished: is the method you'll need to implement on the delegate.
2) Set a completion block on the surrounding CATransaction. You'll need to manually start the CATransaction rather than having CABasicAnimation do that automatically for you. See: Objective-C - CABasicAnimation applying changes after animation?
3) See OMZ's comment below...

The right answer is to set the layer's position property, but as you've pointed out, it makes it more difficult because you're wanting a 0.4 second delay prior to the position change. Is there any reason you couldn't perform the delay first and then do the animation? Something like this:
- (IBAction)didTapMove:(id)sender
{
[self performSelector:#selector(animate) withObject:nil afterDelay:0.4];
}
- (void)animate
{
CGPoint endPt = CGPointMake(160, 53);
CABasicAnimation *anim5 = [CABasicAnimation animationWithKeyPath:#"position"];
anim5.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
anim5.fromValue = [NSValue valueWithCGPoint:_imageView.layer.position];
anim5.toValue = [NSValue valueWithCGPoint:endPt];
anim5.duration = 1.5;
anim5.speed = 2;
[_imageView.layer addAnimation:anim5 forKey:#"position"];
[_imageView.layer setPosition:CGPointMake(160, 53)];
}
Noticed I've removed your begin time from the animation since the delay is occurring in the perform selector call.

Related

Why can't I set the opacity of a layer after a CABasicAnimation finished?

I have a view with a sublayer called specialLayer. Implicit animations for opacity are disabled like this:
self.specialLayer.actions = [NSDictionary dictionaryWithObjectsAndKeys:
[NSNull null], #"opacity", nil];
There are only two methods which access this sublayer.
- (void)touchDown {
self.specialLayer.opacity = 0.25f;
}
- (void)touchUp {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
animation.fromValue = [NSNumber numberWithFloat:self.specialLayer.opacity];
animation.toValue = [NSNumber numberWithFloat:1.0];
animation.duration = 0.5;
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[self.specialLayer addAnimation:animation forKey:#"opacity"];
}
This happens:
1) User presses a button down (holding) and -touchDown gets called. specialLayer's opacity instantly becomes 0.25
2) User releases button and -touchUp gets called. specialLayer's opacity animates back to 1.0
This sequence only works ONCE!
3) User presses a button down again (holding) and -touchDown gets called. But specialLayer's opacity does not change on screen! Nothing happens even though self.specialLayer.opacity = 0.25f; gets called. Opacity appears to remain 1.0f.
4) User lifts finger up and -touchUp gets called. specialLayer's opacity JUMPS to 0.25 and from there animates to 1.0f.
I checked with NSLog to make sure the methods get called in this order. They do.
When I uncomment the animation code and set the opacity directly to 1.0, subsequent direct opacity changes are effective immediately. It is definitely the CABasicAnimation which causes problems. Removing the code that disables implicit animations has no effect on this problem.
I can't for the life of me figure out what is happening here. Why does CALayer not reflect the opacity value I set to it some time later after I added a CABasicAnimation for opacity? Is this a bug?
What am I doing wrong?
You set the animation's removedOnCompletion to NO. This means that when the animation completes, it still stays on the layer and still controls the value of opacity for the purposes of rendering. When you call this code a second time, the new animation replaces the old, which causes the jump (as it's now using the newly-set 0.25f as the start point).
The solution is to just stop setting removedOnCompletion to NO. Instead, you should just set the actual opacity of the layer to the same value as the endpoint of the animation., at the same time as you add the animation.
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"opacity"];
// you can omit fromValue since you're setting it to the layer's opacity
// by omitting fromValue, it uses the current value instead. Just remember
// to not change layer.opacity until after you add the animation to the layer
animation.toValue = [NSNumber numberWithFloat:1];
animation.duration = 0.5;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
[layer addAnimation:animation forKey:#"opacity"];
layer.opacity = 1; // or layer.opacity = [animation.toValue floatValue];
The reason for this is because of the way layer rendering works. When CoreAnimation renders a layer tree to the screen, it copies the model tree and produces what's called the presentation tree. It does this by copying all the layers, and then applying all the animations. So it goes over each animation on the layer, plugs the time into the timing function, plugs the resulting progress into the interpolation between the from/to values, and applies that final value back onto the presentation layer's property.
So when you leave an animation on the layer permanently, this animation will always override whatever the actual model value is for the key. It doesn't matter how many times you change layer.opacity, the animation that controls opacity will always overwrite the value for presentation purposes.
This same reason is also why you can set the layer.opacity to whatever you want after adding the animation, because the animation will take precedence. Only when the animation is removed (which happens by default when the animation completes) will the model value for that property get used again.

Stop CABasicAnimation at specific point

I'm using a rotation animation created with CABasicAnimation. It rotates a UIView over 2 seconds. But I need to be able to stop it when the UIView is touched. If I remove the animation the view is in the same position as before the animation started.
Here's my animation code:
float duration = 2.0;
float rotationAngle = rotationDirection * ang * speed * duration;
//rotationAngle = 3*(2*M_PI);//(double)rotationAngle % (double)(2*M_PI) ;
CABasicAnimation* rotationAnimation;
rotationAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.z"];
rotationAnimation.toValue = [NSNumber numberWithFloat: rotationAngle ];
rotationAnimation.duration = duration;
rotationAnimation.cumulative = YES;
rotationAnimation.removedOnCompletion = NO;
rotationAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut];
rotationAnimation.fillMode = kCAFillModeForwards;
rotationAnimation.delegate = self;
[self.view.layer addAnimation:rotationAnimation forKey:#"rotationAnimation"];
How can I stop the UIView's rotation right where it is, when it's touched? I know how to manage the touch part, but I can't figure out how to stop the view at the animation's current angle.
Solution:
I solved the problem by getting the angle of the presentation layer, removing the animation and setting the view's transform. Here's the code:
[self.view.layer removeAllAnimations];
CALayer* presentLayer = self.view.layer.presentationLayer;
float currentAngle = [(NSNumber *)[presentLayer valueForKeyPath:#"transform.rotation.z"] floatValue];
self.view.transform = CGAffineTransformMakeRotation(currentAngle);
Good question! For this, it's helpful to know the Core Animation architecture.
If you check out the diagram in the Core Animation Programming Guide that describes the Core Animation Rendering Architecture, you can see that there's three trees.
You have the model tree. That's where you set the values of what you want to happen. Then there's the presentation tree. That's what is pretty much happening as far as the runtime is concerned. Then, finally is the render tree. That's what the user sees.
In your case, you want to query the values of the presentation tree.
It's easy to do. For the view that you have attached the animation, get the layer and for that layer, query the presentationLayer's values. For example:
CATransform3D myTransform = [(CALayer*)[self.view.layer presentationLayer] transform];
There's no way to "pause" an animation mid flow. All you can do is query the values, remove it, and then re-create it again from where you left off.
It's a bit of a pain!
Have a look at some of my other posts where I go into this in a bit more detail, e.g.
Restoring animation where it left off when app resumes from background
Don't forget also that when you add an animation to a view's layer, you aren't actually changing the underlying view's properties. So what happens? We'll you get weird effects where the animation stops and you see the view in it's original position.
That's where you need to use the CAAnimation delegates. Have a look at my answer to this post where I cover this:
CABasicAnimation rotate returns to original position
You need to set the rotation to the rotation of the presentationLayer and then remove the animation from the layer. You can read about the presentation layer in my blog post about Hit testing animating layers.
The code to set the final rotation would be something like:
self.view.layer.transform = [(CALayer*)[self.view.layer presentationLayer] transform];
[self.view.layer removeAnimationForKey:#"rotationAnimation"];

CABasicAnimation disabling any touch activity within animated layer

- (void)fadeOutStuff
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"opacity"];
anim.delegate = self;
anim.duration = 0.25f;
anim.removedOnCompletion = NO;
anim.fillMode = kCAFillModeForwards;
anim.fromValue = [NSNumber numberWithFloat:1.0f];
anim.fromValue = [NSNumber numberWithFloat:0.0f];
[self.searchList.layer addAnimation:anim forKey:#"animationOpacity"];
}
I have this code to simply animate an object in and out, and after the animation is complete, the layer is not touchable. Is the animation process setting the layer down a level/index? I can still touch elements behind the animated layer, but not the animated layer itself. Am I missing a setting? Am I going about animation the wrong way, based on this code?
I figure this out, and the property fillMode is mainly responsible for disabling touch events in animated objects. Do not use it if whatever you're animating needs to handle touch events. Basically, the workaround I used was removed the fillMode property, and set the final stage of the animation manually after adding the animation to the layer
[self.searchList.layer addAnimation:anim forKey:#"animationOpacity"];
[self.searchList.layer setValue:[NSNumber numberWithFloat:endingOpacityValue forKey:#"opacity"]];
If I remember correctly, hidden objects won't receive touches. I don't know if the applies if it's just opacity set to zero, but try seeing what happens if you do it to just 0.01f instead of all the way to 0.
By the way, I don't know if it's a typo or not, but you set anim.fromValue twice, and you don't set anim.toValue.

CABasicAnimation not animating my property

I've been trying to understand what is wrong with my animation and I still haven't figure it out. I think it should be really straight forward, but there is probably something I'm missing, even after reading lot of examples and documentation.
My problem comes originally form the fact that on the iPhone, you cannot resize layers automatically (with the view). The documentation says otherwise but there is no autoresizeMask for the layer in the SDKs. So I decided to make a little workaround and animate the layer myself.
I've got this simple piece of code that should do a simple resize animation. The values are good and I even set the delegate in order to trace if the anim start/end.
// I've got a property named layerImage (which is a CALayer)
- (void)animateTestWithFrame:(CGRect)value {
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"layerImage.frame"];
animation.duration = 1;
animation.fromValue = [NSValue valueWithCGRect:self.frame];
animation.toValue = [NSValue valueWithCGRect:value];
animation.removedOnCompletion = YES;
animation.delegate = self;
[self.layer addAnimation:animation forKey:#"layerImage.frame"];
}
So, any ideas? (This view that contains the code is the subview of a subview of the window if that could make a difference)
--- EDIT ---
It seems that frame is not animatable via CABasicAnimation and the named property "frame". When using bounds, I've got some strange result, but at least I'm getting something. Will continue investigating on this.
So it's good that you've figured things out here, but your answer to your own question has some inaccuracies. Let me correct a few things:
The frame property can be animated--just not with explicit animation. If you do the following to a layer other than the root layer of a view, the frame will animate just fine:
[CATransaction begin];
[CATransaction setAnimationDuration:2.0f];
[animationLayer setFrame:CGRectMake(100.0f, 100.0f, 100.0f, 100.0f)];
[CATransaction commit];
Remember that setting a property on a layer will animate that property change by default. In fact you have to disable animations on a property change if you don't want it to animate. (Of course this is only true if you are animating a layer other than the root layer of a view.) You are correct in animating both position and bounds if you need to use an explicit animation.
You can animate the frame on a UIView using implicit animation:
[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:3.0f];
[[self view] setFrame:CGRectMake(45.0f, 45.0f, 100.0f, 100.0f)];
[UIView commitAnimations];
This will animate from the view's current frame (bounds and position) to x = 45.0, y = 45.0, w = 100.0, h = 100.0.
It seems you may also be misunderstanding the difference between an animation and a layer. You add animations to layers, but adding an animation to a layer does not automatically set the property that you're animating.
CALayers are model objects. They contain information about the layer that eventually gets rendered to screen. You must set a layer's property if you want that property to actually have that value. If you simply animate the property, it will only be a visual representation and not actual--which is to say this is why the value snaps back to the original value of the layer because you never actually changed it.
Which leads me to the next point. You said:
Use "animation.removedOnCompletion =
NO; animation.fillMode =
kCAFillModeForwards;" to ensure that
the values are not reseted at the end
of the animation
This is not exactly right. These two values simply cause the animation to remain at it's final position visually, however, the layer's actual values have not changed. They are still the exact same values they were when you started the animation. In general (with a few exceptions) you don't want to use these two parameters because they are visual only. What you want is to actually set the layer value for the property you're animating.
Say, for example, that you want to animate the position of your layer using an explicit animation. Here is the code you want:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
[animation setFromValue:[NSValue valueWithCGPoint:CGPointMake(70.0f, 70.0f)]];
[animation setToValue:[NSValue valueWithCGPoint:CGPointMake(150.0f, 150.0f)]];
[animation setDuration:2.0f];
// Actually set the position on the *layer* that you want it to be
// when the animation finishes.
[animationLayer setPosition:CGPointMake(150.0f, 150.0f)];
// Add the animation for the position key to ensure that you
// override the animation for position changes with your animation.
[animationLayer addAnimation:animation forKey:#"position"];
You may also want to consider animation grouping. With an animation group, you can group several animations together and then control how they relate to each other. In your case the duration for your bounds and position animations are the same and so what you are trying to do will work fine without a group, but if you wanted to offset the start of the animations, for example you didn't want the position animation to start until a second or two into the frame animation, you could stagger them by setting the beginTime value of the position animation.
Finally, I would be curious to know why you couldn't use the implicit animations available on UIView. I use these in the vast majority of the Core Animation code I write and can't see why this wouldn't work for your situation.
Best regards.
The key path should only be the key path of the property, not the name of the object as well.
Use this
[CABasicAnimation animationWithKeyPath:#"frame"]
instead of this
[CABasicAnimation animationWithKeyPath:#"layerImage.frame"]
And just BTW, when you add animation to a layer, the key doen't mean the key property to animate. Just the key (name) that you want this animation to have (this refers to the last line your code)
So, the solution was to animate the #"bounds" and the #"position" of the layer because frame is not animatable on iPhone. It took me some time to understand that the position was the center of the layer and the resize of the bounds was extending from the center, but that was the easy part.
So, what I did in resume was:
In the setFrame, create 2 animations with the bounds and position property.
Use "animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards;" to ensure that the values are not reseted at the end of the animation
Register the delegate to self in order to implements "animationDidStop:finished:". It seems that you still need to set the values: "layerImage.bounds = [animation.toValue CGRectValue]; layerImage.position = [animation.toValue CGPointValue];".
I wasn't able to use the UIView animation system directly because it wasn't doing what I wanted on the layers.
Thanks tadej5553 for pointing me out the layer problem I had with the "addAnimation". So here is the code for those who would like to see what it looks like.
- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
CABasicAnimation *animation = (CABasicAnimation*)anim;
if ([animation.keyPath isEqualToString:#"bounds"]) {
layerImage.bounds = [animation.toValue CGRectValue];
} else if ([animation.keyPath isEqualToString:#"position"]) {
layerImage.position = [animation.toValue CGPointValue];
}
}
- (void)setFrame:(CGRect)value {
CGRect bounds = CGRectMake(0, 0, value.size.width, value.size.height);
if ([UIView isAnimationStarted]) {
// animate the bounds
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"bounds"];
animation.duration = [UIView animationDuration];
animation.fromValue = [NSValue valueWithCGRect:layerImage.bounds];
animation.toValue = [NSValue valueWithCGRect:bounds];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [UIView animationFunction];
animation.delegate = self;
[layerImage addAnimation:animation forKey:#"BoundsAnimation"];
// animate the position so it stays at 0, 0 of the frame.
animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.duration = [UIView animationDuration];
animation.fromValue = [NSValue valueWithCGPoint:layerImage.position];
animation.toValue = [NSValue valueWithCGPoint:CGPointMake(bounds.size.width / 2, bounds.size.height / 2)];
animation.removedOnCompletion = NO;
animation.fillMode = kCAFillModeForwards;
animation.timingFunction = [UIView animationFunction];
animation.delegate = self;
[layerImage addAnimation:animation forKey:#"PositionAnimation"];
} else {
layerImage.frame = bounds;
}
[super setFrame:value];
}

How come my Core Animation transformation always returns to it's start-state?

I am trying to perform some kind of animation of a layer in my iPhone application. It does not matter what I do I always get the same results: after the animation is done it jerks back into it's original position. Even though I set removedOnCompletion to false there is no difference.
What am I missing here?
Thanks in advance!
EDIT: Really need help with this one guys. I am creating animations with CAKeyframeAnimation and CABasicAnimation objects, then adding them to a CAAnimationGroup which I in turn att to the layer. The animation works as predicted except that it always snaps back to it's original state. This is the case even though I set removedOnCompletion = NO; on all animation-objects and the animation group.
Some one please point me in the right direction! I you live in the Stockholm area I will buy you a coffe. =) New code posted below:
CABasicAnimation *leveloutLeafAnimation = [CABasicAnimation animationWithKeyPath:#"transform"];
leveloutLeafAnimation.removedOnCompletion = NO;
leveloutLeafAnimation.duration = 1.0;
leveloutLeafAnimation.repeatDuration = 20;
CATransform3D transformLeafToRotation = CATransform3DMakeRotation(0.0, 0.0, 0.0, 1);
CATransform3D transformLeafFromRotation = CATransform3DMakeRotation([self _degreesToRadians:270.0], 0.0, 0.0, 1);
leveloutLeafAnimation.fromValue = [NSValue valueWithCATransform3D:transformLeafFromRotation];
leveloutLeafAnimation.toValue = [NSValue valueWithCATransform3D:transformLeafToRotation];
//Create an animation group to combine the animations.
CAAnimationGroup *theAnimationGroup = [CAAnimationGroup animation];
//The animationgroup conf.
theAnimationGroup.delegate = self;
theAnimationGroup.duration = animationDuration;
theAnimationGroup.removedOnCompletion = NO;
theAnimationGroup.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
theAnimationGroup.animations = [NSArray arrayWithObjects:leveloutLeafAnimation, leafMoveAnimation, nil];
// Add the animation group to the leaf layer.
[leafViewLayer addAnimation:theAnimationGroup forKey:#"animatLeafFalling"];
Not sure if this is helpful, but this guy seems like he was having a similar problem.
You might have to set the transform property of the layer while the animation is running to transformLeafToRotation.
Good luck!