I have an insert animation for UICollectionCell (like slide-in), I called the insert animation in this method -collectionView:willDisplayCell:forItemAtIndexPath:.
Everything just works fine when I called this [self.collectionView reloadData]; to reload data. In this way, the animation will be shown currently.
------------
A
------------
<= inserted C (slide-in Animation)
------------
B
------------
But when I tried to add another animation to this inserted cell (like slide-down), things went wrong.
Instead of using -reloadData, I used [self.collectionView performBatchUpdates:completion:] and a custom UICollectionViewFlowLayout with initialLayoutAttributesForAppearingItemAtIndexPath method to implement the slide down animation.
------------
A
------------
------------ <= inserted C (Second, slide-in animation)
B (First, slide-down animation)
------------
In the cell, the log of animation's delegate method animationDidStop:finished: shows that the finished = NO
UICollectionCell:
- (void)setAnimationInLayer:(CALayer *)layer
{
CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:#"transform.translation.x"];
anim.fromValue = [NSNumber numberWithFloat:(layer.bounds.size.width * 0.33f)];
anim.toValue = [NSNumber numberWithFloat:0.0];
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:#"opacity"];
opacityAnim.fromValue = [NSNumber numberWithFloat:0];
opacityAnim.toValue = [NSNumber numberWithFloat:1];
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:anim,opacityAnim, nil];
animGroup.timingFunction = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseInEaseOut];
animGroup.duration = 5.5f;
animGroup.delegate = self;
[layer addAnimation:animGroup forKey:nil];
}
- (void)animationDidStart:(CAAnimation *)animation
{
NSLog(#"-------> %s",__func__);
NSLog(#"%f",[self.articlesCollection.layer presentationLayer].frame.origin.x);
}
- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)finished
{
NSLog(#"-------> %s finished: %d",__func__,finished);
NSLog(#"%f",[self.articlesCollection.layer presentationLayer].frame.origin.x);
}
To sum up, when I use -performBatchUpdates:completion instead of -reloadData, every animation of UICollectionView disappeared.
So the question is what situation may cause an animation disappear?
I mean looks like it has been removed from the layer it is attached to. But I didn't do that. Any idea?
Related
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))
Good Evening!
I have a peace of code but it is not calling the animationdidstop method. I could not identify why it does not work. I`ve tried many solutions..
-(IBAction)MakeCircle:(id)sender{
// Add to parent layer
[self.view.layer addSublayer:circle];
// Configure animation
drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
drawAnimation.duration = 5.0;
drawAnimation.repeatCount = 1.0;
drawAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
drawAnimation.toValue = [NSNumber numberWithFloat:1.0f];
drawAnimation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear];
// Add the animation
[circle addAnimation:drawAnimation forKey:#"drawCircleAnimation"];}
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
if(anim == [self.view.layer animationForKey:#"drawCircleAnimation"]){
Label.text = [NSString stringWithFormat:#"Loser"];
}
}
Thanks!!
You are not setting yourself as the delegate of the animation. Add this line before adding the animation:
drawAnimation.delegate = self;
Edit:
Oh. I know exactly what the problem is. The system copies your animation object when you submit the animation, so it won't be the same object in the completion routine. Try adding a unique key to the animation and checking that instead of checking to see if it's the same animation object you submitted.
e.g.:
drawAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
[drawAnimation setValue: #"mydrawCircleAnimation" forKey: #"animationKey"];
//The rest of your animation code...
Then in your animationDidStop:
-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
if([anim valueForKey: #"animationKey"
isEqualToString: #"mydrawCircleAnimation"])
{
Label.text = [NSString stringWithFormat:#"Loser"];
}
}
Edit #2:
There is an even cleaner way to handle animation completion code. You can attach a block to your animations and then write a general animationDidStop:completion: method that looks for the block and invokes it if found. See my answer in this thread:
How to identify CAAnimation within the animationDidStop delegate?
There are a lot of related questions, but this situation does not seem to be addressed by any the existing questions.
I have created a view with a custom layer so that one of the properties can be animated. Using the CABasicAnimation class, the animation works correctly.
However, I need a little more control over the animation, such as the ease in and ease out and sequential animations and tried to switch to using block animations. However, when I do that, the animation completes immediately rather than animating over time.
How can I get this block animation to work correctly?
Working animation code:
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"inputValue"];
animation.duration = DEFAULT_ANIMATION_DURATION;
if (flipped) {
animation.fromValue = [NSNumber numberWithDouble:0.0];
animation.toValue = [NSNumber numberWithDouble:1.0];
self.myLayer.inputValue = 1.0;
} else {
animation.fromValue = [NSNumber numberWithDouble:1.0];
animation.toValue = [NSNumber numberWithDouble:0.0];
self.myLayer.inputValue = 0.0;
}
[self.layer addAnimation:animation forKey:#"animateInputValue"];
Animation that incorrectly completes immediately, but finished is YES:
[UIView animateWithDuration:10.0 delay:0.0 options:0 animations:^{
self.myLayer.inputValue = 1.0;
} completion:^(BOOL finished) {
NSLog(#"done %#", finished?#"and finished":#", but not finished");
}];
The CALayer being animated:
#import "UViewLayer.h"
#import "YoYouStyleKit.h"
#implementation UViewLayer
+ (BOOL)needsDisplayForKey:(NSString *)key {
if( [key isEqualToString:#"inputValue"] )
return YES;
return [super needsDisplayForKey:key];
}
- (void)setInputValue:(CGFloat)inputValue {
_inputValue = inputValue;
[self setNeedsDisplay];
}
- (void)drawInContext:(CGContextRef)context {
UIGraphicsPushContext(context);
[YoYouStyleKit drawUShapeWithFrame:self.bounds input:self.inputValue];
UIGraphicsPopContext();
}
Adding #dynamic inputValue; in the custom layer seems to make no difference.
Do not mix UIKit and Core Animation animations.
Implement like this:
[CATransaction begin];
[CATransaction setCompletionBlock:^
{
NSLog(#"done");
}];
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"inputValue"];
animation.duration = DEFAULT_ANIMATION_DURATION;
if (flipped)
{
animation.fromValue = [NSNumber numberWithDouble:0.0];
animation.toValue = [NSNumber numberWithDouble:1.0];
self.myLayer.inputValue = 1.0;
}
else
{
animation.fromValue = [NSNumber numberWithDouble:1.0];
animation.toValue = [NSNumber numberWithDouble:0.0];
self.myLayer.inputValue = 0.0;
}
[self.layer addAnimation:animation forKey:#"animateInputValue"];
[CATransaction commit];
In addition to Leo Natan's answer, as mentioned in the Apple docs :
Custom layer objects ignore view-based animation block parameters and
use the default Core Animation parameters instead.
What's more tricky is that you can animate the UIView's own layer properties, provided you change one of the animatable properties.
For custom properties like your layer inputValue, you can provide the CABasicAnimation in your layer (id<CAAction>)actionForKey:(NSString *)key but it will not use the UIView animation parameters (duration...).
This animation will play when you change the layer property in the UIView block animation, but it will also play when you simply set the value.
The code provided by Leo Natan is the easiest when to animate your layer from the UIView.
I'm animating a CALayer's opacity-property with the following code:
Creating the animation in a method:
+ (CABasicAnimation *)fadeIn:(float)begin duration:(float)duration remove:(BOOL)remove{
CABasicAnimation *fadeAnimation = [CABasicAnimation animationWithKeyPath:#"opacity"];
fadeAnimation.fromValue = [NSNumber numberWithFloat:0.0];
fadeAnimation.toValue = [NSNumber numberWithFloat:1.0];
fadeAnimation.additive = NO;
fadeAnimation.removedOnCompletion = remove;
fadeAnimation.beginTime = begin;
fadeAnimation.duration = duration;
fadeAnimation.fillMode = kCAFillModeBoth;
return fadeAnimation;
}
Adding the animation to the layer:
[overlayLayer addAnimation:[VideoComposerHelpers fadeIn:1.0 duration:0.5 remove:NO] forKey:nil];
This is working perfect. However, now I want to add another animation to the same layer, right after the first animation finished.
[overlayLayer addAnimation:[VideoComposerHelpers fadeOut:1.5 duration:0.5 remove:NO] forKey:nil]; // fadeOut is a method similar to fadeIn
What should happen is, the layer fades in with a duration of 0.5 and right after that, it fades out with a duration of 0.5.
This doesn't seem to work, though. Is it because the start point of the second animation is the same as the end point of the first one?
You should use a CAAnimationGroup something like this:
CABasicAnimation *fadeIn = [VideoComposerHelpers fadeIn:1.0 duration:0.5 remove:NO];
CABasicAnimation *fadeOut = [VideoComposerHelpers fadeOut:1.5 duration:0.5 remove:NO];
CAAnimationGroup *group = [CAAnimationGroup animation];
group.fillMode = kCAFillModeForwards;
group.removedOnCompletion = NO;
[group setAnimations:[NSArray arrayWithObjects:fadeIn, fadeOut, nil]];
group.duration = 2;
[overlayLayer addAnimation:group forKey:#"savingAnimation"];
Also I'm not sure if I get the right values vor start, end, duration of the animations (you should check them) :)).
A CABasicAnimation can have a delegate. The delegate will be sent animationDidStop:finished:. Now you can ask for the next animation in the chain.
In your case, though, you don't even need that; just use an animation where autoreverses is YES.
(Oh, and do not mess with removedOnCompletion and fillMode; that's just wrong, and examples that use them are equally wrong. They are needed only in the middle complex grouped animations.)
When I launch my application and go to flipSideViewController from mainViewController the image view in flipSideViewController will animate. I want to animate it every time the user
transitions from mainViewController to flipSideViewController.
Here's some code:
[self animate];
//Not sure where to put this instance method. Would I put this in a certain method? I have no idea what I would write for it!
- (void)animate
{
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"transform.rotation"];
animation.toValue = [NSNumber numberWithFloat:((-10*M_2_PI)/180)];
animation.duration = .3;
animation.autoreverses = YES;
animation.repeatCount = 4;
animation.speed = 9;
pwAnimation.removedOnCompletion = YES;
[_ImageView.layer addAnimation:pwAnimation forKey:#"rotation"];
}
Use ViewDidAppear delegate method of flipSideViewController to invoke your -animate method