Jak zidentyfikować CAAnimation w delegacie AnimationDidStop?

102

Miałem problem polegający na tym, że miałem serię nakładających się sekwencji CATransition / CAAnimation, z których wszystkie potrzebowałem do wykonywania niestandardowych operacji po zatrzymaniu animacji, ale chciałem tylko jednego delegata obsługi dla animationDidStop.

Jednak miałem problem, wydaje się, że nie ma sposobu na unikalną identyfikację każdego przejścia CATransition / CAAnimation w delegacie AnimationDidStop.

Rozwiązałem ten problem za pomocą systemu klucza / wartości ujawnionego w ramach CAAnimation.

Po uruchomieniu animacji użyj metody setValue w CATransition / CAAnimation, aby ustawić swoje identyfikatory i wartości, które mają być używane podczas uruchamiania animacjiDidStop:

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

W twojej animacji DidStop delegata:

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

Innym aspektem tego jest to, że pozwala to na utrzymanie stanu w systemie parowania klucz-wartość zamiast konieczności przechowywania go w klasie delegatów. Im mniej kodu, tym lepiej.

Koniecznie zapoznaj się z odniesieniem firmy Apple na temat kodowania pary klucz-wartość .

Czy istnieją lepsze techniki identyfikacji przejścia CAAnimation / CAT w delegacie AnimationDidStop?

Dzięki, - Batgar

Batgar
źródło
4
Batgar, Kiedy wyszukałem w Google hasło „iphone animationDidStop zidentyfikuj”, pierwszym trafieniem był Twój post, sugerujący użycie klucza-wartości do identyfikacji animacji. Właśnie tego potrzebowałem, dziękuję. Rudi
rudifa Sierpnia
1
Pamiętaj, że CAAnimationto delegatejest mocne, więc może być konieczne ustawienie go, nilaby uniknąć zachowania cykli!
Iulian Onofrei,

Odpowiedzi:

92

Technika Batgara jest zbyt skomplikowana. Dlaczego nie skorzystać z parametru forKey w addAnimation? Miał właśnie taki cel. Po prostu wyłącz wywołanie setValue i przenieś ciąg klucza do wywołania addAnimation. Na przykład:

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

Następnie w wywołaniu zwrotnym animacjiDidStop możesz zrobić coś takiego:

if (theAnimation == [[hearingAidHalo layer] animationForKey:@"Throb"]) ...
vocaro
źródło
Chciałbym wspomnieć, że korzystając z powyższego PRZYROSTA LICZBY ZATRZYMANIA! Być ostrzeżonym. To znaczy, AnimationForKey: zwiększa liczbę zachowań obiektu CAAnimation.
mmilo
1
@mmilo To nie jest zbyt zaskakujące, prawda? Dodając animację do warstwy, warstwa jest właścicielem animacji, więc liczba zachowań animacji jest oczywiście zwiększana.
GorillaPatch
16
Nie działa - do czasu wywołania selektora zatrzymania animacja już nie istnieje. Otrzymasz zerowe odniesienie.
Adam,
4
To niewłaściwe użycie parametru forKey: i nie ma takiej potrzeby. To, co robił Batgar, jest dokładnie słuszne - kodowanie klucz-wartość pozwala dołączyć dowolne dane do animacji, dzięki czemu można je łatwo zidentyfikować.
mat.
7
Adamie, zobacz odpowiedź jimta poniżej - musisz ustawić anim.removedOnCompletion = NO;tak, aby nadal istniała, gdy -animationDidStop:finished:zostanie wywołana.
Yang Meyer
46

Właśnie wymyśliłem jeszcze lepszy sposób tworzenia kodu uzupełniającego dla CAAnimations:

Stworzyłem typedef dla bloku:

typedef void (^animationCompletionBlock)(void);

I klucz, którego używam, aby dodać blok do animacji:

#define kAnimationCompletionBlock @"animationCompletionBlock"

