CALayer/CAAnimation doesn't animate

  • I have spent hours on this - I now throw in the towel...

    I have a view that will contain several layers I want to animate.
    There are no subviews in my view, and I'd like to keep it that way.

    I have made this simple example to illustrate the problem. It's a
    method of the view that is supposed to add a layer and animate it:

    - (void)animateNewLayer
    {
        // Create layer
        CALayer *layer = [CALayer layer];

        // Configure layer
        layer.frame = CGRectMake (0, 0, 24, 24);
        layer.contents = myCGImageRef; // Created earlier in this method
        CGImageRelease (myCGImageRef);

        // Add the layer to our view's root layer
        self.wantsLayer = YES;
        [self.layer addSublayer:layer];

        // Create animation
        CABasicAnimation *anim = [CABasicAnimation
    animationWithKeyPath:@"frameOrigin"];

        // Configure animation
        anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
        anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    100)];
        anim.timingFunction = [CAMediaTimingFunction
    functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        anim.duration      = 2.0;
        anim.delegate      = self;

        // Add animation to layer; also triggers the animation
        [layer addAnimation:anim forKey:@"frameOrigin"];
    }

    There are several problems with this code:

    1) When the code runs, the layer appears containing the loaded image
    at (0, 0) as expected, but it doesn't move
    2) If I change the keyPath to either @"opacity" or @"borderWidth", the
    layer does animate, but as soon as it's done animating, the layer goes
    back to its original appearance, and the animation doesn't take the 2
    secs to complete - only a fraction
    3) The delegate method, animationDidStart:, is only called in (2), not
    in (1), and animationDidStop: is never called in any of the scenarios

    I'm totally stuck in this. Any help is hugely appreciated. I think I'm
    doing what's suggested in the examples in Core Animation Programming
    Guide, but obviously not.

    Am I handling the layer hierarchy correctly? For example, can I use
    the view's backing layer as the root of the layer hierarchy?

    Thaks in advance,
    Joachim
  • Hi Joachim,

    You should not be manipulating the layer that the view creates for its
    self as a general rule.

    I.e. if you want to manipulate the layer (as you do in this code) make
    the view a 'layer hosting view' instead of a 'layer backed view'. The
    apple docs on this subject are pretty good (if you search for either
    of the quoted terms you should get to the article, if not please ping
    me again and I'll track it down).

    On the more practical side of making this work...

    If you call [self setLayer:[CALayer layer]] right before you call
    [self setWantsLayer:YES] you should start to see behavior that more
    closely matches what you want/expect.

    HTH,

    -bd-
    http://bill.dudney.net/roller/objc

    On Jan 14, 2008, at 3:37 PM, Joachim wrote:

    > I have spent hours on this - I now throw in the towel...
    >
    > I have a view that will contain several layers I want to animate.
    > There are no subviews in my view, and I'd like to keep it that way.
    >
    > I have made this simple example to illustrate the problem. It's a
    > method of the view that is supposed to add a layer and animate it:
    >
    > - (void)animateNewLayer
    > {
    > // Create layer
    > CALayer *layer = [CALayer layer];
    >
    > // Configure layer
    > layer.frame = CGRectMake (0, 0, 24, 24);
    > layer.contents = myCGImageRef; // Created earlier in this method
    > CGImageRelease (myCGImageRef);
    >
    > // Add the layer to our view's root layer
    > self.wantsLayer = YES;
    > [self.layer addSublayer:layer];
    >
    > // Create animation
    > CABasicAnimation *anim = [CABasicAnimation
    > animationWithKeyPath:@"frameOrigin"];
    >
    > // Configure animation
    > anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
    > anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    > 100)];
    > anim.timingFunction = [CAMediaTimingFunction
    > functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    > anim.duration      = 2.0;
    > anim.delegate      = self;
    >
    > // Add animation to layer; also triggers the animation
    > [layer addAnimation:anim forKey:@"frameOrigin"];
    > }
    >
    > There are several problems with this code:
    >
    > 1) When the code runs, the layer appears containing the loaded image
    > at (0, 0) as expected, but it doesn't move
    > 2) If I change the keyPath to either @"opacity" or @"borderWidth",
    > the layer does animate, but as soon as it's done animating, the
    > layer goes back to its original appearance, and the animation
    > doesn't take the 2 secs to complete - only a fraction
    > 3) The delegate method, animationDidStart:, is only called in (2),
    > not in (1), and animationDidStop: is never called in any of the
    > scenarios
    >
    > I'm totally stuck in this. Any help is hugely appreciated. I think
    > I'm doing what's suggested in the examples in Core Animation
    > Programming Guide, but obviously not.
    >
    > Am I handling the layer hierarchy correctly? For example, can I use
    > the view's backing layer as the root of the layer hierarchy?
    >
    > Thaks in advance,
    > Joachim
  • On Jan 14, 2008, at 6:07 PM, Bill Dudney wrote:

    > Hi Joachim,
    >
    > You should not be manipulating the layer that the view creates for
    > its self as a general rule.

    True.

    If you don't create it and set it, you shoudn't mess with it.

    >
    >

    > If you call [self setLayer:[CALayer layer]] right before you call
    > [self setWantsLayer:YES] you should start to see behavior that more
    > closely matches what you want/expect.

    This is how you should always do it.  Set the layer to your requested
    layer class, then enabled layers.

    But, no, you won't see the behavior you expect.

    > CABasicAnimation *anim = [CABasicAnimation
    > animationWithKeyPath:@"frameOrigin"];
    >
    > // Configure animation
    > anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
    > anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    > 100)];
    > anim.timingFunction = [CAMediaTimingFunction
    > functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    > anim.duration      = 2.0;
    > anim.delegate      = self;
    >
    > // Add animation to layer; also triggers the animation
    > [layer addAnimation:anim forKey:@"frameOrigin"];

    oints 1 - frameOrigin isn't a property of CALayer.  position is. You
    can only animate properties of a layer that exist, and that are marked
    as animatable in the CALayer class. conceptual doc does cover this.
    Perhaps you're confusing with the view frameOrigin method?

    point 2 - you're using explicit animation and you're using the default
    fillMode (kCAFillModeRemoved). This causes the explicit animation to
    be removed when the animation is over. So you blink, and it's gone
    back to the start point.

    BTW, I'm not sure you want an explicit animation in this case
    anyways.. you'd expect that the layer would keep its 100,100 location,
    wouldn't you? in that case you could just use implicit animation.  If
    you really want the different timing function than the default you can
    change the implicit animation for the "frame" property.

    point 3 is because you're using the wrong delegate method if you're
    using animationDidStop: this isn't a CALayer delegate method.
    animationDidStop:finished: is.
  • Bill, Scott,

    Thanks for setting this straight! This was a great help. Having read
    the documentation numerous times, I still wasn't clear on how to add
    my own root layer in a layer-hosting view. I thought I could "reuse"
    the layer-backed view layer, set in IB and/or code. Apparently not.
    Also, animating the wrong property doesn't exactly make things happen
    either. :-)

    The reason I use a layer-hosted view and explicit animations is
    because I need some more complicated animations to happen and several
    of them. As many as 30-40 layers animating at a time. The code below
    is just to make even a simple animation work for me.

    I now got the layer to move, but a few more problems remain...

    - (void)animateNewLayer
    {
        // Create animating layer
        CALayer *layer = [CALayer layer];

        // Configure layer
        layer.anchorPoint = CGPointMake (0, 0); // Use lower/left corner
    as with views
        layer.frame = CGRectMake (0, 0, 24, 24);
        layer.contents = myCGImageRef; // Created earlier in this method
        CGImageRelease (myCGImageRef);

        // If we don't have a root layer, create and set it
        if (!self.layer) {
            [self setLayer:[CALayer layer]];
            [self setWantsLayer:YES];
        }
        [[self layer] addSublayer:layer]; // Add our animating sublayer

        // Create animation
        CABasicAnimation *anim = [CABasicAnimation
    animationWithKeyPath:@"position"];

        // Configure animation
        anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
        anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    100)];
        anim.timingFunction = [CAMediaTimingFunction
    functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
        anim.duration      = 2.0;
        animation.fillMode  = kCAFillModeForwards;
        anim.delegate      = self;

        // Add animation to layer; also triggers the animation
        [layer addAnimation:anim forKey:@"position"];
    }

    I think I'm now setting the root layer as I should. If the root layer
    doesn't exist, I create and add it to the hosting view and then add
    and remove sublayers in the lifetime of the view as needed. The
    hosting view never goes away, and so neither does the root layer once
    it's added.

    These are the problems I see:

    1) When the layer starts animating, the hosting view dissapears and
    never reappears (this also happened in the original code)
    2) Setting the fillMode to kCAFillModeForwards still makes the layer
    jump back to its original position (or opacity or whatever I animate
    on) after the animation is done

    Once again, thank you for taking the time.

    Joachim

    On 15-Jan-08, at 9:10 AM, Scott Anguish wrote:

    > On Jan 14, 2008, at 6:07 PM, Bill Dudney wrote:
    >
    >> Hi Joachim,
    >>
    >> You should not be manipulating the layer that the view creates for
    >> its self as a general rule.
    >
    > True.
    >
    > If you don't create it and set it, you shoudn't mess with it.
    >
    >> If you call [self setLayer:[CALayer layer]] right before you call
    >> [self setWantsLayer:YES] you should start to see behavior that more
    >> closely matches what you want/expect.
    >
    > This is how you should always do it.  Set the layer to your
    > requested layer class, then enabled layers.
    >
    > But, no, you won't see the behavior you expect.
    >
    >> CABasicAnimation *anim = [CABasicAnimation
    >> animationWithKeyPath:@"frameOrigin"];
    >>
    >> // Configure animation
    >> anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
    >> anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    >> 100)];
    >> anim.timingFunction = [CAMediaTimingFunction
    >> functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    >> anim.duration      = 2.0;
    >> anim.delegate      = self;
    >>
    >> // Add animation to layer; also triggers the animation
    >> [layer addAnimation:anim forKey:@"frameOrigin"];
    >
    > oints 1 - frameOrigin isn't a property of CALayer.  position is. You
    > can only animate properties of a layer that exist, and that are
    > marked as animatable in the CALayer class. conceptual doc does cover
    > this.  Perhaps you're confusing with the view frameOrigin method?
    >
    > point 2 - you're using explicit animation and you're using the
    > default fillMode (kCAFillModeRemoved). This causes the explicit
    > animation to be removed when the animation is over. So you blink,
    > and it's gone back to the start point.
    >
    > BTW, I'm not sure you want an explicit animation in this case
    > anyways.. you'd expect that the layer would keep its 100,100
    > location, wouldn't you? in that case you could just use implicit
    > animation.  If you really want the different timing function than
    > the default you can change the implicit animation for the "frame"
    > property.
    >
    > point 3 is because you're using the wrong delegate method if you're
    > using animationDidStop: this isn't a CALayer delegate method.
    > animationDidStop:finished: is.
  • Hi Joachim,

    On Jan 15, 2008, at 7:38 AM, Joachim wrote:

    > Bill, Scott,
    >
    > Thanks for setting this straight! This was a great help. Having read
    > the documentation numerous times, I still wasn't clear on how to add
    > my own root layer in a layer-hosting view. I thought I could "reuse"
    > the layer-backed view layer, set in IB and/or code. Apparently not.
    > Also, animating the wrong property doesn't exactly make things
    > happen either. :-)
    >
    > The reason I use a layer-hosted view and explicit animations is
    > because I need some more complicated animations to happen and
    > several of them. As many as 30-40 layers animating at a time. The
    > code below is just to make even a simple animation work for me.
    >
    > I now got the layer to move, but a few more problems remain...
    >
    > - (void)animateNewLayer
    > {
    > // Create animating layer
    > CALayer *layer = [CALayer layer];
    >
    > // Configure layer
    > layer.anchorPoint = CGPointMake (0, 0); // Use lower/left corner
    > as with views
    > layer.frame = CGRectMake (0, 0, 24, 24);
    > layer.contents = myCGImageRef; // Created earlier in this method
    > CGImageRelease (myCGImageRef);
    >
    > // If we don't have a root layer, create and set it
    > if (!self.layer) {
    > [self setLayer:[CALayer layer]];
    > [self setWantsLayer:YES];
    > }
    > [[self layer] addSublayer:layer]; // Add our animating sublayer
    >
    > // Create animation
    > CABasicAnimation *anim = [CABasicAnimation
    > animationWithKeyPath:@"position"];
    >
    > // Configure animation
    > anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
    > anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    > 100)];
    > anim.timingFunction = [CAMediaTimingFunction
    > functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    > anim.duration      = 2.0;
    > animation.fillMode  = kCAFillModeForwards;
    > anim.delegate      = self;
    >
    > // Add animation to layer; also triggers the animation
    > [layer addAnimation:anim forKey:@"position"];
    > }
    >
    > I think I'm now setting the root layer as I should. If the root
    > layer doesn't exist, I create and add it to the hosting view and
    > then add and remove sublayers in the lifetime of the view as needed.
    > The hosting view never goes away, and so neither does the root layer
    > once it's added.
    >
    > These are the problems I see:
    >
    > 1) When the layer starts animating, the hosting view dissapears and
    > never reappears (this also happened in the original code)

    Hm... are you drawing into the view? That is another no-no, once you
    take over the layer of a view you should not draw in it (with the
    drawRect: method) but instead do all your drawing via the layer.

    >
    > 2) Setting the fillMode to kCAFillModeForwards still makes the layer
    > jump back to its original position (or opacity or whatever I animate
    > on) after the animation is done

    Not sure what this is all about. Are you ever setting the value or
    just depending on the animation to set it for you?

    >
    >
    > Once again, thank you for taking the time.

    my pleasure - HTH,

    -bd-
    http://bill.dudney.net/roller/objc

    >
    >
    > Joachim
    >
    >
    > On 15-Jan-08, at 9:10 AM, Scott Anguish wrote:
    >
    >> On Jan 14, 2008, at 6:07 PM, Bill Dudney wrote:
    >>
    >>> Hi Joachim,
    >>>
    >>> You should not be manipulating the layer that the view creates for
    >>> its self as a general rule.
    >>
    >> True.
    >>
    >> If you don't create it and set it, you shoudn't mess with it.
    >>
    >>> If you call [self setLayer:[CALayer layer]] right before you call
    >>> [self setWantsLayer:YES] you should start to see behavior that
    >>> more closely matches what you want/expect.
    >>
    >> This is how you should always do it.  Set the layer to your
    >> requested layer class, then enabled layers.
    >>
    >> But, no, you won't see the behavior you expect.
    >>
    >>> CABasicAnimation *anim = [CABasicAnimation
    >>> animationWithKeyPath:@"frameOrigin"];
    >>>
    >>> // Configure animation
    >>> anim.fromValue      = [NSValue valueWithPoint:NSMakePoint (0, 0)];
    >>> anim.toValue        = [NSValue valueWithPoint:NSMakePoint (100,
    >>> 100)];
    >>> anim.timingFunction = [CAMediaTimingFunction
    >>> functionWithName:kCAMediaTimingFunctionEaseInEaseOut];
    >>> anim.duration      = 2.0;
    >>> anim.delegate      = self;
    >>>
    >>> // Add animation to layer; also triggers the animation
    >>> [layer addAnimation:anim forKey:@"frameOrigin"];
    >>
    >> oints 1 - frameOrigin isn't a property of CALayer.  position is.
    >> You can only animate properties of a layer that exist, and that are
    >> marked as animatable in the CALayer class. conceptual doc does
    >> cover this.  Perhaps you're confusing with the view frameOrigin
    >> method?
    >>
    >> point 2 - you're using explicit animation and you're using the
    >> default fillMode (kCAFillModeRemoved). This causes the explicit
    >> animation to be removed when the animation is over. So you blink,
    >> and it's gone back to the start point.
    >>
    >> BTW, I'm not sure you want an explicit animation in this case
    >> anyways.. you'd expect that the layer would keep its 100,100
    >> location, wouldn't you? in that case you could just use implicit
    >> animation.  If you really want the different timing function than
    >> the default you can change the implicit animation for the "frame"
    >> property.
    >>
    >> point 3 is because you're using the wrong delegate method if you're
    >> using animationDidStop: this isn't a CALayer delegate method.
    >> animationDidStop:finished: is.

  • On 15-Jan-08, at 9:54 PM, Bill Dudney wrote:

    >> 1) When the layer starts animating, the hosting view dissapears and
    >> never reappears (this also happened in the original code)
    >
    > Hm... are you drawing into the view? That is another no-no, once you
    > take over the layer of a view you should not draw in it (with the
    > drawRect: method) but instead do all your drawing via the layer.

    Ahem... No comments :-)
    You're right. I need to use the delegate method instead. In my case
    drawLayer:inContext:.

    >> 2) Setting the fillMode to kCAFillModeForwards still makes the
    >> layer jump back to its original position (or opacity or whatever I
    >> animate on) after the animation is done
    >
    > Not sure what this is all about. Are you ever setting the value or
    > just depending on the animation to set it for you?

    Adding animation.removedOnCompletion = NO solved this.

    Once again, thanks for the help.

    Joachim
previous month january 2008 next month
MTWTFSS
  1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31      
Go to today