CAGradientLayer properties not animating within UIView animation block
Asked Answered
B

2

13

I have a feeling I'm overlooking something elementary, but what better way to find it than to be wrong on the internet?

I have a fairly basic UI. The view for my UIViewController is a subclass whose +layerClass is CAGradientLayer. Depending on the user's actions, I need to move some UI elements around, and change the values of the background's gradient. The code looks something like this:

[UIView animateWithDuration:0.3 animations:^{
  self.subview1.frame = CGRectMake(...);
  self.subview2.frame = CGRectMake(...);
  self.subview2.alpha = 0;

  NSArray* newColors = [NSArray arrayWithObjects:
                         (id)firstColor.CGColor,
                         (id)secondColor.CGColor,
                         nil];
  [(CAGradientLayer *)self.layer setColors:newColors];
}];

The issue is that the changes I make in this block to the subviews animate just fine (stuff moves and fades), but the change to the gradient's colors does not. It just swaps.

Now, the documentation does say that Core Animation code within an animation block won't inherit the block's properties (duration, easing, etc.). But is it the case that that doesn't define an animation transaction at all? (The implication of the docs seems to be that you'll get a default animation, where I get none.)

Do I have to use explicit CAAnimation to make this work? (And if so, why?)

Billybillycock answered 21/2, 2012 at 16:19 Comment(0)
B
13

There seem to be two things going on here. The first (as Travis correctly points out, and the documentation states) is that UIKit animations don't seem to hold any sway over the implicit animation applied to CALayer property changes. I think this is weird (UIKit must be using Core Animation), but it is what it is.

Here's a (possibly very dumb?) workaround for that problem:

  NSTimeInterval duration = 2.0; // slow things down for ease of debugging
  [UIView animateWithDuration:duration animations:^{
    [CATransaction begin];
    [CATransaction setAnimationDuration:duration];

    // ... do stuff to things here ...

    [CATransaction commit];
  }];

The other key is that this gradient layer is my view's layer. That means that my view is the layer's delegate (where, if the gradient layer was just a sublayer, it wouldn't have a delegate). And the UIView implementation of -actionForLayer:forKey: returns NSNull for the "colors" event. (Probably every event that isn't on a specific list of UIView animations.)

Adding the following code to my view will cause the color change to be animated as expected:

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
  id<CAAction> action = [super actionForLayer:layer forKey:event];
  if( [@"colors" isEqualToString:event]
      && (nil == action || (id)[NSNull null] == action) ) {
    action = [CABasicAnimation animationWithKeyPath:event];
  }
  return action;
}
Billybillycock answered 28/2, 2012 at 17:46 Comment(1)
Fantastic find! Would have never guessed at this.Detroit
C
1

You have to use explicit CAAnimations, because you're changing the value of a CALayer. UIViewAnimations work on UIView properties, but not directly on their CALayer's properties...

Actually, you should use a CABasicAnimation so that you can access its fromValue and toValue properties.

The following code should work for you:

-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
    [UIView animateWithDuration:2.0f
                          delay:0.0f
                        options:UIViewAnimationCurveEaseInOut
                     animations:^{
                         CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"colors"];
                         animation.duration = 2.0f;
                         animation.delegate = self;
                         animation.fromValue = ((CAGradientLayer *)self.layer).colors;
                         animation.toValue = [NSArray arrayWithObjects:(id)[UIColor blackColor].CGColor,(id)[UIColor whiteColor].CGColor,nil];
                         [self.layer addAnimation:animation forKey:@"animateColors"];
                     }
                     completion:nil];
}

-(void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag {
    NSString *keyPath = ((CAPropertyAnimation *)anim).keyPath;
    if ([keyPath isEqualToString:@"colors"]) {
        ((CAGradientLayer *)self.layer).colors = ((CABasicAnimation *)anim).toValue;
    }
}

There is a trick with CAAnimations in that you HAVE to explicitly set the value of the property AFTER you complete the animation.

You do this by setting the delegate, in this case I set it to the object which calls the animation, and then override its animationDidStop:finished: method to include the setting of the CAGradientLayer's colors to their final value.

You'll also have to do a bit of casting in the animationDidStop: method, to access the properties of the animation.

Choriocarcinoma answered 25/2, 2012 at 8:50 Comment(7)
The thing is, CALayer properties have implicit animations. It does seem to be the case that the animation properties defined by a UIView animation aren't applied to CALayer changes, but that doesn't explain why I'm not seeing any animation of the color change.Billybillycock
Both UIView and CALayer have implicit animations. When you trigger a UIViewAnimation, and you want to trigger an animation on its CALayer you have to this explicitly at the same time. That's what the CABasicAnimation does in the in the UIView's animations:^{} block...Choriocarcinoma
But the key is that the implicit animation isn't happening because the layer in question is the view's layer. That's a completely separate concern from whether the change is made inside of a UIView animation block. (The whole point of implicit animations is that they don't require explicit animation blocks.)Billybillycock
Of course. Your answer below is explicitly calling a CAAnimation in the end anyways. My answer achieves the same result, but lets you customize as well, just a different approach which provides the same result. You apply an action to a layer, whereas i'm adding an animation to the layer...Choriocarcinoma
The why was the important part of the question, though. I never doubted that I could just throw a CABasicAnimation at the problem, in the way you outline, and make it go away. But it wasn't at all clear why doing so was necessary. (You're also doing way more work than you need to to achieve the desired effect, even with explicit animations.)Billybillycock
Good point. I was looking through the use of Transactions or explicit animations a few days ago and decided against Transactions. I actually just spent a couple of hours working through why it's better in this case because I wasn't completely sure why I chose to not use CATransactions. For the project I'm currently working on, I need to be able to dynamically change the animation properties of a wide variety of objects, and this seems like the most flexible way to do it.Choriocarcinoma
Also, in going through the last couple hours, I ran into: xissburg.com/2011/08/05/completion-blocks-in-caanimation ... so, i was able to do away with animationDidStop:finished:Choriocarcinoma

© 2022 - 2024 — McMap. All rights reserved.