Come identificare CAAnimation all'interno del delegato animationDidStop?


102

Ho avuto un problema in cui avevo una serie di sequenze CATransition / CAAnimation sovrapposte, tutte necessarie per eseguire operazioni personalizzate quando le animazioni si sono interrotte, ma volevo solo un gestore delegato per animationDidStop.

Tuttavia, ho avuto un problema, non sembrava esserci un modo per identificare in modo univoco ogni CATransition / CAAnimation nel delegato animationDidStop.

Ho risolto questo problema tramite il sistema chiave / valore esposto come parte di CAAnimation.

Quando avvii l'animazione, utilizza il metodo setValue su CATransition / CAAnimation per impostare gli identificatori e i valori da utilizzare quando viene attivato 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];
}

Nella tua animazioneDidStop delegato:

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

L'altro aspetto di ciò è che consente di mantenere lo stato nel sistema di associazione dei valori chiave invece di doverlo archiviare nella classe delegato. Meno codice, meglio è.

Assicurati di controllare il riferimento Apple sulla codifica della coppia di valori chiave .

Esistono tecniche migliori per l'identificazione CAAnimation / CATransition nel delegato animationDidStop?

Grazie, Batgar


4
Batgar, quando ho cercato su Google "iphone animationDidStop identificare", il primo colpo è stato il tuo post, che suggeriva l'uso del valore-chiave per identificare l'animazione. Proprio quello di cui avevo bisogno, grazie. Rudi
rudifa

1
Essere consapevoli del fatto che CAAnimation's delegateè forte, quindi potrebbe essere necessario impostarlo nilper evitare di mantenere i cicli!
Iulian Onofrei

Risposte:


92

La tecnica di Batgar è troppo complicata. Perché non sfruttare il parametro forKey in addAnimation? Era destinato proprio a questo scopo. Basta eliminare la chiamata a setValue e spostare la stringa della chiave nella chiamata addAnimation. Per esempio:

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

Quindi, nella richiamata AnimationDidStop, puoi fare qualcosa come:

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

Vorrei menzionare che l'utilizzo di quanto sopra AUMENTA IL CONTO RETAIN! Stai attento. Ovvero, animationForKey: incrementa il conteggio di conservazione dell'oggetto CAAnimation.
mmilo

1
@mmilo Non è molto sorprendente, vero? Aggiungendo un'animazione a un livello, il livello possiede l'animazione, quindi il conteggio di ritenzione dell'animazione viene ovviamente incrementato.
GorillaPatch

16
Non funziona: quando viene chiamato il selettore di arresto, l'animazione non esiste più. Ottieni un riferimento nullo.
Adam

4
Questo è un uso improprio del parametro forKey: e non ce n'è bisogno. Ciò che Batgar stava facendo è esattamente giusto: la codifica del valore-chiave ti consente di allegare dati arbitrari alla tua animazione, in modo da poterla identificare facilmente.
matt

7
Adam, vedi la risposta di jimt di seguito: devi impostare in anim.removedOnCompletion = NO;modo che esista ancora quando -animationDidStop:finished:viene chiamato.
Yang Meyer

46

Ho appena escogitato un modo ancora migliore per creare il codice di completamento per CAAnimations:

Ho creato un typedef per un blocco:

typedef void (^animationCompletionBlock)(void);

E una chiave che utilizzo per aggiungere un blocco a un'animazione:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Quindi, se voglio eseguire il codice di completamento dell'animazione al termine di una CAAnimation, mi imposto come delegato dell'animazione e aggiungo un blocco di codice all'animazione utilizzando setValue: forKey:

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

Quindi, implemento un metodo animationDidStop: finito:, che controlla un blocco alla chiave specificata e lo esegue se trovato:

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

La bellezza di questo approccio è che puoi scrivere il codice di pulizia nello stesso punto in cui crei l'oggetto di animazione. Meglio ancora, poiché il codice è un blocco, ha accesso alle variabili locali nello scope di inclusione in cui è definito. Non devi perdere tempo con l'impostazione dei dizionari userInfo o altre sciocchezze simili, e non devi scrivere un'animazione in continua crescitaDidStop: finito: metodo che diventa sempre più complesso man mano che aggiungi diversi tipi di animazioni.

A dire il vero, CAAnimation dovrebbe avere una proprietà di blocco di completamento incorporata e il supporto di sistema per chiamarlo automaticamente se specificato. Tuttavia, il codice sopra ti offre la stessa funzionalità con solo poche righe di codice extra.


7
Qualcuno ha anche messo insieme una categoria su CAAnimation per questo: github.com/xissburg/CAAnimationBlocks
Jay Peyer

Questo non sembra essere giusto. Molto spesso, ottengo un EXEC_Err subito dopo theBlock();essere stato richiamato e credo che sia dovuto al fatto che l'ambito del blocco è stato distrutto.
mahboudz

Uso il blocco da un po 'e funziona MOLTO meglio del terribile approccio "ufficiale" di Apple.
Adam

3
Sono abbastanza sicuro che avresti bisogno di [copiare in blocco] quel blocco prima di impostarlo come valore per una proprietà.
Fiona Hopkins