Następnie, jeśli chcę uruchomić kod zakończenia animacji po zakończeniu CAAnimation, ustawiam siebie jako delegata animacji i dodaję blok kodu do animacji za pomocą setValue: forKey:

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

Następnie implementuję metodę animationDidStop: finish:, która sprawdza blok pod określonym kluczem i wykonuje go, jeśli zostanie znaleziony:

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

Piękno tego podejścia polega na tym, że możesz napisać kod czyszczenia w tym samym miejscu, w którym tworzysz obiekt animacji. Co więcej, ponieważ kod jest blokiem, ma dostęp do zmiennych lokalnych w otaczającym zakresie, w którym jest zdefiniowany. Nie musisz majstrować przy ustawianiu słowników userInfo lub innych tego typu bzdurach i nie musisz pisać stale rosnącej animacji DidStop: finish: metoda, która staje się coraz bardziej złożona, gdy dodajesz różne rodzaje animacji.

Prawdę mówiąc, CAAnimation powinno mieć wbudowaną właściwość bloku uzupełniania i obsługę systemu umożliwiającą automatyczne wywoływanie jej, jeśli taka jest określona. Jednak powyższy kod zapewnia tę samą funkcjonalność z zaledwie kilkoma wierszami dodatkowego kodu.

Duncan C
źródło
7
Ktoś również utworzył w tym celu kategorię w CAAnimation: github.com/xissburg/CAAnimationBlocks
Jay Peyer
To nie wydaje się być słuszne. Dość często dostaję EXEC_Err zaraz po theBlock();wywołaniu i wydaje mi się, że jest to spowodowane tym, że zakres bloku został zniszczony.
mahboudz
Używam tego bloku od jakiegoś czasu i działa ZNACZNIE lepiej niż straszne „oficjalne” podejście Apple'a.
Adam
3
Jestem prawie pewien, że trzeba by [blokować kopiowanie] tego bloku przed ustawieniem go jako wartości właściwości.
Fiona Hopkins
1
Nie, nie musisz kopiować bloku.
Duncan C,
33

Drugie podejście zadziała tylko wtedy, gdy wyraźnie ustawisz animację tak, aby nie była usuwana po zakończeniu przed jej uruchomieniem:

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

Jeśli tego nie zrobisz, Twoja animacja zostanie usunięta przed zakończeniem, a wywołanie zwrotne nie znajdzie jej w słowniku.

jimt
źródło
10
To powinien być komentarz, a nie odpowiedź.
Do
2
Zastanawiam się, czy konieczne jest późniejsze jawne usunięcie go za pomocą removeAnimationForKey?
bompf
To naprawdę zależy od tego, co chcesz robić. Możesz go usunąć, jeśli to konieczne, lub zostawić, ponieważ chcesz zrobić coś innego w tandemie.
applejack42,
31

Wszystkie inne odpowiedzi są zbyt skomplikowane! Dlaczego po prostu nie dodasz własnego klucza, aby zidentyfikować animację?

To rozwiązanie jest bardzo proste, wystarczy dodać własny klucz do animacji (w tym przykładzie identyfikator animacji )

Wstaw tę linię, aby zidentyfikować animację1 :

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

a to w celu zidentyfikowania animacji2 :

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

Przetestuj to w ten sposób:

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

Nie wymaga żadnych zmiennych instancji :

Tibidabo
źródło
Otrzymuję wartość int (int (0)) w animacjiDidStop as[animation valueForKey:@"animationID"]
abhimuralidharan
14

Aby wyjaśnić, co wynika z góry (i co mnie tu sprowadziło po kilku zmarnowanych godzinach): nie spodziewaj się, że oryginalny obiekt animacji, który przydzieliłeś, zostanie ci przekazany z powrotem przez

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

kiedy animacja się kończy, ponieważ [CALayer addAnimation:forKey:]tworzy kopię animacji.

