CABasicAnimation si reimposta sul valore iniziale al termine dell'animazione


143

Sto ruotando un CALayer e sto cercando di fermarlo nella sua posizione finale dopo che l'animazione è stata completata.

Ma una volta completata l'animazione, viene ripristinata nella posizione iniziale.

(i documenti xcode dicono esplicitamente che l'animazione non aggiorna il valore della proprietà.)

eventuali suggerimenti su come raggiungere questo obiettivo.


1
Questa è una di quelle strane domande su SO in cui quasi tutte le risposte sono completamente sbagliate - totalmente SBAGLIATE . Osserva semplicemente .presentation()per ottenere il valore "finale, visto". Cerca le risposte corrette in basso che spiegano che è stato fatto con il livello di presentazione.
Fattie,

Risposte:


285

Ecco la risposta, è una combinazione della mia risposta e di quella di Krishnan.

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

Il valore predefinito è kCAFillModeRemoved. (Qual è il comportamento di ripristino che stai vedendo.)


6
Questa probabilmente NON è la risposta corretta - vedi il commento sulla risposta di Krishnan. Di solito hai bisogno di questo e di Krishnan; solo l'uno o l'altro non funzionerà.
Adam,

19
Questa non è la risposta corretta, vedi la risposta di @Leslie Godwin di seguito.
Mark Ingram il

6
La soluzione @Leslie è migliore perché memorizza il valore finale nel livello e non nell'animazione.
Matthieu Rouif,

1
Fare attenzione quando si imposta removeOnCompletion su NO. Se si sta assegnando il delegato all'animazione su "self", l'animazione verrà mantenuta dall'istanza. Quindi, dopo aver chiamato removeFromSuperview se si dimentica di rimuovere / distruggere l'animazione da soli, dealloc per esempio non verrà chiamato. Avrai una perdita di memoria.
emrahgunduz,

1
Questa risposta di oltre 200 punti è totalmente, completamente, assolutamente errata.
Fattie,

83

Il problema con rimossoOnCompletion è che l'elemento dell'interfaccia utente non consente l'interazione dell'utente.

I tecnica consiste nell'impostare il valore FROM nell'animazione e il valore TO sull'oggetto. L'animazione riempirà automaticamente il valore TO prima di iniziare e quando viene rimossa lascerà l'oggetto nel suo stato corretto.

// 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"];

7
Non funziona se si imposta un ritardo di avvio. ad esempio alphaAnimation.beginTime = 1; , fillModenon è necessario per quanto posso dire ...?
Sam,

in caso contrario, questa è una buona risposta, la risposta accettata non ti permetterà di modificare un valore dopo il completamento dell'animazione ...
Sam,

2
dovrei anche notare che beginTimenon è relativo, dovresti usare ad esempio:CACurrentMediaTime()+1;
Sam

È 'suo' non "è" - its-not-its.info. Scusa se era solo un errore di battitura.
fatuhoku,


16

Impostare la seguente proprietà:

animationObject.removedOnCompletion = NO;

@Nilesh: accetta la tua risposta. Ciò darà fiducia ad altri utenti SO sulla tua risposta.
Krishnan,

7
Ho dovuto usare sia .fillMode = kCAFillModeForwards che .removedOnCompletion = NO. Grazie!
William Rust,

12

inseriscilo nel tuo codice

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

12

Si può semplicemente impostare la chiave di CABasicAnimationa positionquando lo si aggiunge al livello. In questo modo, sovrascriverà l'animazione implicita eseguita sulla posizione per il passaggio corrente nel ciclo di esecuzione.

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

Puoi avere maggiori informazioni su Apple WWDC Video Session 421 2011 - Core Animation Essentials (metà del video)


1
Sembra il modo corretto di farlo. Animazione è naturalmente rimosso (default), e una volta che l'animazione è completa, si ritorna ai valori impostati prima dell'animazione ha iniziato, in particolare questa linea: someLayer.position = endPosition;. E grazie per il riferimento al WWDC!
bauerMusic

10

Un CALayer ha un livello modello e un livello presentazione. Durante un'animazione, il livello di presentazione si aggiorna indipendentemente dal modello. Al termine dell'animazione, il livello di presentazione viene aggiornato con il valore dal modello. Se vuoi evitare un salto sbilenco dopo che l'animazione termina, la chiave è mantenere sincronizzati i due livelli.

Se conosci il valore finale, puoi semplicemente impostare direttamente il modello.

self.view.layer.opacity = 1;

Ma se si dispone di un'animazione in cui non si conosce la posizione finale (ad esempio una dissolvenza lenta che l'utente può mettere in pausa e quindi invertire), è possibile eseguire una query direttamente sul livello di presentazione per trovare il valore corrente e quindi aggiornare il modello.

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

L'estrazione del valore dal livello di presentazione è inoltre particolarmente utile per il ridimensionamento o la rotazione dei punti chiave. (ad es transform.scale. transform.rotation)


8

Senza usare il removedOnCompletion

Puoi provare questa tecnica:

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

3
Questa sembra essere la risposta più
efficace

Concordato. Nota @ commento di ayman che è necessario definire la proprietà .fromValue sul valore iniziale. Altrimenti, avrai sovrascritto il valore iniziale nel modello e ci sarà l'animazione.
Jeff Collier,

Questa e la risposta di @ jason-moore sono migliori della risposta accettata. Nella risposta accettata l'animazione è appena in pausa ma il nuovo valore non è effettivamente impostato sulla proprietà. Ciò può comportare comportamenti indesiderati.
Laka,

