Как определить CAAnimation в делегате animationDidStop?


102

У меня возникла проблема, когда у меня была серия перекрывающихся последовательностей CATransition / CAAnimation, все из которых мне нужно было выполнять пользовательские операции при остановке анимации, но мне нужен был только один обработчик делегата для animationDidStop.

Однако у меня возникла проблема: похоже, не было способа однозначно идентифицировать каждый CATransition / CAAnimation в делегате animationDidStop.

Я решил эту проблему с помощью системы ключ / значение, представленной как часть CAAnimation.

Когда вы запускаете анимацию, используйте метод setValue в CATransition / CAAnimation, чтобы установить идентификаторы и значения, которые будут использоваться при срабатывании 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.
 }

Другой аспект этого заключается в том, что он позволяет вам сохранять состояние в системе пар ключ-значение вместо того, чтобы хранить его в вашем классе делегата. Чем меньше кода, тем лучше.

Обязательно ознакомьтесь со справочником Apple по кодированию пар ключевых значений .

Есть ли лучшие методы идентификации CAAnimation / CATransition в делегате animationDidStop?

Спасибо, Батгар


4
Batgar, Когда я погуглил по запросу "iphone animationDidStop identify", первым попаданием было ваше сообщение, предлагающее использовать ключ-значение для идентификации анимации. Как раз то, что мне было нужно, спасибо. Rudi
rudifa

1
Имейте в виду , что CAAnimation«s delegateсильна, так что вам , возможно , придется установить его , nilчтобы избежать сохранения циклов!
Юлиан Онофрей

Ответы:


92

Техника Батгара слишком сложна. Почему бы не воспользоваться параметром forKey в addAnimation? Он был предназначен именно для этого. Просто возьмите вызов setValue и переместите ключевую строку в вызов addAnimation. Например:

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

Затем в обратном вызове animationDidStop вы можете сделать что-то вроде:

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

Я хотел бы упомянуть, что использование вышеупомянутого УВЕЛИЧЕНИЯ СОХРАНЕНИЯ СЧЕТА! Имейте в виду. То есть animationForKey: увеличивает счетчик удержания вашего объекта CAAnimation.
mmilo

1
@mmilo Это не так уж и удивительно, правда? Добавляя анимацию к слою, слой становится владельцем анимации, поэтому счетчик сохранения анимации, конечно, увеличивается.
GorillaPatch

17
Не работает - к моменту вызова селектора остановки анимации уже нет. Вы получаете нулевую ссылку.
Adam

4
Это неправильное использование параметра forKey :, и в этом нет необходимости. То, что сделал Батгар, совершенно верно - кодирование «ключ-значение» позволяет вам прикреплять любые произвольные данные к вашей анимации, чтобы вы могли легко их идентифицировать.
matt

7
Адам, см. Ответ Джимта ниже - вы должны установить anim.removedOnCompletion = NO;так, чтобы он все еще существовал при -animationDidStop:finished:вызове.
Ян Мейер

46

Я только что придумал еще лучший способ сделать код завершения для CAAnimations:

Я создал typedef для блока:

typedef void (^animationCompletionBlock)(void);

И ключ, который я использую для добавления блока к анимации:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Затем, если я хочу запустить код завершения анимации после завершения CAAnimation, я устанавливаю себя в качестве делегата анимации и добавляю блок кода к анимации, используя setValue: forKey:

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

Затем я реализую метод animationDidStop: finished:, который проверяет наличие блока по указанному ключу и выполняет его, если он найден:

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

Прелесть этого подхода в том, что вы можете написать код очистки в том же месте, где вы создаете объект анимации. Еще лучше, поскольку код является блоком, он имеет доступ к локальным переменным в охватывающей области, в которой он определен. Вам не нужно возиться с настройкой словарей userInfo или другой подобной ерунды, и вам не нужно писать постоянно растущий метод animationDidStop: finished:, который становится все более и более сложным по мере добавления различных видов анимации.

По правде говоря, CAAnimation должен иметь встроенное свойство блока завершения и поддержку системы для его автоматического вызова, если он указан. Однако приведенный выше код дает вам ту же функциональность с помощью всего лишь нескольких строк дополнительного кода.


7
Кто-то также создал для этого категорию на CAAnimation: github.com/xissburg/CAAnimationBlocks
Джей Пейер,

Это не кажется правильным. Довольно часто я получаю EXEC_Err сразу после theBlock();вызова, и я считаю, что это связано с тем, что область действия блока была уничтожена.
mahboudz

Я использую блок уже некоторое время, и он работает НАМНОГО лучше, чем ужасный «официальный» подход Apple.
Адам

3
Я почти уверен, что вам понадобится [заблокировать копию] этого блока, прежде чем устанавливать его в качестве значения для свойства.
Фиона Хопкинс

1
Нет, копировать блок не нужно.
Duncan C

