8
votes

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.

3

3 Answers

26
votes

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.

3
votes

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)

1
votes

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:

  1. In the setFrame, create 2 animations with the bounds and position property.
  2. Use "animation.removedOnCompletion = NO; animation.fillMode = kCAFillModeForwards;" to ensure that the values are not reseted at the end of the animation
  3. 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];
}