【问题标题】:Custom property animation with actionForKey: how do I get the new value for the property?使用 actionForKey 的自定义属性动画:如何获取属性的新值?
【发布时间】:2023-03-12 10:31:01
【问题描述】:

在我正在处理的 CALayer 子类中,我有一个想要自动设置动画的自定义属性,也就是说,假设该属性称为“myProperty”,我想要以下代码:

[myLayer setMyProperty:newValue];

从当前值到“newValue”的平滑动画。

使用覆盖 actionForKey: 和 needsDisplayForKey: 的方法(参见以下代码)我能够让它运行得非常好,只需在新旧值之间进行插值。

我的问题是我想根据属性的当前值和新值使用稍微不同的动画持续时间或路径(或其他),但我无法弄清楚如何获取新值从 actionForKey 内部:

提前致谢

@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

【问题讨论】:

    标签: core-animation


    【解决方案1】:

    您需要避免为要设置动画的属性自定义 getter 和 setter。

    覆盖didChangeValueForKey: 方法。用它来设置你想要动画的属性的模型值。

    不要在动作动画上设置 toValue。

    @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
    

    【讨论】:

    • 在您的自定义访问器中,您需要调用 [self willChangeValueForKey:key],设置值,然后调用 [self didChangeValueForKey:key]。
    【解决方案2】:

    Core Animation 在更新属性值之前调用actionForKey:。它通过发送runActionForKey:object:arguments: 更新属性值后运行该操作。 runActionForKey:object:arguments:CAAnimation 实现只调用 [object addAnimation:self forKey:key]

    您可以返回CAAction,而不是从actionForKey: 返回动画,该CAAction 在运行时创建并安装动画。像这样的:

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

    您可以找到类似技术的工作示例(在 Swift 中,动画 cornerRadiusin this answer

    【讨论】:

    • 很好的答案!
    【解决方案3】:

    您可以在 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];
    }
    

    【讨论】:

    • 为@dynamic 属性 (myProperty) 实现访问器使所有魔法都消失了——CA 只是停止为属性启动隐式事务。
    • 值得知道,但鉴于 OP 想要一个明确的动画,我会说这不是问题。
    • OP 想要一个隐含的。 “我有一个要自动设置动画的自定义属性”。另外,myProperty 在原始 sn-p 中是 @dynamic。
    • 自动与隐式不同 - OP 希望指定使用的确切动画,而不是使用内置动画。
    • "Core Animation 还支持显式动画模型。显式动画模型要求您创建一个动画对象,并设置开始和结束值。显式动画只有在您将动画应用于一层。” Core Animation Programming Guide: Animation
    猜你喜欢
    • 2015-08-13
    • 2010-10-29
    • 2013-04-16
    • 2015-04-26
    • 2011-03-18
    • 2013-05-30
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多