如何识别 AnimationDidStop 委托中的 CAAnimation?

我遇到了一个问题,我有一系列重叠的 CATransfer/CAAnimation 序列,当动画停止时,所有这些序列都需要执行自定义操作,但是我只需要一个 AnimationDidStop 的委托处理程序。

然而,我遇到了一个问题,似乎没有一种方法可以唯一地标识 animationDidStop 委托中的每个 CATransfer/CAAnimation。

我通过 CAAnimation 中公开的键/值系统解决了这个问题。

当你开始你的动画时,使用 CATransfer/CAAnimation 上的 setValue 方法来设置你的标识符和值,当 animationDidStop 触发时使用:

-(void)volumeControlFadeToOrange
{
CATransition* volumeControlAnimation = [CATransition animation];
[volumeControlAnimation setType:kCATransitionFade];
[volumeControlAnimation setSubtype:kCATransitionFromTop];
[volumeControlAnimation setDelegate:self];
[volumeControlLevel setBackgroundImage:[UIImage imageNamed:@"SpecialVolume1.png"] forState:UIControlStateNormal];
volumeControlLevel.enabled = true;
[volumeControlAnimation setDuration:0.7];
[volumeControlAnimation setValue:@"Special1" forKey:@"MyAnimationType"];
[[volumeControlLevel layer] addAnimation:volumeControlAnimation forKey:nil];
}


- (void)throbUp
{
doThrobUp = true;


CATransition *animation = [CATransition animation];
[animation setType:kCATransitionFade];
[animation setSubtype:kCATransitionFromTop];
[animation setDelegate:self];
[hearingAidHalo setBackgroundImage:[UIImage imageNamed:@"m13_grayglow.png"] forState:UIControlStateNormal];
[animation setDuration:2.0];
[animation setValue:@"Throb" forKey:@"MyAnimationType"];
[[hearingAidHalo layer] addAnimation:animation forKey:nil];
}

在你的 AnimationDidStop 代表:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag{


NSString* value = [theAnimation valueForKey:@"MyAnimationType"];
if ([value isEqualToString:@"Throb"])
{
//... Your code here ...
return;
}




if ([value isEqualToString:@"Special1"])
{
//... Your code here ...
return;
}


//Add any future keyed animation operations when the animations are stopped.
}

这样做的另一个方面是,它允许您将状态保存在键值配对系统中,而不必将其存储在委托类中。代码越少越好。

一定要看看 苹果关键值对编码的借鉴

在 AnimationDidStop 委托中,是否有更好的 CAAnimation/CA 过渡识别技术?

谢谢, 巴特加

52297 次浏览

IMHO using Apple's key-value is the elegant way of doing this: it's specifically meant to allow adding application specific data to objects.

Other much less elegant possibility is to store references to your animation objects and do a pointer comparision to identify them.

Batgar's technique is too complicated. Why not take advantage of the forKey parameter in addAnimation? It was intended for this very purpose. Just take out the call to setValue and move the key string to the addAnimation call. For example:

[[hearingAidHalo layer] addAnimation:animation forKey:@"Throb"];

Then, in your animationDidStop callback, you can do something like:

if (theAnimation == [[hearingAidHalo layer] animationForKey:@"Throb"]) ...

The second approach will only work if you explicitly set your animation to not be removed on completion before running it:

CAAnimation *anim = ...
anim.removedOnCompletion = NO;

If you fail to do so, your animation will get removed before when it completes, and the callback will not find it in the dictionary.

To make explicit what's implied from above (and what brought me here after a few wasted hours): don't expect to see the original animation object that you allocated passed back to you by

 - (void)animationDidStop:(CAAnimation*)animation finished:(BOOL)flag

when the animation finishes, because [CALayer addAnimation:forKey:] makes a copy of your animation.

What you can rely on, is that the keyed values you gave to your animation object are still there with equivalent value (but not necessarily pointer equivalence) in the replica animation object passed with the animationDidStop:finished: message. As mentioned above, use KVC and you get ample scope to store and retrieve state.

I just came up with an even better way to do completion code for CAAnimations:

I created a typedef for a block:

typedef void (^animationCompletionBlock)(void);

And a key that I use to add a block to an animation:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Then, if I want to run animation completion code after a CAAnimation finishes, I set myself as the delegate of the animation, and add a block of code to the animation using setValue:forKey:

animationCompletionBlock theBlock = ^void(void)
{
//Code to execute after the animation completes goes here
};
[theAnimation setValue: theBlock forKey: kAnimationCompletionBlock];

Then, I implement an animationDidStop:finished: method, that checks for a block at the specified key and executes it if found:

