Como identificar CAAnimation no delegado animationDidStop?

102

Tive um problema em que havia uma série de sequências CATransition / CAAnimation sobrepostas, todas as quais eu precisava para realizar operações personalizadas quando as animações parassem, mas eu queria apenas um manipulador delegado para animationDidStop.

No entanto, eu tive um problema, não parecia haver uma maneira de identificar exclusivamente cada CATransition / CAAnimation no delegado animationDidStop.

Resolvi esse problema por meio do sistema de chave / valor exposto como parte do CAAnimation.

Ao iniciar sua animação, use o método setValue em CATransition / CAAnimation para definir seus identificadores e valores a serem usados ​​quando o animationDidStop for acionado:

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

Em seu delegado 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.
 }

O outro aspecto disso é que ele permite que você mantenha o estado no sistema de emparelhamento de valor-chave em vez de ter que armazená-lo em sua classe de delegado. Quanto menos código, melhor.

Certifique-se de verificar a Referência da Apple sobre codificação de pares de valores-chave .

Existem técnicas melhores para identificação de CAAnimation / CATransition no delegado animationDidStop?

Obrigado, - Batgar

Batgar
fonte
4
Batgar, Quando pesquisei "iphone animationDidStop identifique", o primeiro hit foi sua postagem, sugerindo o uso de key-value para identificar a animação. Exatamente o que eu precisava, obrigado. Rudi
rudifa de
1
Esteja ciente de que CAAnimation's delegateé forte, então talvez seja necessário configurá-lo para nilevitar reter ciclos!
Iulian Onofrei

Respostas:

92

A técnica de Batgar é muito complicada. Por que não tirar vantagem do parâmetro forKey em addAnimation? Foi planejado exatamente para este propósito. Basta realizar a chamada para setValue e mover a string chave para a chamada addAnimation. Por exemplo:

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

Então, em seu retorno de chamada animationDidStop, você pode fazer algo como:

if (theAnimation == [[hearingAidHalo layer] animationForKey:@"Throb"]) ...
vocaro
fonte
Eu gostaria de mencionar que usando o acima, INCREMENTA A CONTAGEM DE RETENÇÃO! Esteja avisado. Ou seja, animationForKey: incrementa a contagem de retenção de seu objeto CAAnimation.
mmilo
1
@mmilo Isso não é muito surpreendente, não é? Ao adicionar uma animação a uma camada, a camada fica com a animação, portanto, a contagem de retenção da animação é incrementada.
GorillaPatch de
16
Não funciona - no momento em que o seletor de parada é chamado, a animação não existe mais. Você obtém uma referência nula.
Adam
4
Isso é um uso indevido do parâmetro forKey: e não há necessidade disso. O que Batgar estava fazendo é exatamente correto - a codificação de valor-chave permite que você anexe quaisquer dados arbitrários à sua animação, para que possa identificá-los facilmente.
matt
7
Adam, veja a resposta de Jimt abaixo - você deve definir anim.removedOnCompletion = NO;para que ele ainda exista quando -animationDidStop:finished:for chamado.
Yang Meyer
46

Acabei de descobrir uma maneira ainda melhor de fazer o código de conclusão para CAAnimations:

Eu criei um typedef para um bloco:

typedef void (^animationCompletionBlock)(void);

E uma chave que uso para adicionar um bloco a uma animação:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Então, se eu quiser executar o código de conclusão da animação após o término de um CAAnimation, eu me coloco como delegado da animação e adiciono um bloco de código à animação usando setValue: forKey:

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

Em seguida, implemento um método animationDidStop: completed:, que verifica se há um bloco na chave especificada e o executa se encontrado:

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

A beleza dessa abordagem é que você pode escrever o código de limpeza no mesmo lugar onde cria o objeto de animação. Melhor ainda, como o código é um bloco, ele tem acesso a variáveis ​​locais no escopo envolvente em que está definido. Você não tem que mexer com a configuração de dicionários userInfo ou outras bobagens, e não tem que escrever um método de animação DidStop: terminado que fica cada vez mais complexo conforme você adiciona diferentes tipos de animações.

Verdade seja dita, CAAnimation deve ter uma propriedade de bloco de conclusão incorporada a ele e suporte do sistema para chamá-lo automaticamente se for especificado. No entanto, o código acima oferece a mesma funcionalidade com apenas algumas linhas de código extra.

Duncan C
fonte
7
Alguém também montou uma categoria em CAAnimation para isso: github.com/xissburg/CAAnimationBlocks
Jay Peyer
Isso não parece estar certo. Muitas vezes, recebo um EXEC_Err logo após theBlock();ser invocado e acredito que seja devido ao fato de que o escopo do bloco foi destruído.
Mahboudz
Eu tenho usado o bloco por um tempo, e ele funciona MUITO melhor do que a terrível abordagem "oficial" da Apple.
Adam
3
Tenho quase certeza de que você precisaria de [bloquear a cópia] desse bloco antes de defini-lo como um valor para uma propriedade.
Fiona Hopkins
1
Não, você não precisa copiar o bloco.
Duncan C de
33