8

Quindi il mio problema era che stavo provando a ruotare un oggetto sul gesto di panoramica e quindi avevo più animazioni identiche su ogni mossa. Ho avuto entrambi fillMode = kCAFillModeForwardse isRemovedOnCompletion = falsema non ha aiutato. Nel mio caso, ho dovuto assicurarmi che la chiave di animazione fosse diversa ogni volta che aggiungevo una nuova animazione :

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)")

7

Semplicemente ambientato fillModee removedOnCompletionnon ha funzionato per me. Ho risolto il problema impostando tutte le proprietà seguenti sull'oggetto 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];

Questo codice si trasforma myViewall'85% delle sue dimensioni (terza dimensione inalterata).


7

L'animazione principale mantiene due gerarchie di livelli: il livello del modello e il livello di presentazione . Quando l'animazione è in corso, il livello del modello è effettivamente intatto e mantiene il valore iniziale. Per impostazione predefinita, l'animazione viene rimossa una volta completata. Quindi il livello di presentazione torna al valore del livello del modello.

Basta impostare removedOnCompletionsuNO significa che l'animazione non verrà rimossa e sprecherà memoria. Inoltre, il livello del modello e il livello di presentazione non saranno più sincroni, il che potrebbe portare a potenziali bug.

Quindi sarebbe una soluzione migliore per aggiornare la proprietà direttamente sul livello del modello al valore finale.

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

Se c'è qualche animazione implicita causata dalla prima riga del codice sopra, prova a disattivare se disattivato:

[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];

Grazie! Questa dovrebbe davvero essere la risposta accettata, poiché spiega correttamente questa funzionalità invece di usare solo un cerotto per farlo funzionare
bplattenburg,

6

Questo funziona:

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")

L'animazione principale mostra un "livello di presentazione" sopra il livello normale durante l'animazione. Quindi imposta l'opacità (o qualsiasi altra cosa) su ciò che vuoi vedere quando l'animazione termina e il livello di presentazione scompare. Fallo sulla linea prima di aggiungere l'animazione per evitare uno sfarfallio al termine.

Se vuoi avere un ritardo, procedi come segue:

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")

Infine, se si utilizza 'removeOnCompletion = false', perderanno CAAnimations fino a quando il layer non verrà eliminato. Evitare.


Ottima risposta, ottimi consigli. Grazie per il commento rimossoOnCompletion.
iWheelBuy

4

La risposta di @Leslie Godwin non è davvero buona, "self.view.layer.opacity = 1;" viene eseguito immediatamente (richiede circa un secondo), si prega di fissare alphaAnimation.duration su 10.0, in caso di dubbi. Devi rimuovere questa linea.

Pertanto, quando ripari fillMode su kCAFillModeForwards e rimossoOnCompletion su NO, lasci che l'animazione rimanga nel layer. Se correggi il delegato dell'animazione e prova qualcosa di simile:

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

... il layer viene ripristinato immediatamente nel momento in cui si esegue questa riga. È quello che volevamo evitare.

È necessario correggere la proprietà del livello prima di rimuovere l'animazione da essa. Prova questo:

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

1

Sembra che il flag rimossoOnCompletion impostato su false e fillMode impostato su kCAFillModeForwards non funziona neanche per me.

Dopo aver applicato una nuova animazione su un livello, un oggetto di animazione si ripristina al suo stato iniziale e quindi si anima da quello stato. Inoltre, è necessario impostare la proprietà desiderata del livello modello in base alla proprietà del livello presentazione prima di impostare una nuova animazione in questo modo:

someLayer.path = ((CAShapeLayer *)[someLayer presentationLayer]).path;
[someLayer addAnimation:someAnimation forKey:@"someAnimation"];

0

La soluzione più semplice è utilizzare animazioni implicite. Questo gestirà tutti questi problemi per te:

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

Se si desidera personalizzare, ad esempio, la durata, è possibile utilizzare NSAnimationContext:

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

Nota: questo è testato solo su macOS.

Inizialmente non ho visto alcuna animazione mentre lo facevo. Il problema è che il livello di un livello supportato dalla vista non implica animazioni implicite. Per risolvere questo problema, assicurati di aggiungere tu stesso un layer (prima di impostare la vista su layer supportato).

Un esempio di come fare sarebbe:

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

L'uso self.wantsLayernon ha fatto alcuna differenza nei miei test, ma potrebbe avere alcuni effetti collaterali che non conosco.


0

Ecco un esempio dal parco giochi:

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. Crea un modello di animazione
  2. Imposta la posizione iniziale dell'animazione (potrebbe essere saltata, dipende dal layout del livello corrente)
  3. Imposta la posizione finale dell'animazione
  4. Imposta la durata dell'animazione
  5. Ritarda l'animazione per un secondo
  6. Non impostare falsesu isRemovedOnCompletion: consente di pulire Core Animation al termine dell'animazione
  7. Ecco la prima parte del trucco - dici a Core Animation di posizionare l'animazione nella posizione iniziale (impostata nel passaggio 2) prima ancora che l'animazione sia stata avviata - estendila indietro nel tempo
  8. Copia l'oggetto animazione preparato, aggiungilo al livello e avvia l'animazione dopo il ritardo (impostato al passaggio 5)
  9. La seconda parte consiste nell'impostare la posizione finale corretta del livello : dopo che l'animazione è stata eliminata, il livello verrà mostrato nella posizione corretta
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.