CABasicAnimation resetuje się do wartości początkowej po zakończeniu animacji

143

Obracam CALayer i próbuję zatrzymać go w ostatecznej pozycji po zakończeniu animacji.

Ale po zakończeniu animacji wraca do swojej początkowej pozycji.

(Dokumenty xcode wyraźnie mówią, że animacja nie zaktualizuje wartości właściwości).

wszelkie sugestie, jak to osiągnąć.

Nilesh Ukey
źródło
1
To jedno z tych dziwnych pytań SO, na które prawie wszystkie odpowiedzi są całkowicie błędne - po prostu całkowicie NIEPRAWIDŁOWE . Wystarczy spojrzeć, .presentation()aby uzyskać „ostateczną, widzianą” wartość. Wyszukaj poniżej poprawne odpowiedzi, które wyjaśniają, że zostało to zrobione z warstwą prezentacji.
Fattie

Odpowiedzi:

285

Oto odpowiedź, to połączenie mojej odpowiedzi i odpowiedzi Krishnana.

cabasicanimation.fillMode = kCAFillModeForwards;
cabasicanimation.removedOnCompletion = NO;

Wartość domyślna to kCAFillModeRemoved. (Jakie jest zachowanie resetowania, które widzisz).

Nilesh Ukey
źródło
6
To prawdopodobnie NIE jest poprawna odpowiedź - zobacz komentarz do odpowiedzi Krishnana. Zwykle potrzebujesz tego ORAZ Krishnana; tylko jeden lub drugi nie zadziała.
Adam
19
To nie jest poprawna odpowiedź, zobacz odpowiedź @Leslie Godwin poniżej.
Mark Ingram,
6
Rozwiązanie @Leslie jest lepsze, ponieważ przechowuje ostateczną wartość w warstwie, a nie w animacji.
Matthieu Rouif
1
Zachowaj ostrożność, ustawiając removeOnCompletion na NO. Jeśli przypisujesz delegata animacji do „siebie”, Twoja instancja zostanie zachowana przez animację. Tak więc, po wywołaniu removeFromSuperview, jeśli zapomnisz samodzielnie usunąć / zniszczyć animację, na przykład dealloc nie zostanie wywołany. Będziesz mieć wyciek pamięci.
emrahgunduz
1
Ta ponad 200-punktowa odpowiedź jest całkowicie, całkowicie, całkowicie błędna.
Fattie
83

Problem z removeOnCompletion polega na tym, że element UI nie pozwala na interakcję z użytkownikiem.

I technika polega na ustawieniu wartości FROM w animacji i wartości TO na obiekcie. Animacja automatycznie wypełni wartość TO przed rozpoczęciem, a po usunięciu pozostawi obiekt we właściwym stanie.

// fade in
CABasicAnimation *alphaAnimation = [CABasicAnimation animationWithKeyPath: @"opacity"];
alphaAnimation.fillMode = kCAFillModeForwards;

alphaAnimation.fromValue = NUM_FLOAT(0);
self.view.layer.opacity = 1;

[self.view.layer addAnimation: alphaAnimation forKey: @"fade"];
Leslie Godwin
źródło
7
Nie działa, jeśli ustawisz opóźnienie rozpoczęcia. np. alphaAnimation.beginTime = 1; Poza tym, o fillModeile wiem, nie jest potrzebne ...?
Sam
w przeciwnym razie jest to dobra odpowiedź, zaakceptowana odpowiedź nie pozwoli ci zmienić wartości po zakończeniu animacji ...
Sam
2
Powinienem również zauważyć, że beginTimenie jest to względne, powinieneś użyć np .:CACurrentMediaTime()+1;
Sam
To „to”, a nie „to” - its-not-its.info. Przepraszam, jeśli to tylko literówka.
fatuhoku
6
poprawna odpowiedź, ale nie potrzebna fillMode, zobacz więcej w artykule Zapobieganie powracaniu warstw do oryginalnych wartości podczas używania jawnych
animacji
16

Ustaw następującą właściwość:

animationObject.removedOnCompletion = NO;
Krishnan
źródło
@Nilesh: Zaakceptuj swoją odpowiedź. To da innym użytkownikom SO pewność co do Twojej odpowiedzi.
Krishnan
7
Musiałem użyć obu .fillMode = kCAFillModeForwards i .removedOnCompletion = NO. Dzięki!
William Rust
12

po prostu umieść go w swoim kodzie

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;
msk_sureshkumar
źródło
12