A segunda abordagem só funcionará se você definir explicitamente sua animação para não ser removida na conclusão antes de executá-la:

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

Se você não fizer isso, sua animação será removida antes de ser concluída e o retorno de chamada não a encontrará no dicionário.

Jimt
fonte
10
Isso deve ser um comentário, não uma resposta.
Até
2
Eu me pergunto se é necessário removê-lo explicitamente depois com removeAnimationForKey?
bompf
Realmente depende do que você deseja fazer. Você pode removê-lo se necessário ou deixá-lo porque deseja fazer outra coisa em conjunto.
applejack42
31

Todas as outras respostas são muito complicadas! Por que você simplesmente não adiciona sua própria chave para identificar a animação?

Esta solução é muito fácil, tudo que você precisa é adicionar sua própria chave para a animação (animationID neste exemplo)

Insira esta linha para identificar a animação1 :

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

e isso para identificar a animação2 :

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

Teste assim:

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

Não requer nenhuma variável de instância :

Tibidabo
fonte
Estou recebendo algum valor int (int (0)) em animationDidStop as[animation valueForKey:@"animationID"]
abhimuralidharan
14

Para tornar explícito o que está implícito acima (e o que me trouxe aqui depois de algumas horas perdidas): não espere ver o objeto de animação original que você alocou devolvido a você por

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

quando a animação termina, porque [CALayer addAnimation:forKey:]faz uma cópia da sua animação.

O que você pode confiar é que os valores chaveados que você deu ao seu objeto de animação ainda estão lá com valor equivalente (mas não necessariamente equivalência de ponteiro) no objeto de animação de réplica passado com a animationDidStop:finished:mensagem. Conforme mencionado acima, use KVC e você terá amplo escopo para armazenar e recuperar o estado.

primeiro
fonte
1
+1 Esta é a melhor solução! Você pode definir o 'nome' da animação com [animation setValue:@"myanim" forKey:@"name"]e pode até definir a camada que está sendo animada usando [animation setValue:layer forKey:@"layer"]. Esses valores podem então ser recuperados nos métodos delegados.
trojanfoe
valueForKey:retorna nilpara mim, alguma ideia do porquê?
Iulian Onofrei
@IulianOnofrei verifique se sua animação não foi substituída por outra animação da mesma propriedade - pode acontecer como um efeito colateral inesperado.
partir de
@ t0rst, Desculpe, tendo várias animações e usando copiar e colar, eu estava definindo valores diferentes na mesma variável de animação.
Iulian Onofrei
2

Posso ver a maioria das respostas objc. Farei uma para o swift 2.3 com base na melhor resposta acima.

Para começar, será bom armazenar todas essas chaves em um struct privado para que seja seguro para tipos e alterá-lo no futuro não trará bugs irritantes apenas porque você se esqueceu de alterá-lo em todo o código:

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

Como você pode ver, mudei os nomes das variáveis ​​/ animações para que fique mais claro. Agora definindo essas chaves quando a animação é criada.

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

(...)

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

Então, finalmente, lidar com o delegado para quando a animação parar

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
        }
    }
}
apinho
fonte
0

IMHO usando o valor-chave da Apple é a maneira elegante de fazer isso: destina-se especificamente a permitir a adição de dados específicos do aplicativo aos objetos.

Outra possibilidade muito menos elegante é armazenar referências a seus objetos de animação e fazer uma comparação de ponteiro para identificá-los.

Teemu Kurppa
fonte
Isso nunca funcionará - você não pode fazer equivalência de ponteiro, porque a Apple muda o ponteiro.
Adam
0

Para eu verificar se 2 objetos CABasicAnimation são a mesma animação, eu uso a função keyPath para fazer exatamente isso.

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

  • Não há necessidade de definir KeyPath para CABasicAnimation, pois ele não será mais animado
Sirisilp Kongsilp
fonte
a questão está relacionada a retornos de chamada delegados e keyPath não é um método em CAAnimation
Max MacLeod
0

Gosto de usar setValue:forKey: para manter uma referência da visualização que estou animando, é mais seguro do que tentar identificar de forma única a animação com base no ID, porque o mesmo tipo de animação pode ser adicionado a diferentes camadas.

Esses dois são equivalentes:

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

com este:

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;

e no método delegado:

- (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];
    }
}
Andrei Marincas
fonte
0

Xcode 9 Swift 4.0

Você pode usar os valores-chave para relacionar uma animação adicionada à animação retornada no método de delegado animationDidStop.

Declare um dicionário para conter todas as animações ativas e conclusões relacionadas:

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

Ao adicionar sua animação, defina uma chave para ela:

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

Em animationDidStop, a mágica acontece:

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