UIButton doesn't receive touch after CAKeyframeAnimation - ios

My situation:
I've a UIButton that it's animate with a CAKeyframeAnimation that's declared as a category on UIView:
CAKeyframeAnimation * scale = [CAKeyframeAnimation animationWithKeyPath:#"transform.scale"];
scale.duration = duration;
scale.beginTime = delay;
scale.fillMode = kCAFillModeForwards;
NSMutableArray * times = [[NSMutableArray alloc] init];
scale.values = values;
scale.timingFunctions = times;
CAAnimationGroup * group = [CAAnimationGroup animation];
[group setDelegate:self];
[group setDuration:duration + delay];
[group setFillMode:kCAFillModeForwards];
[group setRemovedOnCompletion:NO];
[group setAnimations:[NSArray arrayWithObject:scale]];
[self.layer addAnimation:group forKey:#"scale"];
The problem is that after the animation, the button doesn't receive touch. If I remove the animation all works fine.
Does anyone know what I'm doing wrong ?
Thanks

You shouldn't use kCAFillModeForwards and removedOnCompletion = NO to stick the animated layer in the final position. This will not work for a control, and results in the behavior you noticed.
Instead, set the final state for the button before adding the animation to it's layer.
self.layer.transform = CGAffineTransformMakeScale(finalScaleX, finalScaleY);
[self.layer addAnimation:group forKey:#"scale"];

The problem is that the animation only changes the presentation of the button but the touch target is still the same as before. You should either remove the animation after completion and set the transform on the button or ht test the presentation layer.
I wrote about hit testing animating layers in a blog post that explains it in more detail.

You must insert the following line of code self.layer.transform = CGAffineTransformMakeScale(finalScaleX, finalScaleY); when the animation is ended.

Related

CALayer CABasicAnimation chaining

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.)

How do I simultaneously animate a UIImageView's image and the UIImageView itself?

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];
}

Cannot get current position of CALayer during animation

I am trying to achieve an animation that when you hold down a button it animates a block down, and when you release, it animates it back up to the original position, but I cannot obtain the current position of the animating block no matter what. Here is my code:
-(IBAction)moveDown:(id)sender{
CGRect position = [[container.layer presentationLayer] frame];
[movePath moveToPoint:CGPointMake(container.frame.origin.x, position.y)];
[movePath addLineToPoint:CGPointMake(container.frame.origin.x, 310)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
moveAnim.fillMode = kCAFillModeForwards;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, nil];
animGroup.duration = 2.0;
animGroup.removedOnCompletion = NO;
animGroup.fillMode = kCAFillModeForwards;
[container.layer addAnimation:animGroup forKey:nil];
}
-(IBAction)moveUp:(id)sender{
CGRect position = [[container.layer presentationLayer] frame];
UIBezierPath *movePath = [UIBezierPath bezierPath];
[movePath moveToPoint:CGPointMake(container.frame.origin.x, position.y)];
[movePath addLineToPoint:CGPointMake(container.frame.origin.x, 115)];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
moveAnim.fillMode = kCAFillModeForwards;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim, nil];
animGroup.duration = 2.0;
animGroup.removedOnCompletion = NO;
animGroup.fillMode = kCAFillModeForwards;
[container.layer addAnimation:animGroup forKey:nil];
}
But the line
CGRect position = [[container.layer presentationLayer] frame];
is only returning the destination position not the current position. I need to basically give me the current position of the container thats animating once I release the button, so I can perform the next animation. What I have now does not work.
I haven't analyzed your code enough to be 100% sure why [[container.layer presentationLayer] frame] might not return what you expect. But I see several problems.
One obvious problem is that moveDown: doesn't declare movePath. If movePath is an instance variable, you probably want to clear it or create a new instance each time moveDown: is called, but I don't see you doing that.
A less obvious problem is that (judging from your use of removedOnCompletion and fillMode, in spite of your use of presentationLayer) you apparently don't understand how Core Animation works. This turns out to be surprisingly common, so forgive me if I'm wrong. Anyway, read on, because I will explain how Core Animation works and then how to fix your problem.
In Core Animation, the layer object you normally work with is a model layer. When you attach an animation to a layer, Core Animation creates a copy of the model layer, called the presentation layer, and the animation changes the properties of the presentation layer over time. An animation never changes the properties of the model layer.
When the animation ends, and (by default) is removed, the presentation layer is destroyed and the values of the model layer's properties take effect again. So the layer on screen appears to “snap back” to its original position/color/whatever.
A common, but wrong way to fix this is to set the animation's removedOnCompletion to NO and its fillMode to kCAFillModeForwards. When you do this, the presentation layer hangs around, so there's no “snap back” on screen. The problem is that now you have the presentation layer hanging around with different values than the model layer. If you ask the model layer (or the view that owns it) for the value of the animated property, you'll get a value that's different than what's on screen. And if you try to animate the property again, the animation will probably start from the wrong place.
To animate a layer property and make it “stick”, you need to change the model layer's property value, and then apply the animation. That way, when the animation is removed, and the presentation layer goes away, the layer on screen will look exactly the same, because the model layer has the same property values as its presentation layer had when the animation ended.
Now, I don't know why you're using a keyframe to animate straight-line motion, or why you're using an animation group. Neither seems necessary here. And your two methods are virtually identical, so let's factor out the common code:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromValue];
animation.toValue = [NSValue valueWithCGPoint:toValue];
animation.duration = 2;
[layer addAnimation:animation forKey:animation.keyPath];
}
Notice that we're giving the animation a key when I add it to the layer. Since we use the same key every time, each new animation will replace (remove) the prior animation if the prior animation hasn't finished yet.
Of course, as soon as you play with this, you'll find that if you moveUp: when the moveDown: is only half finished, the moveUp: animation will appear to be at half speed because it still has a duration of 2 seconds but only half as far to travel. We should really compute the duration based on the distance to be travelled:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y withBaseY:(CGFloat)baseY {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:#"position"];
animation.fromValue = [NSValue valueWithCGPoint:fromValue];
animation.toValue = [NSValue valueWithCGPoint:toValue];
animation.duration = 2.0 * (toValue.y - fromValue.y) / (y - baseY);
[layer addAnimation:animation forKey:animation.keyPath];
}
If you really need it to be a keypath animation in an animation group, your question should show us why you need those things. Anyway, it works with those things too:
- (void)animateLayer:(CALayer *)layer toY:(CGFloat)y withBaseY:(CGFloat)baseY {
CGPoint fromValue = [layer.presentationLayer position];
CGPoint toValue = CGPointMake(fromValue.x, y);
layer.position = toValue;
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:fromValue];
[path addLineToPoint:toValue];
CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:#"position"];
animation.path = path.CGPath;
animation.duration = 2.0 * (toValue.y - fromValue.y) / (y - baseY);
CAAnimationGroup *group = [CAAnimationGroup animation];
group.duration = animation.duration;
group.animations = #[animation];
[layer addAnimation:group forKey:animation.keyPath];
}
You can find the full code for my test program in this gist. Just create a new Single View Application project and replace the contents of ViewController.m with the contents of the gist.

