I'm trying to create a custom loading animation. The animation consists of two parts. First, 5 lines are drawn (animated) outwards. When the lines are finished drawing, they are faded away and the cycle is started over. In order to get this sequential behavior with two animations, I am using two CABasicAnimations, both of which have the same delegate. In the delegate method animationDidStop:, I dispatch to the correct animation (animation 2 if animation 1 just finished, and vise versa). Here is the relevant code:
-(void)startAnimating{//this method launches the line growing animation
self.hidden = NO;
BOOL delegateSet = NO;
animationPhase = 1;
for(CAShapeLayer * pathLayer in _lines){
[pathLayer removeAllAnimations];
pathLayer.strokeColor = _strokeColor.CGColor;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = .95;
pathAnimation.fromValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:1.0f];
if(!delegateSet){//just set one delegate
pathAnimation.delegate = self;
delegateSet = YES;
}
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
}
//this delegate method dispatches the next animation
- (void) animationDidStop:(CAAnimation *)anim finished:(BOOL)flag{
if(animationPhase == 1)[self fadeLines];
else if(animationPhase == 2)[self startAnimating];
}
-(void)fadeLines{//this method launches the fading animation
self.hidden = NO;
BOOL delegateSet = NO;
animationPhase = 2;
for(CAShapeLayer * pathLayer in _lines){
[pathLayer removeAllAnimations];
CABasicAnimation *strokeAnim = [CABasicAnimation animationWithKeyPath:#"strokeColor"];
strokeAnim.fromValue = (id) pathLayer.strokeColor;
strokeAnim.toValue = (id) [UIColor clearColor].CGColor;
strokeAnim.duration = .2;
if(!delegateSet){//just set one delegate
strokeAnim.delegate = self;
delegateSet = YES;
}
[pathLayer addAnimation:strokeAnim forKey:#"animateStrokeColor"];
pathLayer.strokeColor = [UIColor clearColor].CGColor;
}
}
So this all works great when it's run on it's own (not while other things are being done in the background). But when I use it as a loading animation (while other things are being done in the background), the first initial grow animation is performed, but animationDidStop: is never called.
I've tried using transactions with completion blocks instead of using the delegate pattern, but I get the same issue. I can't use CAAnimationGroup with appropriate beginTimes because I need the animations to loop.
Another thing to note is if I just perform the growing animation with a high repeat count, everything works fine. The only issue is the sequential looping of the two animations. If anyone knows how I can fix this, or knows a different way to sequentially loop two CAAnimations, I will be very grateful.
Related
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?
I'm adding CAShapeLayer with some CABasicAnimation. I want to be able to remove the layer and draw it again but I can't succeed with this.
- (void)drawRect:(CGRect)rect {
[self drawLayer];
}
- (void)drawLayer {
if (_alayer && _layer.superlayer) {
[_alayer removeFromSuperlayer];
_alayer = nil;
}
_alayer = [[CAShapeLayer alloc] init];
_alayer.path = [self myPath].CGPath;
_alayer.strokeColor = [UIColor redColor].CGColor;
_alayer.fillColor = [UIColor clearColor].CGColor;
_alayer.lineWidth = 2.f;
_alayer.strokeStart = 0.f;
_alayer.strokeEnd = 1.f;
[self.layer addSublayer:_alayer];
// animate
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
animation.duration = 2.f;
animation.fromValue = #0.f;
animation.toValue = #1.f;
[_alayer addAnimation:animateStrokeEnd forKey:#"strokeEndAnimation"];
}
}
However a moment after calling [self setNeedsDisplay]; even though it stops at the breakpoint in drawLayer method, the drawing doesn't disappear and animate in again. alayer is declared as nonatomic, strong property. What am I doing wrong?
You should not call [self drawLayer]; fromm drawRect: because it is called periodically.
Use other viewDidLoad or call it from other method instead
Edited:
drawRect: should only be used for drawing, I don't know if it will even work if you add animation from there.
If you want it to disappear and appear, you should try to add delay using [self performSelector:withObject:afterDelay:].
I think changing hidden or opacity should suffice, you only need to removeAllAnimations to make sure no more animation attached to that layer.
However I think you should try other way than calling drawLayer from drawRect: because I don't it its good practice anyway.
The UIView has its own property layer by default. Please try to change a variable name to shapeLayer or another one.
The title may not be so clear, but what I want to do is to make the UIImageView display a series of images (sort of like a gif) and I do this by setting the animationImages to an array of images and then calling [imageView startAnimating];. This works fine. I also have code that moves the UIImageView around with a CABasicAnimation, and this animation code also works fine. However, when I try to both animate the images of the UIImageView and try to move the UIImageView around, the images of the UIImageView stop animating. Is there a workaround?
Here's my code for animating the content of the UIImageView:
self.playerSprite = [[UIImageView alloc] initWithImage:[self.player.animationImages objectAtIndex:0]];
[self.playerSprite setFrame:CGRectMake(self.center.x, self.center.y, self.tileSet.constants.TILE_WIDTH, self.tileSet.constants.TILE_HEIGHT)];
self.playerSprite.animationImages = self.player.animationImages;
self.playerSprite.animationDuration = self.tileSet.constants.animationDuration;
self.playerSprite.animationRepeatCount = 0; //Makes it repeat indefinitely
And here's my coding for animating the UIImageView with a CABasicAnimation:
float playerSpriteX = self.playerSprite.center.x;
float playerSpriteY = self.playerSprite.center.y;
CABasicAnimation *moveAnimation = [CABasicAnimation animation];
moveAnimation.toValue = [NSValue valueWithCGPoint:CGPointMake(playerSpriteX + TILE_WIDTH, playerSpriteY)];
[moveAnimation setDelegate:self];
[moveAnimation setFillMode:kCAFillModeForwards];
[moveAnimation setRemovedOnCompletion:NO];
[moveAnimation setDuration:MOVE_ANIMATION_DURATION];
[self.playerSprite.layer addAnimation:moveAnimation forKey:#"position"];
So the gif effect isn't working while the UIImageView's position is being animated. My question is how can I make it so the UIImageView cycles through an array of images while its position is being animated?
This is speculation and I haven't tested this idea at all, but have you considered implementing the image animation in the same fashion you're animating the position, with a CAKeyframeAnimation? You'd need to construct an array of CGImage objects from your UIImage array (to set the values property of the keyframe animation) but it looks like a pretty straightforward conversion:
CAKeyframeAnimation *imageAnimation = [CAKeyframeAnimation animation];
imageAnimation.calculationMode = kCAAnimationDiscrete; // or maybe kCAAnimationPaced
imageAnimation.duration = self.tileSet.constants.animationDuration;
imageAnimation.repeatCount = HUGE_VALF;
// the following method will need to be implemented to cast your UIImage array to CGImages
imageAnimation.values = [self animationCGImagesArray];
[self.playerSprite.layer addAnimation:imageAnimation forKey:#"contents"];
-(NSArray*)animationCGImagesArray {
NSMutableArray *array = [NSMutableArray arrayWithCapacity:[self.player.animationImages count]];
for (UIImage *image in self.player.animationImages) {
[array addObject:(id)[image CGImage]];
}
return [NSArray arrayWithArray:array];
}
I've just done something similar. The code I used looks like this:
CGRect r = ziv.frame;
r.origin.x += WrongDistance;
[ziv startAnimating];
[UIView animateWithDuration:3.0 animations:^(void){
[ziv setFrame:r];
} completion:^(BOOL finished){
[ziv stopAnimating];
if (finished){
// not canceled in flight
if (NumWrong == MaxWrong)
[self endOfGame:NO];
else
[self nextRound:self];
}
}];
Perhaps the issue you're running into is because both animations are on the same thread?
Your "basic" problem starts on this line:
CABasicAnimation *moveAnimation = [CABasicAnimation animation];
A CABasicAnimation object uses only a single keyframe. That means that the content of the layer that you're animating is drawn once, and then that image is used for the duration of the animation.
Using a CAKeyframeAnimation as Seamus suggests is one way to deal with the problem -- a keyframe animation will redraw the content of the animated layer multiple times, so drawing successive images for each keyframe will animate the content as well as position.
This is not a good way to make a sprite. I could insist that you should be using OpenGL, but there may be no need to go that far; you can do it all with Core Animation of a layer, and I think you'll be better off doing that than trying to use a full-fledged UIImageView.
Using Core Animation of a layer, I was able to make this PacMan sprite animate across the screen while opening and closing his mouth; isn't that the sort of thing you had in mind?
Here is a video showing the animation!
http://www.youtube.com/watch?v=WXCLc9ww8MI
And yet the actual code creating the animation is extremely simple: just two layer animations (Core Animation), one for the changing image, the other for the position.
You should not award this answer the bounty! I am now merely echoing what Seamus Campbell said; I'm just filling out the details of his answer a little.
Okay, so here's the code that generates the movie linked above:
- (void)viewDidLoad {
[super viewDidLoad];
// construct arr, an array of CGImage (omitted)
// it happens I've got 5 images, the last being the same as the first
self.images = [arr copy];
// place sprite into the interface
self.sprite = [CALayer new];
self.sprite.frame = CGRectMake(30,30,24,24);
self.sprite.contentsScale = [UIScreen mainScreen].scale;
[self.view.layer addSublayer:self.sprite];
self.sprite.contents = self.images[0];
}
- (void)animate {
CAKeyframeAnimation* anim =
[CAKeyframeAnimation animationWithKeyPath:#"contents"];
anim.values = self.images;
anim.keyTimes = #[#0,#0.25,#0.5,#0.75,#1];
anim.calculationMode = kCAAnimationDiscrete;
anim.duration = 1.5;
anim.repeatCount = HUGE_VALF;
CABasicAnimation* anim2 =
[CABasicAnimation animationWithKeyPath:#"position"];
anim2.duration = 10;
anim2.toValue = [NSValue valueWithCGPoint: CGPointMake(350,30)];
CAAnimationGroup* group = [CAAnimationGroup animation];
group.animations = #[anim, anim2];
group.duration = 10;
[self.sprite addAnimation:group forKey:nil];
}
I am drawing a path in my UIView's drawRect method. In certain situations that path will go away and I want to animate this - let's call it "undrawing" of the path.
For the animation, I am creating a CAShapeLayer, give it myPath and then set up a CABasicAnimation (animationWithKeyPath:#"strokeEnd").
My problem is how to switch from what is drawn in drawRect to the animation. I have to remove myPath from what is drawn from drawRect and call setNeedsDisplay - otherwise the animation would be hidden by the path drawn from drawRect.
- (void)animationDidStart:(CAAnimation *)theAnimation
{
myPath = nil;
[self setNeedsDisplay];
}
But this way I see my path, then a quick flickering with no path, then the path is rendered by CoreAnimation again and is nicely undrawn.
Can I do better to avoid the flickering?
Background
This is how I set up the animation:
- (void) startAnimation
{
pathLayer.path = myPath.CGPath;
pathLayer.hidden = false;
CABasicAnimation *pathAnimation = [CABasicAnimation animationWithKeyPath:#"strokeEnd"];
pathAnimation.duration = 1;
pathAnimation.fromValue = [NSNumber numberWithFloat:1.0f];
pathAnimation.toValue = [NSNumber numberWithFloat:0.0f];
pathAnimation.fillMode = kCAFillModeForwards;
pathAnimation.removedOnCompletion = NO;
pathAnimation.delegate = self;
[pathLayer addAnimation:pathAnimation forKey:#"strokeEnd"];
}
I believe the cause of the flickering is the implicit animation that runs when you call
pathLayer.hidden = NO;
By default, standalone CALayers animate most property changes implicitly. If you disable the implicit animation for the hidden property, it should run without flickering (regardless whether you use drawRect: or drawLayer:inContext:):
[CATransaction begin];
[CATransaction setDisableActions:YES];
pathLayer.hidden = NO;
[CATransaction commit];
Here is my solution: Don't combine UIKits drawRect with CAAnimation but draw into a CALayer instead:
- (void)drawLayer:(CALayer *)theLayer inContext:(CGContextRef)theContext
This allows flicker-free switching from drawing to animation.
I've been implementing a simple FlipView in iOS : A UIView that contains two subviews, displaying one at a time, and when you click on it, it flips them.
I'm using the following to animate the flipping.
- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event{
#synchronized(self){
if(!self.flipping){
self.flipping = YES;
UIView *toView = self.currentView == self.primaryView ? self.secondaryView : self.primaryView;
[UIView transitionFromView:self.currentView toView:toView duration:self.speed options:UIViewAnimationOptionTransitionFlipFromLeft|UIViewAnimationOptionCurveEaseInOut completion:^(BOOL finished) {
[self.currentView removeFromSuperview];
self.currentView = toView;
self.flipping = NO;
}];
}
}
}
Pretty straight forward, right ?
But what bugs me is that, while the views are flip, the flipped content is darkened. Which shows, against a light background.
Would anyone knows a solution to have the exact same animation, but without the darkening (<= is that even a word ?)
Thanks in advance !
PS : I'm targeting IOS 5 and above.
I recently had a problem with similar symptoms and I was adding a subview over and over again else where in my code whenever I committed a certain action. Maybe you are doing something similar? When your touches end, are you doing something else to your flipped content? You probably need to remove the subviews being added IF that is your problem.
I succeeded, getting inspiration in the code I found here http://www.mycodestudio.com/blog/2011/01/10/coreanimation/ (and he, himself, took inspiration from http://www.mentalfaculty.com/mentalfaculty/Blog/Entries/2010/9/22_FLIPPIN_OUT_AT_NSVIEW.html)
Anyway, what I do spin between two views.
- (void)flip{
#synchronized(self){
if(!self.flipping){
self.flipping = YES;
UIView *bottomView = self.currentView == self.primaryView ? self.secondaryView : self.primaryView;
CALayer *top = self.currentView.layer;
CALayer *bot = bottomView.layer;
CAAnimation *topAnimation = [self flipAnimationWithDuration:self.speed/2.0 forLayerBeginningOnTop:YES scaleFactor:1];
CAAnimation *bottomAnimation = [self flipAnimationWithDuration:self.speed/2.0 forLayerBeginningOnTop:NO scaleFactor:1];
CGFloat zDistance = 1500.0f;
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1. / zDistance;
top.transform = perspective;
bot.transform = perspective;
topAnimation.delegate = self;
[CATransaction setCompletionBlock:^{
[top removeAllAnimations];
[self.currentView removeFromSuperview];
self.currentView = bottomView;
[self addSubview:bottomView];
[CATransaction setCompletionBlock:^{
self.flipping = NO;
[bot removeAllAnimations];
}];
[CATransaction begin];
[bot addAnimation:bottomAnimation forKey:#"flip"];
[CATransaction commit];
}];
[CATransaction begin];
[top addAnimation:topAnimation forKey:#"flip"];
[CATransaction commit];
}
}
}
-(CAAnimation *)flipAnimationWithDuration:(NSTimeInterval)aDuration forLayerBeginningOnTop:(BOOL)beginsOnTop scaleFactor:(CGFloat)scaleFactor
{
// Rotating halfway (pi radians) around the Y axis gives the appearance of flipping
CABasicAnimation *flipAnimation = [CABasicAnimation animationWithKeyPath:#"transform.rotation.y"];
CGFloat startValue = beginsOnTop ? 0.0f : M_PI/2;
CGFloat endValue = beginsOnTop ? -M_PI/2 : 0.0f;
flipAnimation.fromValue = [NSNumber numberWithDouble:startValue];
flipAnimation.toValue = [NSNumber numberWithDouble:endValue];
// Shrinking the view makes it seem to move away from us, for a more natural effect
// Can also grow the view to make it move out of the screen
CABasicAnimation *shrinkAnimation = nil;
if (scaleFactor != 1.0 ) {
shrinkAnimation = [CABasicAnimation animationWithKeyPath:#"transform.scale"];
shrinkAnimation.toValue = [NSNumber numberWithFloat:scaleFactor];
// We only have to animate the shrink in one direction, then use autoreverse to "grow"
shrinkAnimation.duration = aDuration * 0.5;
shrinkAnimation.autoreverses = YES;
}
// Combine the flipping and shrinking into one smooth animation
CAAnimationGroup *animationGroup = [CAAnimationGroup animation];
animationGroup.animations = [NSArray arrayWithObjects:flipAnimation, shrinkAnimation, nil];
// As the edge gets closer to us, it appears to move faster. Simulate this in 2D with an easing function
animationGroup.timingFunction = [CAMediaTimingFunction functionWithName:beginsOnTop?kCAMediaTimingFunctionEaseIn:kCAMediaTimingFunctionEaseOut];
animationGroup.duration = aDuration;
// this really means keep the state of the object at whatever the anim ends at
// if you don't do this then it reverts back to the original state (e.g. brown layer)
animationGroup.fillMode = kCAFillModeForwards;
animationGroup.removedOnCompletion = NO;
return animationGroup;
}
The two views are named primaryView and secondaryView. You can use any view, (ImageView, text view...)