Możesz po prostu ustawić klucz od CABasicAnimationdo positionpodczas dodawania go do warstwy. W ten sposób nadpisuje niejawną animację wykonaną na pozycji dla bieżącego przebiegu w pętli uruchamiania.

CGFloat yOffset = 30;
CGPoint endPosition = CGPointMake(someLayer.position.x,someLayer.position.y + yOffset);

someLayer.position = endPosition; // Implicit animation for position

CABasicAnimation * animation =[CABasicAnimation animationWithKeyPath:@"position.y"]; 

animation.fromValue = @(someLayer.position.y);
animation.toValue = @(someLayer.position.y + yOffset);

[someLayer addAnimation:animation forKey:@"position"]; // The explicit animation 'animation' override implicit animation

Możesz uzyskać więcej informacji na temat 2011 Apple WWDC Video Session 421 - Core Animation Essentials (środek wideo)

yageek
źródło
1
Wygląda na to, że należy to zrobić we właściwy sposób. Animacja jest naturalnie usuwany (domyślnie), a po zakończeniu animacji, powraca do wartości ustawionych przed rozpoczęciem animacji, szczególnie tej linii: someLayer.position = endPosition;. I dzięki za referencje do WWDC!
bauerMusic
10

CALayer ma warstwę modelu i warstwę prezentacji. Podczas animacji warstwa prezentacji jest aktualizowana niezależnie od modelu. Po zakończeniu animacji warstwa prezentacji jest aktualizowana o wartość z modelu. Jeśli chcesz uniknąć gwałtownego skoku po zakończeniu animacji, kluczem jest zsynchronizowanie obu warstw.

Jeśli znasz wartość końcową, możesz po prostu ustawić model bezpośrednio.

self.view.layer.opacity = 1;

Ale jeśli masz animację, w której nie znasz pozycji końcowej (np. Powolne zanikanie, które użytkownik może zatrzymać, a następnie cofnąć), możesz bezpośrednio zapytać warstwę prezentacji, aby znaleźć bieżącą wartość, a następnie zaktualizować model.

NSNumber *opacity = [self.layer.presentationLayer valueForKeyPath:@"opacity"];
[self.layer setValue:opacity forKeyPath:@"opacity"];

Pobieranie wartości z warstwy prezentacji jest również szczególnie przydatne przy skalowaniu lub obracaniu ścieżek klawiszy. (na przykład transform.scale, transform.rotation)

Jason Moore
źródło
8

Bez używania removedOnCompletion

Możesz wypróbować tę technikę:

self.animateOnX(item: shapeLayer)

func animateOnX(item:CAShapeLayer)
{
    let endPostion = CGPoint(x: 200, y: 0)
    let pathAnimation = CABasicAnimation(keyPath: "position")
    //
    pathAnimation.duration = 20
    pathAnimation.fromValue = CGPoint(x: 0, y: 0)//comment this line and notice the difference
    pathAnimation.toValue =  endPostion
    pathAnimation.fillMode = kCAFillModeBoth

    item.position = endPostion//prevent the CABasicAnimation from resetting item's position when the animation finishes

    item.add(pathAnimation, forKey: nil)
}
Ayman Ibrahim
źródło
3
Wydaje się, że jest to najlepiej działająca odpowiedź
rambossa
Zgoda. Zwróć uwagę na komentarz @ ayman, że należy zdefiniować właściwość .fromValue do wartości początkowej. W przeciwnym razie wartość początkowa w modelu zostanie nadpisana i będzie animacja.
Jeff Collier
Ten i odpowiedź @ jason-moore są lepsze niż zaakceptowana odpowiedź. W zaakceptowanej odpowiedzi animacja jest po prostu wstrzymana, ale nowa wartość nie jest w rzeczywistości ustawiona na właściwość. Może to spowodować niepożądane zachowanie.
laka
8

Mój problem polegał na tym, że próbowałem obrócić obiekt za pomocą gestu przesuwania, więc miałem wiele identycznych animacji w każdym ruchu. Miałem jedno fillMode = kCAFillModeForwardsi drugie, isRemovedOnCompletion = falseale to nie pomogło. W moim przypadku musiałem upewnić się, że klucz animacji jest inny za każdym razem, gdy dodaję nową animację :

let angle = // here is my computed angle
let rotate = CABasicAnimation(keyPath: "transform.rotation.z")
rotate.toValue = angle
rotate.duration = 0.1
rotate.isRemovedOnCompletion = false
rotate.fillMode = CAMediaTimingFillMode.forwards