33

Второй подход будет работать только в том случае, если вы явно укажете, что анимация не удаляется по завершении перед ее запуском:

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

Если вы этого не сделаете, ваша анимация будет удалена до того, как завершится, и обратный вызов не найдет ее в словаре.


10
Это должен быть комментарий, а не ответ.
до

2
Интересно, нужно ли потом явно удалить его с помощью removeAnimationForKey?
bompf

Это действительно зависит от того, чем вы хотите заниматься. Вы можете удалить его, если необходимо, или оставить, потому что хотите сделать что-то еще в тандеме.
applejack42 05

31

Все остальные ответы слишком сложны! Почему бы вам просто не добавить свой собственный ключ для идентификации анимации?

Это очень простое решение, все, что вам нужно, это добавить свой собственный ключ к анимации (в этом примере animationID)

Вставьте эту строку, чтобы идентифицировать animation1 :

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

и это для идентификации animation2 :

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

Протестируйте это так:

- (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
    }
}

Не требует никаких переменных экземпляра :


Я получаю некоторое значение int (int (0)) в animationDidStop as[animation valueForKey:@"animationID"]
abhimuralidharan

14

Чтобы пояснить, что подразумевается выше (и что привело меня сюда после нескольких потраченных впустую часов): не ожидайте, что исходный объект анимации, который вы выделили, передан вам обратно

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

когда анимация закончится, потому что [CALayer addAnimation:forKey:]делает копию вашей анимации.

На что вы можете положиться, так это на то, что ключевые значения, которые вы дали своему объекту анимации, все еще существуют с эквивалентным значением (но не обязательно эквивалентностью указателя) в реплике объекта анимации, переданном с animationDidStop:finished:сообщением. Как упоминалось выше, используйте KVC, и вы получите достаточно возможностей для хранения и извлечения состояния.


1
+1 Это лучшее решение! Вы можете установить «имя» анимации с помощью, [animation setValue:@"myanim" forKey:@"name"]и вы даже можете установить анимированный слой с помощью [animation setValue:layer forKey:@"layer"]. Затем эти значения можно получить в методах делегата.
trojanfoe

valueForKey:возвращается nilдля меня, любая идея почему?
Юлиан Онофрей

@IulianOnofrei убедитесь, что ваша анимация не была заменена другой анимацией для того же свойства - это может произойти как неожиданный побочный эффект.
t0r

@ t0rst, извините, имея несколько анимаций и используя копипаст, я устанавливал разные значения для одной и той же переменной анимации.
Юлиан Онофрей

2

Я вижу в основном ответы objc, которые я сделаю для быстрого 2.3 на основе лучшего ответа выше.

Для начала было бы хорошо хранить все эти ключи в частной структуре, чтобы она была безопасной по типу, и ее изменение в будущем не принесет вам досадных ошибок только потому, что вы забыли изменить его везде в коде:

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

Как видите, я изменил имена переменных / анимаций, чтобы они были более понятными. Теперь устанавливаем эти ключи при создании анимации.

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

(...)

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

Затем, наконец, обработка делегата, когда анимация останавливается

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
        }
    }
}

0

ИМХО, использование ключа-значения Apple - это элегантный способ сделать это: он специально предназначен для добавления данных, специфичных для приложения, к объектам.

Другая, менее элегантная возможность - сохранить ссылки на объекты анимации и выполнить сравнение указателей для их идентификации.


Это никогда не сработает - вы не можете сделать эквивалент указателя, потому что Apple меняет указатель.
Adam

0

Чтобы проверить, являются ли 2 объекта CABasicAnimation одной и той же анимацией, я использую функцию keyPath, чтобы сделать именно это.

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

  • Нет необходимости устанавливать KeyPath для CABasicAnimation, так как он больше не будет анимировать

вопрос относится к обратным вызовам делегатов, а keyPath не является методом в CAAnimation
Макс Маклауд,

0

Мне нравится использовать setValue:forKey: для сохранения ссылки на вид, который я анимирую, это более безопасно, чем пытаться однозначно идентифицировать анимацию на основе идентификатора, потому что один и тот же вид анимации может быть добавлен к разным слоям.

Эти два эквивалента:

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

с этим:

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;

и в методе делегата:

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

0

Xcode 9 Swift 4.0

Вы можете использовать ключевые значения, чтобы связать анимацию, которую вы добавили, с анимацией, возвращенной в методе делегата animationDidStop.

Объявите словарь, содержащий все активные анимации и связанные дополнения:

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

Когда вы добавляете свою анимацию, установите для нее ключ:

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

В animationDidStop происходит волшебство:

    let animObject = anim as NSObject
    if let keyValue = animObject.value(forKey: "CompletionId") as? Int {
        if let completion = animating.removeValue(forKey: keyValue) {
            completion()
        }
    }
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.