Is there a way to give a CALayer's action dictionary permanent precendece over its UIView delegate?

I have the following situation:
From within a view controller, I go about creating an arbitrary number of transparent UIButtons which toogle the appearance and disappearance of UIViews over the controller's main view. Right now, when my "zoom in" and "zoom out" methods are called, there's code for modifying the default animation of two CALayer properties (just duration really). This code then gets called every time the actions take place. But I'd like to set the animation code once inside the layer and not have it in the zoom methods. I'll explain more after I show the basic idea:
The buttons/sensors creation code is too long to post (and not that relevant to the real question), but it's something along the lines of this pseudo code:
- (void) createSensors:[number of sensors]
{
for(number of sensors)
{
create UIVIew with some content;
create transparent UIBUtton as sensor and place somewhere in main view;
calculate a transform to "shrink" the UIView's layer to the size and position of the button.
apply transform;
set the UIVIew's layer opacity to 0;
add UIVIew to main view
(but the rest of my interface is in a container view, so I can dim the screen and have this view displayed in a modal-like way.)
do some tagging so the button's tag can later be used to retrieve this view and transform.
}
}
Now, when a sensor is clicked, I perform a "zoom in" for the view associated with the sensor/view. I actually dim the screen to white, and animate the view's (the layer's really) position and scaling to the center of the screen. And on clicking a button in the view, I do the opposite to dismiss it. This is the code:
- (void)animateZoomIn:(UIButton*)sender
{
NSString *sourceTagKey = [NSString stringWithFormat:#"%i",sender.tag];
AnimationSettings *settings = [zoomTransforms objectForKey:sourceTagKey];
UIView *newBox = [[self.view subviews] objectAtIndex:sender.tag];
activeZoomBox = newBox;
CABasicAnimation *a = [CABasicAnimation animation];
a.duration = .4;
[mainCanvas.layer addAnimation:a forKey:#"opacity"];
a = [CABasicAnimation animation];
a.duration = 1;
[activeZoomBox.layer addAnimation:a forKey:#"opacity"];
a = [CABasicAnimation animation];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
a.duration = .5;
[activeZoomBox.layer addAnimation:a forKey:#"transform"];
[mainCanvas setUserInteractionEnabled:NO];
if([settings wantsBackgroundFade])
[mainCanvas.layer setOpacity:.2];
[activeZoomBox.layer setOpacity:1];
[activeZoomBox.layer setTransform:CATransform3DIdentity];
}
- (void)animateZoomOut:(UIButton*)sender
{
NSString *sourceTagKey = [NSString stringWithFormat:#"%i",sender.tag];
AnimationSettings *settings = [zoomTransforms objectForKey:sourceTagKey];
CABasicAnimation *a = [CABasicAnimation animation];
a.duration = .4;
[mainCanvas.layer addAnimation:a forKey:#"opacity"];
a = [CABasicAnimation animation];
a.duration = 1;
[activeZoomBox.layer addAnimation:a forKey:#"opacity"];
a = [CABasicAnimation animation];
a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
a.duration = .5;
[activeZoomBox.layer addAnimation:a forKey:#"transform"];
[mainCanvas setUserInteractionEnabled:YES];
if([settings wantsBackgroundFade])
[mainCanvas.layer setOpacity:1];
[activeZoomBox.layer setOpacity:0];
[activeZoomBox.layer setTransform:[settings transform]];
}
So, the question:
I'd like to not have this kind of code:
CABasicAnimation *a = [CABasicAnimation animation];
a.duration = .4;
[mainCanvas.layer addAnimation:a forKey:#"opacity"];
in the action methods. I'd rather replace the animation once and make it "permanent" for each layer created and for the mainCanvas view. For instance, I'd prefer to have this code once on the controller's init method and forget about it:
CABasicAnimation *a = [CABasicAnimation animation];
a.duration = .4;
NSMutableDictionary *animations = [NSMutableDictionary dictionaryWithDictionary:[mainCanvas.layer actions]];
[animations setObject:a forKey:#"opacity"];
[mainCanvas.layer setActions:animations];
Then, whenever I'd call [mainCanvas setOpacity:] it would last .4 secs by default. With a delegate-less CALayer it'd work.But in this case, my layers have their views as delegates, which have precedence over the actions dictionary, and I sort of need the views as delegates..
So is there a way to still have the layers respond to the dictionary instead? And if not, what would be the best way to "permanently replace" or add animations to these layers, so that the animations are contained in the layer objects (which is why I'd prefer to not override the view's delegate methods for the CAAction Protocol)?
Why not just override -actionForLayer:forKey: on the UIView to provide the action you want on the important keys (and call super for the others)? There is no way to change the precedence, so your only options are implement -actionForLayer:forKey: or go with the manual animations.

CAAnimationGroup reverts to original position on completion

In iOS I'm trying to create the effect of an icon shrinking in size, and flying across the screen in an arc while fading out, and then disappearing. I've achieved these 3 effects with an CAAnimationGroup, and it does what I want. The problem is when the animation ends, the view appears back at the original position, full size and full opacity. Can anyone see what I am doing wrong in the code below?
The animation should not revert to it's original position, but just disappear at the end.
UIBezierPath *movePath = [UIBezierPath bezierPath];
CGPoint libraryIconCenter = CGPointMake(610, 40);
CGPoint ctlPoint = CGPointMake(self.imgViewCropped.center.x, 22.0);
movePath moveToPoint:self.imgViewCropped.center];
[movePath addQuadCurveToPoint:libraryIconCenter
controlPoint:ctlPoint];
CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:#"position"];
moveAnim.path = movePath.CGPath;
moveAnim.removedOnCompletion = NO;
CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:#"transform"];
scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity];
scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)];
scaleAnim.removedOnCompletion = NO;
CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:#"alpha"];
opacityAnim.fromValue = [NSNumber numberWithFloat:1.0];
opacityAnim.toValue = [NSNumber numberWithFloat:0.0];
opacityAnim.removedOnCompletion = NO;
CAAnimationGroup *animGroup = [CAAnimationGroup animation];
animGroup.animations = [NSArray arrayWithObjects:moveAnim,scaleAnim,opacityAnim, nil];
animGroup.duration = 0.6;
animGroup.delegate = self;
animGroup.removedOnCompletion = NO;
[self.imgViewCropped.layer addAnimation:animGroup forKey:nil];
I believe you need to set the fillMode property of your animations to kCAFillModeForwards. That should freeze the animations at their end time. Another suggestion (and honestly, this is what I'd usually do) is just se the properties of the layer itself to their final position after you've set up the animation. That way when the animation is removed, the layer will still have the final properties as part of its model.
As an aside, the removedOnCompletion flag of animations contained within a CAAnimationGroup is ignored. You should probably just remove those assignments since they're misleading. Replace them with assignments to fillMode as specified above.

Resources