- (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag
{
animationCompletionBlock theBlock = [theAnimation valueForKey: kAnimationCompletionBlock];
if (theBlock)
theBlock();
}

The beauty of this approach is that you can write the cleanup code in the same place where you create the animation object. Better still, since the code is a block, it has access to local variables in the enclosing scope in which it's defined. You don't have to mess with setting up userInfo dictionaries or other such nonsense, and don't have to write an ever-growing animationDidStop:finished: method that gets more and more complex as you add different kinds of animations.

Truth be told, CAAnimation should have a completion block property built into it, and system support for calling it automatically if one is specified. However, the above code gives you that same functionality with only a few lines of extra code.

For me to check if 2 CABasicAnimation object are the same animation, I use keyPath function to do exactly as that.

if([animationA keyPath] == [animationB keyPath])

  • There are no need to set KeyPath for CABasicAnimation as it will no longer animate

All other answers are way too complicated! Why don't you just add your own key to identify the animation?

This solution is very easy all you need is to add your own key to the animation (animationID in this example)

Insert this line to identify animation1:

[myAnimation1 setValue:@"animation1" forKey:@"animationID"];

and this to identify animation2:

[myAnimation2 setValue:@"animation2" forKey:@"animationID"];

Test it like this:

- (void)animationDidStop:(CAAnimation *)animation finished:(BOOL)flag
{
if([[animation valueForKey:@"animationID"] isEqual:@"animation1"]) {
//animation is animation1


} else if([[animation valueForKey:@"animationID"] isEqual:@"animation2"]) {
//animation is animation2


} else {
//something else
}
}

It does not require any instance variables:

I like to use setValue:forKey: to keep a reference of the view I'm animating, it's more safe than trying to uniquely identify the animation based on ID because the same kind of animation can be added to different layers.

These two are equivalent:

[UIView animateWithDuration: 0.35
animations: ^{
myLabel.alpha = 0;
} completion: ^(BOOL finished) {
[myLabel removeFromSuperview];
}];

with this one:

CABasicAnimation *fadeOut = [CABasicAnimation animationWithKeyPath:@"opacity"];
fadeOut.fromValue = @([myLabel.layer opacity]);
fadeOut.toValue = @(0.0);
fadeOut.duration = 0.35;
fadeOut.fillMode = kCAFillModeForwards;
[fadeOut setValue:myLabel forKey:@"item"]; // Keep a reference to myLabel
fadeOut.delegate = self;
[myLabel.layer addAnimation:fadeOut forKey:@"fadeOut"];
myLabel.layer.opacity = 0;

and in the delegate method:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
id item = [anim valueForKey:@"item"];


if ([item isKindOfClass:[UIView class]])
{
// Here you can identify the view by tag, class type
// or simply compare it with a member object


[(UIView *)item removeFromSuperview];
}
}

I can see mostly objc answers I will make one for swift 2.3 based on the best answer above.

For a start it will be good to store all those keys on a private struct so it is type safe and changing it in the future won't bring you annoying bugs just because you forgot to change it everywhere in the code:

private struct AnimationKeys {
static let animationType = "animationType"
static let volumeControl = "volumeControl"
static let throbUp = "throbUp"
}

As you can see I have changed the names of the variables/animations so it is more clear. Now setting these keys when the animation is created.

volumeControlAnimation.setValue(AnimationKeys.volumeControl, forKey: AnimationKeys.animationType)

(...)

throbUpAnimation.setValue(AnimationKeys.throbUp, forKey: AnimationKeys.animationType)

Then finally handling the delegate for when the animation stops

override func animationDidStop(anim: CAAnimation, finished flag: Bool) {
if let value = anim.valueForKey(AnimationKeys.animationType) as? String {
if value == AnimationKeys.volumeControl {
//Do volumeControl handling
} else if value == AnimationKeys.throbUp {
//Do throbUp handling
}
}
}

Xcode 9 Swift 4.0

You can use Key Values to relate an animation you added to the animation returned in animationDidStop delegate method.

Declare a dictionary to contain all active animations and related completions:

 var animationId: Int = 1
var animating: [Int : () -> Void] = [:]

When you add your animation, set a key for it:

moveAndResizeAnimation.setValue(animationId, forKey: "CompletionId")
animating[animationId] = {
print("completion of moveAndResize animation")
}
animationId += 1

In animationDidStop, the magic happens:

    let animObject = anim as NSObject
if let keyValue = animObject.value(forKey: "CompletionId") as? Int {
if let completion = animating.removeValue(forKey: keyValue) {
completion()
}
}