head.layer.add(rotate, forKey: "rotate\(angle)")
sunshinejr
źródło
7

Po prostu ustawienie fillModei removedOnCompletionnie działało dla mnie. Rozwiązałem problem, ustawiając wszystkie poniższe właściwości na obiekt CABasicAnimation:

CABasicAnimation* ba = [CABasicAnimation animationWithKeyPath:@"transform"];
ba.duration = 0.38f;
ba.fillMode = kCAFillModeForwards;
ba.removedOnCompletion = NO;
ba.autoreverses = NO;
ba.repeatCount = 0;
ba.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.85f, 0.85f, 1.0f)];
[myView.layer addAnimation:ba forKey:nil];

Ten kod przekształca się myViewdo 85% swojego rozmiaru (trzeci wymiar niezmieniony).

Paula Vasconcelos Gueiros
źródło
7

Podstawowa animacja zachowuje dwie hierarchie warstw: warstwę modelu i warstwę prezentacji . Gdy animacja jest w toku, warstwa modelu jest faktycznie nienaruszona i zachowuje wartość początkową. Domyślnie animacja jest usuwana po zakończeniu. Następnie warstwa prezentacji wraca do wartości warstwy modelu.

Po prostu ustawienie removedOnCompletionna NOdrodze animacja nie zostaną usunięte i pamięci odpadów. Ponadto warstwa modelu i warstwa prezentacji nie będą już synchroniczne, co może prowadzić do potencjalnych błędów.

Dlatego lepszym rozwiązaniem byłoby zaktualizowanie właściwości bezpośrednio na warstwie modelu do wartości końcowej.

self.view.layer.opacity = 1;
CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];

Jeśli istnieje jakakolwiek niejawna animacja spowodowana przez pierwszą linię powyższego kodu, spróbuj ją wyłączyć:

[CATransaction begin];
[CATransaction setDisableActions:YES];
self.view.layer.opacity = 1;
[CATransaction commit];

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
animation.fromValue = 0;
animation.toValue = 1;
[self.view.layer addAnimation:animation forKey:nil];
Lizhen Hu
źródło
Dziękuję Ci! To naprawdę powinna być akceptowana odpowiedź, ponieważ poprawnie wyjaśnia tę funkcjonalność zamiast po prostu używać pomocy opaski, aby to działało
bplattenburg
6

To działa:

let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3

someLayer.opacity = 1 // important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")

Główna animacja pokazuje podczas animacji „warstwę prezentacji” na wierzchu zwykłej warstwy. Ustaw więc krycie (lub cokolwiek) na to, co chcesz widzieć, gdy animacja się zakończy, a warstwa prezentacji zniknie. Zrób to w wierszu przed dodaniem animacji, aby uniknąć migotania po jej zakończeniu.

Jeśli chcesz mieć opóźnienie, wykonaj następujące czynności:

let animation = CABasicAnimation(keyPath: "opacity")
animation.fromValue = 0
animation.toValue = 1
animation.duration = 0.3
animation.beginTime = someLayer.convertTime(CACurrentMediaTime(), fromLayer: nil) + 1
animation.fillMode = kCAFillModeBackwards // So the opacity is 0 while the animation waits to start.

someLayer.opacity = 1 // <- important, this is the state you want visible after the animation finishes.
someLayer.addAnimation(animation, forKey: "myAnimation")

Wreszcie, jeśli użyjesz opcji „removeOnCompletion = false”, spowoduje to wyciek CAAnimations, aż warstwa zostanie ostatecznie usunięta - unikaj.

Chris
źródło
Świetna odpowiedź, świetne wskazówki. Dziękujemy za komentarz removeOnCompletion.
iWheelBuy
4

Odpowiedź @Leslie Godwin nie jest zbyt dobra, „self.view.layer.opacity = 1;” jest wykonywana natychmiast (zajmuje to około jednej sekundy), proszę poprawić alphaAnimation.duration do 10.0, jeśli masz wątpliwości. Musisz usunąć tę linię.

Tak więc, kiedy ustawisz fillMode na kCAFillModeForwards i usunieszOnCompletion na NO, pozwolisz animacji pozostać w warstwie. Jeśli naprawisz delegata animacji i spróbujesz czegoś takiego:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
 [theLayer removeAllAnimations];
}

... warstwa jest przywracana natychmiast po wykonaniu tej linii. Tego chcieliśmy uniknąć.

Musisz naprawić właściwość warstwy przed usunięciem z niej animacji. Spróbuj tego:

