7

In a CALayer subclass I'm working on I have a custom property that I want to animate automatically, that is, assuming the property is called "myProperty", I want the following code:

[myLayer setMyProperty:newValue];

To cause a smooth animation from the current value to "newValue".

Using the approach of overriding actionForKey: and needsDisplayForKey: (see following code) I was able to get it to run very nicely to simply interpolate between the old and and new value.

My problem is that I want to use a slightly different animation duration or path (or whatever) depending on both the current value and the new value of the property and I wasn't able to figure out how to get the new value from within actionForKey:

Thanks in advance

@interface ERAnimatablePropertyLayer : CALayer {
    float myProperty;
}
@property (nonatomic, assign) float myProperty;
@end
@implementation ERAnimatablePropertyLayer
@dynamic myProperty;

- (void)drawInContext:(CGContextRef)ctx {
     ... some custom drawing code based on "myProperty"
}

- (id <CAAction>)actionForKey:(NSString *)key {
    if ([key isEqualToString:@"myProperty"]) {
        CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:key];
        theAnimation.fromValue = [[self presentationLayer] valueForKey:key];

        ... I want to do something special here, depending on both from and to values...


        return theAnimation;
        }
    return [super actionForKey:key];
    }

+ (BOOL)needsDisplayForKey:(NSString *)key {
    if ([key isEqualToString:@"myProperty"])
        return YES;
    return [super needsDisplayForKey:key];
    }
    @end
Eyal Redler
  • 438
  • 6
  • 18

3 Answers3

2

You need to avoid custom getters and setters for properties you want to animate.

Override the didChangeValueForKey: method. Use it to set the model value for the property you want to animate.

Don't set the toValue on the action animation.

@interface MyLayer: CALayer

    @property ( nonatomic ) NSUInteger state;

@end

-

@implementation MyLayer

@dynamic state;

- (id<CAAction>)actionForKey: (NSString *)key {

    if( [key isEqualToString: @"state"] )
    {
        CABasicAnimation * bgAnimation = [CABasicAnimation animationWithKeyPath: @"backgroundColor"];
        bgAnimation.fromValue = [self.presentationLayer backgroundColor];
        bgAnimation.duration  = 0.4;

        return bgAnimation;
    }

    return [super actionForKey: key];
}

- (void)didChangeValueForKey: (NSString *)key {

    if( [key isEqualToString: @"state"] )
    {
        const NSUInteger state = [self valueForKey: key];
        UIColor * newBackgroundColor;

        switch (state) 
        {
            case 0: 
                newBackgroundColor = [UIColor redColor]; 
                break;

            case 1: 
                newBackgroundColor = [UIColor blueColor]; 
                break;

            case 2: 
                newBackgroundColor = [UIColor greenColor]; 
                break;

            default: 
                newBackgroundColor = [UIColor purpleColor]; 
        }

        self.backgroundColor = newBackgroundColor.CGColor;
    }

    [super didChangeValueForKey: key];
}

@end
karwag
  • 2,641
  • 1
  • 15
  • 12
  • In your custom accessors, you will need to call [self willChangeValueForKey:key], set the value, and then call [self didChangeValueForKey:key]. – SG1 Sep 15 '13 at 02:40
2

Core Animation calls actionForKey: before updating the property value. It runs the action after updating the property value by sending it runActionForKey:object:arguments:. The CAAnimation implementation of runActionForKey:object:arguments: just calls [object addAnimation:self forKey:key].

Instead of returning the animation from actionForKey:, you can return a CAAction that, when run, creates and installs an animation. Something like this:

@interface MyAction: NSObject <CAAction>
@property (nonatomic, strong) id priorValue;
@end

@implementation MyAction

- (void)runActionForKey:(NSString *)key object:(id)anObject arguments:(NSDictionary *)dict {
    ERAnimatablePropertyLayer *layer = anObject;
    CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:key];
    id newValue = [layer valueForKey:key];
    // Set up animation using self.priorValue and newValue to determine
    // fromValue and toValue. You could use a CAKeyframeAnimation instead of
    // a CABasicAnimation.
    [layer addAnimation:animation forKey:key];
}

@end

@implementation ERAnimatablePropertyLayer

- (id <CAAction>)actionForKey:(NSString *)key {
    if ([key isEqualToString:@"myProperty"]) {
        MyAction *action = [[MyAction alloc] init];
        action.priorValue = [self valueForKey:key];
        return action;
    }
    return [super actionForKey:key];
}

You can find a working example of a similar technique (in Swift, animating cornerRadius) in this answer.

Community
  • 1
  • 1
rob mayoff
  • 375,296
  • 67
  • 796
  • 848
1

You can store the old and new values in CATransaction.

-(void)setMyProperty:(float)value
{
    NSNumber *fromValue = [NSNumber numberWithFloat:myProperty];
    [CATransaction setValue:fromValue forKey:@"myPropertyFromValue"];

    myProperty = value;

    NSNumber *toValue = [NSNumber numberWithFloat:myProperty];
    [CATransaction setValue:toValue forKey:@"myPropertyToValue"];
}

- (id <CAAction>)actionForKey:(NSString *)key {
    if ([key isEqualToString:@"myProperty"]) {
        CABasicAnimation *theAnimation = [CABasicAnimation animationWithKeyPath:key];
        theAnimation.fromValue = [[self presentationLayer] valueForKey:key];
        theAnimation.toValue = [CATransaction objectForKey:@"myPropertyToValue"];

        // here you do something special.
    }

    return [super actionForKey:key];
}
Simon
  • 25,468
  • 44
  • 152
  • 266
  • Implementing an accessor for @dynamic property (myProperty) makes all the magic go away – CA just stops initiating implicit transactions for the property. – zrslv Aug 11 '12 at 08:35
  • Worth knowing, but given that the OP wants an explicit animation I'd say it's not a problem. – Simon Aug 12 '12 at 10:38
  • OP wants an implicit one. "I have a custom property that I want to animate automatically". Also, myProperty is @dynamic in the original snippet. – zrslv Aug 12 '12 at 15:06
  • automatic isn't the same as implicit - the OP wants to specify the exact animation used, rather than use a built-in one. – Simon Aug 12 '12 at 17:28
  • 1
    "Core Animation also supports an explicit animation model. The explicit animation model requires that you create an animation object, and set start and end values. An explicit animation won’t start until you apply the animation to a layer." [Core Animation Programming Guide: Animation](https://developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/CoreAnimation_guide/Articles/AnimatingLayers.html) – zrslv Aug 12 '12 at 20:58
  • And yes, the right term for "automatic" in this case is "implicit". – zrslv Aug 12 '12 at 21:00
  • The OP wants to create an animation object, and set start and end values. – Simon Aug 13 '12 at 06:17
  • let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/15260/discussion-between-zrxq-and-simon) – zrslv Aug 13 '12 at 10:01