Możesz polegać na tym, że wartości klucza, które nadałeś swojemu obiektowi animacji, nadal istnieją z równoważną wartością (ale niekoniecznie równoważnością wskaźnika) w obiekcie animacji repliki przekazanym z animationDidStop:finished:komunikatem. Jak wspomniano powyżej, użyj KVC, a uzyskasz szeroki zakres do przechowywania i pobierania stanu.

t0rst
źródło
1
+1 To najlepsze rozwiązanie! Możesz ustawić „nazwę” animacji za pomocą, [animation setValue:@"myanim" forKey:@"name"]a nawet możesz ustawić animowaną warstwę za pomocą [animation setValue:layer forKey:@"layer"]. Te wartości można następnie pobrać w ramach metod delegata.
trojanfoe
valueForKey:wraca nildo mnie, jakiś pomysł dlaczego?
Iulian Onofrei
@IulianOnofrei sprawdź, czy Twoja animacja nie została zastąpiona przez inną animację o tej samej właściwości - może się to zdarzyć jako nieoczekiwany efekt uboczny.
t0rst
@ t0rst, Przepraszam, mając wiele animacji i używając kopiuj wklej, ustawiałem różne wartości tej samej zmiennej animacji.
Iulian Onofrei
2

Widzę głównie odpowiedzi objc. Zrobię jedną dla Swift 2.3 w oparciu o najlepszą odpowiedź powyżej.

Na początek dobrze będzie przechowywać wszystkie te klucze w prywatnej strukturze, aby można było je bezpiecznie wpisywać, a zmiana w przyszłości nie przyniesie irytujących błędów tylko dlatego, że zapomniałeś zmienić go wszędzie w kodzie:

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

Jak widać, zmieniłem nazwy zmiennych / animacji, aby było bardziej przejrzyste. Teraz ustawiam te klawisze podczas tworzenia animacji.

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

(...)

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

Następnie w końcu obsługa delegata, kiedy animacja się zatrzyma

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
źródło
0

IMHO przy użyciu klucza-wartości firmy Apple jest eleganckim sposobem na zrobienie tego: ma to w szczególności umożliwić dodawanie danych specyficznych dla aplikacji do obiektów.

Inną, znacznie mniej elegancką możliwością jest przechowywanie odniesień do obiektów animacji i porównywanie wskaźników w celu ich identyfikacji.

Teemu Kurppa
źródło
To nigdy nie zadziała - nie możesz zrobić równoważności wskaźnika, ponieważ Apple zmienia wskaźnik.
Adam,
0

Aby sprawdzić, czy 2 obiekty CABasicAnimation są tą samą animacją, używam funkcji keyPath, aby zrobić dokładnie to.

if ([ścieżka_klucza animacjiA] == [ścieżka_klucza animacjiB])

  • Nie ma potrzeby ustawiania KeyPath dla CABasicAnimation, ponieważ nie będzie już animowany
Sirisilp Kongsilp
źródło
Pytanie dotyczy delegowania zwrotnych i keypath nie jest metodą na CAAnimation
Max MacLeod
0

Lubię używać setValue:forKey: aby zachować odniesienie do animowanego widoku, jest to bezpieczniejsze niż próba jednoznacznej identyfikacji animacji na podstawie identyfikatora, ponieważ ten sam rodzaj animacji można dodać do różnych warstw.

Te dwa są równoważne:

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

z tym:

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;

aw metodzie delegata:

- (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 Mărincaș
źródło
0

Xcode 9 Swift 4.0.0

Możesz użyć wartości kluczowych, aby powiązać animację dodaną do animacji zwróconej w metodzie delegata animationDidStop.

Zadeklaruj słownik, aby zawierał wszystkie aktywne animacje i powiązane uzupełnienia:

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

Dodając animację, ustaw dla niej klucz:

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

W animacjiDidStop dzieje się magia:

    let animObject = anim as NSObject
    if let keyValue = animObject.value(forKey: "CompletionId") as? Int {
        if let completion = animating.removeValue(forKey: keyValue) {
            completion()
        }
    }
Eng Yew
źródło