I have a problem I don't understand regarding UIViews and Core Animation. Here's what I want to do:
A small view is presented above a bigger view by putting it as one of its subviews.
When I click a button inside this view, the view should minimize and move to a specified CGRect.
Then the view is removed from its superview.
The first time I present, minimize-and-move and remove the view, everything works fine. But when I present the view again, it displays at the modified position (even though it's supposed to be set at the original position by a call to theView.frame = CGRectMake(600.0, 160.0, 339.0, 327.0);), while all the different responder elements (buttons, textviews, etc.) contained in the view act as if they were at the original position. It's like the view and the layer gets dissynchronized by the animation, and I do not know how to get them back in sync.
Having something like self.view.layer.frame = CGRectMake(600.0, 160.0, 339.0, 327.0); does not get anything right.
My minimize-and-move animation code is given below:
[CATransaction flush];
CABasicAnimation *scale, *translateX, *translateY;
CAAnimationGroup *group = [CAAnimationGroup animation];
group.delegate = delegate;
group.duration = duration;
scale = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
translateX = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
translateY = [CABasicAnimation animationWithKeyPath:#"transform.translation.y"];
scale.toValue = [NSNumber numberWithFloat:0.13];
translateX.toValue = [NSNumber numberWithFloat:137.0];
translateY.toValue = [NSNumber numberWithFloat:-290.0];
group.animations = [NSArray arrayWithObjects: scale, translateX, translateY, nil];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[theView.layer addAnimation:group forKey:#"MyAnimation"];
How to get the layer back to the view after the animation?
What happens if you remove these two lines?
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
What you are telling core animation with those lines is that you want it to continue to display in the forward (final) state of the animation. Meanwhile, you didn't actually set the transform on the layer to have the properties you used for the animation. This would make things appear to be out of sync.
Now, the issue you're going to run into is that removing those lines will cause your transforms to revert back to the starting state when the animation has completed. What you need to do is actually set the transforms on the layer in order for them to hold their position when the animation completes.
Another option is to leave the two lines in and then actually explicitly remove the animation from the layer instead of setting the layer frame as you mentioned when you are ready to revert back to the original state. You do this with:
[theView.layer removeAnimationForKey:#"MyAnimation"];
The -removedOnCompletion property told the layer not to remove the animation when it finished. Now you can explicitly remove it and it should revert back.
HTH.
Related
I am using CABasicAnimation to move and resize an image view. I want the image view to be added to the superview, animate, and then be removed from the superview.
In order to achieve that I am listening for delegate call of my CAAnimationGroup, and as soon as it gets called I remove the image view from the superview.
The problem is that sometimes the image blinks in the initial location before being removed from the superview. What's the best way to avoid this behavior?
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil];
animGroup.duration = .5;
animGroup.delegate = self;
[imageView.layer addAnimation:animGroup forKey:nil];
When you add an animation to a layer, the animation does not change the layer's properties. Instead, the system creates a copy of the layer. The original layer is called the model layer, and the duplicate is called the presentation layer. The presentation layer's properties change as the animation progresses, but the model layer's properties stay unchanged.
When you remove the animation, the system destroys the presentation layer, leaving only the model layer, and the model layer's properties then control how the layer is drawn. So if the model layer's properties don't match the final animated values of the presentation layer's properties, the layer will instantly reset to its appearance before the animation.
To fix this, you need to set the model layer's properties to the final values of the animation, and then add the animation to the layer. You want to do it in this order because changing a layer property can add an implicit animation for the property, which would conflict with the animation you want to explicitly add. You want to make sure your explicit animation overrides the implicit animation.
So how do you do all this? The basic recipe looks like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:myLayer.position];
layer.position = newPosition; // HERE I UPDATE THE MODEL LAYER'S PROPERTY
animation.toValue = [NSValue valueWithCGPoint:myLayer.position];
animation.duration = .5;
[myLayer addAnimation:animation forKey:animation.keyPath];
I haven't used an animation group so I don't know exactly what you might need to change. I just add each animation separately to the layer.
I also find it easier to use the +[CATransaction setCompletionBlock:] method to set a completion handler for one or several animations, instead of trying to use an animation's delegate. You set the transaction's completion block, then add the animations:
[CATransaction begin]; {
[CATransaction setCompletionBlock:^{
[self.imageView removeFromSuperview];
}];
[self addPositionAnimation];
[self addScaleAnimation];
[self addOpacityAnimation];
} [CATransaction commit];
CAAnimations are removed automatically when complete. There is a property removedOnCompletion that controls this. You should set that to NO.
Additionally, there is something known as the fillMode which controls the animation's behavior before and after its duration. This is a property declared on CAMediaTiming (which CAAnimation conforms to). You should set this to kCAFillModeForwards.
With both of these changes the animation should persist after it's complete. However, I don't know if you need to change these on the group, or on the individual animations within the group, or both.
Heres an example in Swift that may help someone
It's an animation on a gradient layer. It's animating the .locations property.
The critical point as #robMayoff answer explains fully is that:
Surprisingly, when you do a layer animation, you actually set the final value, first, before you start the animation!
The following is a good example because the animation repeats endlessly.
When the animation repeats endlessly, you will see occasionally a "flash" between animations, if you make the classic mistake of "forgetting to set the value before you animate it!"
var previousLocations: [NSNumber] = []
...
func flexTheColors() { // "flex" the color bands randomly
let oldValues = previousTargetLocations
let newValues = randomLocations()
previousTargetLocations = newValues
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
// and by the way, this is how you endlessly animate:
CATransaction.setCompletionBlock{ [weak self] in
if self == nil { return }
self?.animeFlexColorsEndless()
}
let a = CABasicAnimation(keyPath: "locations")
a.isCumulative = false
a.autoreverses = false
a.isRemovedOnCompletion = true
a.repeatCount = 0
a.fromValue = oldValues
a.toValue = newValues
a.duration = (2.0...4.0).random()
theLayer.add(a, forKey: nil)
CATransaction.commit()
}
The following may help clarify something for new programmers. Note that in my code I do this:
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
// AND NOW ANIMATE:
CATransaction.begin()
...set up the animation...
CATransaction.commit()
however in the code example in the other answer, it's like this:
CATransaction.begin()
...set up the animation...
// IN FACT, ACTUALLY "SET THE VALUES, BEFORE ANIMATING!"
theLayer.locations = newValues
CATransaction.commit()
Regarding the position of the line of code where you "set the values, before animating!" ..
It's actually perfectly OK to have that line actually "inside" the begin-commit lines of code. So long as you do it before the .commit().
I only mention this as it may confuse new animators.
I will start off by explaining that I have seen many questions and answers regarding this type of feature, but I am still having problems implementing it myself. I am using ARC, and am not using auto-layout or storyboard. I define my layouts with constraints in code, so the way I have been trying to implement my animation is a little different. Lastly, this is an iPad application.
To the specific problem at hand, I have a subview that starts off hidden but appears when an action takes place. I would like this subview to use the hidden feature, but slide in and out after it appears and before it is hidden. So far, I have gotten halfway there and am able to get the view to slide in without issue. Below is the code that accomplishes this.
detailView.hidden = NO;
// Perform Animation - Slide In
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = kAnimationTimeout;
animation.removedOnCompletion = NO;
animation.fromValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(800.0, 0.0, 1.0)];
[detailView.layer addAnimation:animation forKey:nil];
However, I have been unsuccessful with trying getting the view to slide out before it is hidden. Below is the code that I added to attempt at completing this feature.
// Perform Animation - Slide Out
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform"];
animation.duration = kAnimationTimeout;
animation.removedOnCompletion = NO;
animation.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeTranslation(-800.0, 0.0, 1.0)];
[detailView.layer addAnimation:animation forKey:nil];
detailView.hidden = YES;
The result I get is that the view simply disappears like it was hidden, which it always did. Do I need to remove one animation that is added to a view before I add a different animation? Or is my CATransform3DMakeTranslation incorrectly defined?
Turns out detailView.hidden was being called before the animation started. I resolved this by adding a selector with a delay that contained a method to hide my view.
[self performSelector:#selector(hideDetailView) withObject:nil afterDelay:.40];
I've created a 3D cube view transition that does the following:
Creates a layer container.
Adds the current view in 2D space.
Adds the next view to the right and rotated 90 degrees (like in a cube)
Performs an animation to rotate the cube.
Here's the code for that - its working fine. . . actual problem below:
- (CAAnimation*)makeRotationWithMetrics:(BBRotationMetrics*)metrics
{
[CATransaction flush];
CAAnimationGroup* group = [CAAnimationGroup animation];
group.delegate = self;
group.duration = metrics.duration;
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
group.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
CABasicAnimation* translationX = [CABasicAnimation animationWithKeyPath:#"sublayerTransform.translation.x"];
translationX.toValue = metrics.translationPointsX;
CABasicAnimation* rotation = [CABasicAnimation animationWithKeyPath:#"sublayerTransform.rotation.y"];
rotation.toValue = metrics.radians;
CABasicAnimation* translationZ = [CABasicAnimation animationWithKeyPath:#"sublayerTransform.translation.z"];
translationZ.toValue = metrics.translationPointsZ;
group.animations = [NSArray arrayWithObjects:rotation, translationX, translationZ, nil];
return group;
}
At the end of the animation, I would like to perform a jiggle, as though the cube was mounted on a spring. I've tried playing a series of animations to rotate around the Y axis, gradually decaying.
I've set up a series of animations, but each ones starts from the origin, instead of the last point the layer got to. . . how can I fix that? One way would be to set the tranform on the layer first,
But is there a way to do 3D transforms with key-points ?
It sounds to me like you are looking for CAKeyframeAnimation. Instead of specifying a toValue you pass an array of values and optionally keyTimes including start and end values.
The starts from the origin problem comes from using removedOnCompletion. By doing so you are introducing a difference between the presentation (what is shown on screen) and the model (the property value of your layer). A cleaner approach is to explicitly set the end value and animate from the previous value to the end value. That will leave you in a clean state when the animation finishes.
I am trying to animate a CAEmitterLayer's emitterPosition like this:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"emitterPosition.x"] ;
animation.toValue = (id) toValue ;
animation.removedOnCompletion = NO ;
animation.duration = self.translationDuration ;
animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut] ;
animation.completion = ^(BOOL finished)
{
[self animateToOtherSide] ;
} ;
[_emitterLayer addAnimation:animation forKey:#"emitterPosition"] ;
CGPoint newEmitterPosition = CGPointMake(toValue.floatValue, self.bounds.size.height/2.0) ;
_emitterLayer.emitterPosition = newEmitterPosition ;
Note that animation.completion is declared in a category that just calls the corresponding CAAnimation delegate method.
The problem is that this doesn't animate at all and shows the emitter in its final position. I was under the impression that once you add the animation to the layer, you should change the actual model behind it to its final position so that when the animation completes the model is in its final state; i.e., to prevent the animation from "snapping back" to its original position.
I have tried placing the last two lines in the animation.completion block, and this does indeed animate as expected. However, when the animation finishes some particles are intermittently emitted at the emitter's original position. If you put the system under load (for example, scrolling a tableview while the animation is playing), this happens more often.
Another solution I was thinking about is to not move the emitterPosition at all but just move the CAEmitterLayer itself, although I haven't tried that yet.
Thanks in advance for any help you can provide.
Perhaps emitterPosition.x is not a valid key path for animation. Try using emitterPosition instead (and so you'll have to provide CGPoint values wrapped up in an NSValue).
I just tried this on my own machine and it works fine:
CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:#"emitterPosition"];
ba.fromValue = [NSValue valueWithCGPoint:CGPointMake(30,100)];
ba.toValue = [NSValue valueWithCGPoint:CGPointMake(200,100)];
ba.duration = 6;
ba.autoreverses = YES;
ba.repeatCount = HUGE_VALF;
[emit addAnimation:ba forKey:nil];
Other things to think about:
You can typically use nil as the key in addAnimation:forKey:, unless you're going to need to find this animation later (e.g. to remove it or override it in some way). The key path is the important thing.
Setting removedOnCompletion to NO is almost always wrong and is typically the last refuge of a scoundrel (i.e. due to not understanding how animation works).
If, as you say, setting _emitterLayer.emitterPosition = newEmitterPosition inside the completion block does animate, then when why are you using CABasicAnimation at all? Why not just call UIView animate... and set the emitterPosition in the animations block? If that works, it will kill two birds with one stone, moving the position and animating it too.
I am creating some animation on my application and the code below zooms out an object till it disappears. I can't figure out how to make the object to disappear and keep that way, ie. how to make the animation stay put after it finishes. Any gotchas on that? Cheers!
CABasicAnimation* zoomOut = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
zoomOut.duration = 1;
zoomOut.toValue = [NSNumber numberWithFloat:0];
[draggedObject addAnimation:zoomOut forKey:nil];
I found it. It also needs the two methods below:
zoomOut.removedOnCompletion = NO;
zoomOut.fillMode = kCAFillModeForwards;
Ok so this happens because the animation doesn't actually change the underlying property, which is why it jumps back after the animation is complete.
Try adding this line before the line starting the animation -
zoomOut.removedOnCompletion = NO;