- (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag
{
     if([anim isKindOfClass:[CABasicAnimation class] ]) // check, because of the cast
    {
        CALayer *theLayer = 0;
        if(anim==[_b1 animationForKey:@"opacity"])
            theLayer = _b1; // I have two layers
        else
        if(anim==[_b2 animationForKey:@"opacity"])
            theLayer = _b2;

        if(theLayer)
        {
            CGFloat toValue = [((CABasicAnimation*)anim).toValue floatValue];
            [theLayer setOpacity:toValue];

            [theLayer removeAllAnimations];
        }
    }
}
tontonCD
źródło
1

Wygląda na to, że flaga removeOnCompletion ustawiona na false i fillMode ustawiona na kCAFillModeForwards również nie działa dla mnie.

Po zastosowaniu nowej animacji na warstwie, animowany obiekt wraca do swojego stanu początkowego, a następnie animuje z tego stanu. Co jeszcze należy zrobić, to ustawić żądaną właściwość warstwy modelu zgodnie z właściwością warstwy prezentacji przed ustawieniem nowej animacji w następujący sposób:

someLayer.path = ((CAShapeLayer *)[someLayer presentationLayer]).path;
[someLayer addAnimation:someAnimation forKey:@"someAnimation"];
Bartosz Olszanowski
źródło
0

Najłatwiejszym rozwiązaniem jest użycie niejawnych animacji. To rozwiąże wszystkie te problemy za Ciebie:

self.layer?.backgroundColor = NSColor.red.cgColor;

Jeśli chcesz dostosować np. Czas trwania, możesz użyć NSAnimationContext:

    NSAnimationContext.beginGrouping();
    NSAnimationContext.current.duration = 0.5;
    self.layer?.backgroundColor = NSColor.red.cgColor;
    NSAnimationContext.endGrouping();

Uwaga: jest to testowane tylko w systemie macOS.

Początkowo nie widziałem żadnej animacji, kiedy to robiłem. Problem polega na tym, że warstwa warstwy zabezpieczonej widokiem nie jest niejawnie animowana. Aby rozwiązać ten problem, upewnij się, że samodzielnie dodałeś warstwę (przed ustawieniem widoku na oparty na warstwach).

Przykładem, jak to zrobić, byłoby:

override func awakeFromNib() {
    self.layer = CALayer();
    //self.wantsLayer = true;
}

Używanie self.wantsLayernie miało żadnego wpływu na moje testy, ale może mieć pewne skutki uboczne, o których nie wiem.

paxos
źródło
0

Oto próbka z placu zabaw:

import PlaygroundSupport
import UIKit

let resultRotation = CGFloat.pi / 2
let view = UIView(frame: CGRect(x: 0.0, y: 0.0, width: 200.0, height: 300.0))
view.backgroundColor = .red

//--------------------------------------------------------------------------------

let rotate = CABasicAnimation(keyPath: "transform.rotation.z") // 1
rotate.fromValue = CGFloat.pi / 3 // 2
rotate.toValue = resultRotation // 3
rotate.duration = 5.0 // 4
rotate.beginTime = CACurrentMediaTime() + 1.0 // 5
// rotate.isRemovedOnCompletion = false // 6
rotate.fillMode = .backwards // 7

view.layer.add(rotate, forKey: nil) // 8
view.layer.setAffineTransform(CGAffineTransform(rotationAngle: resultRotation)) // 9

//--------------------------------------------------------------------------------

PlaygroundPage.current.liveView = view
  1. Utwórz model animacji
  2. Ustaw pozycję początkową animacji (można ją pominąć, zależy to od bieżącego układu warstw)
  3. Ustaw końcową pozycję animacji
  4. Ustaw czas trwania animacji
  5. Animacja opóźnienia na sekundę
  6. Nie nastawiaj falsesię isRemovedOnCompletion- niech Core Animation czysty po zakończeniu animacji
  7. Oto pierwsza część sztuczki - mówisz do Core Animation, aby umieścić animację w pozycji początkowej (ustawiłeś w kroku 2), zanim animacja się rozpoczęła - wydłuż ją w czasie
  8. Skopiuj przygotowany obiekt animacji, dodaj go do warstwy i uruchom animację po opóźnieniu (ustawiasz w kroku 5)
  9. Druga część to ustawienie poprawnej pozycji końcowej warstwy - po usunięciu animacji warstwa zostanie wyświetlona we właściwym miejscu
adnako
źródło