1
No, non è necessario copiare il blocco.
Duncan C

33

Il secondo approccio funzionerà solo se imposti esplicitamente che l'animazione non venga rimossa al termine prima di eseguirla:

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

Se non lo fai, la tua animazione verrà rimossa prima al termine e il callback non la troverà nel dizionario.


10
Questo dovrebbe essere un commento, non una risposta.
Fino al

2
Mi chiedo se sia necessario rimuoverlo esplicitamente in seguito con removeAnimationForKey?
bompf

Dipende davvero da cosa vuoi fare. Potresti rimuoverlo se necessario o lasciarlo perché vuoi fare qualcos'altro in tandem.
applejack42

31

Tutte le altre risposte sono troppo complicate! Perché non aggiungi semplicemente la tua chiave per identificare l'animazione?

Questa soluzione è molto semplice, tutto ciò di cui hai bisogno è aggiungere la tua chiave all'animazione (animationID in questo esempio)

Inserisci questa riga per identificare l' animazione1 :

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

e questo per identificare l' animazione2 :

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

Provalo in questo modo:

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

Non richiede variabili di istanza :


Ricevo un valore int (int (0)) in animationDidStop as[animation valueForKey:@"animationID"]
abhimuralidharan

14

Per rendere esplicito ciò che è implicito dall'alto (e ciò che mi ha portato qui dopo poche ore sprecate): non aspettarti di vedere l'oggetto di animazione originale che ti è stato assegnato passato da te

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

quando l'animazione finisce, perché [CALayer addAnimation:forKey:]fa una copia della tua animazione.

Ciò su cui puoi fare affidamento è che i valori con chiave che hai fornito all'oggetto di animazione sono ancora lì con un valore equivalente (ma non necessariamente l'equivalenza del puntatore) nell'oggetto di animazione di replica passato con il animationDidStop:finished:messaggio. Come accennato in precedenza, usa KVC e ottieni ampio spazio per memorizzare e recuperare lo stato.


1
+1 Questa è la soluzione migliore! Puoi impostare il "nome" dell'animazione con [animation setValue:@"myanim" forKey:@"name"]e puoi anche impostare il livello da animare usando [animation setValue:layer forKey:@"layer"]. Questi valori possono quindi essere recuperati all'interno dei metodi delegati.
trojanfoe

valueForKey:ritorna nilper me, qualche idea del perché?
Iulian Onofrei

@IulianOnofrei controlla che la tua animazione non sia stata spostata da un'altra animazione per la stessa proprietà - può verificarsi come effetto collaterale inaspettato.
t0rst

@ t0rst, Siamo spiacenti, avendo più animazioni e usando copia incolla, stavo impostando valori diversi sulla stessa variabile di animazione.
Iulian Onofrei

2

Riesco a vedere principalmente risposte objc, ne farò una per swift 2.3 in base alla migliore risposta sopra.

Per cominciare sarà bene memorizzare tutte quelle chiavi su una struttura privata in modo che sia sicuro per i tipi e cambiarlo in futuro non ti porterà fastidiosi bug solo perché ti sei dimenticato di cambiarlo ovunque nel codice:

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

Come puoi vedere ho cambiato i nomi delle variabili / animazioni in modo che sia più chiaro. Ora impostando questi tasti quando viene creata l'animazione.

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

(...)

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

Quindi gestire finalmente il delegato per quando l'animazione si interrompe

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

IMHO che utilizza il valore-chiave di Apple è il modo elegante per farlo: è specificamente pensato per consentire l'aggiunta di dati specifici dell'applicazione agli oggetti.

Un'altra possibilità molto meno elegante è quella di memorizzare i riferimenti agli oggetti di animazione e fare un confronto con i puntatori per identificarli.


Questo non funzionerà mai: non puoi eseguire l'equivalenza del puntatore, perché Apple cambia il puntatore.
Adam,

0

Per poter controllare se 2 oggetti CABasicAnimation sono la stessa animazione, utilizzo la funzione keyPath per fare esattamente lo stesso.

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

  • Non è necessario impostare KeyPath per CABasicAnimation poiché non si animerà più

la domanda si riferisce ai callback delegati e keyPath non è un metodo su CAAnimation
Max MacLeod

0

Mi piace usare setValue:forKey: per mantenere un riferimento alla vista che sto animando, è più sicuro che cercare di identificare in modo univoco l'animazione in base all'ID perché lo stesso tipo di animazione può essere aggiunto a diversi livelli.

Questi due sono equivalenti:

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

con questo:

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 nel metodo delegato:

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

È possibile utilizzare i valori chiave per correlare un'animazione aggiunta all'animazione restituita nel metodo delegato animationDidStop.

Dichiara che un dizionario contenga tutte le animazioni attive e i relativi completamenti:

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

Quando aggiungi la tua animazione, imposta una chiave per essa:

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

In animationDidStop, avviene la magia:

    let animObject = anim as NSObject
    if let keyValue = animObject.value(forKey: "CompletionId") as? Int {
        if let completion = animating.removeValue(forKey: keyValue) {
            completion()
        }
    }
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.