CABasicAnimation wird nach Abschluss der Animation auf den Anfangswert zurückgesetzt


143

Ich drehe einen CALayer und versuche, ihn nach Abschluss der Animation an seiner endgültigen Position anzuhalten.

Nach Abschluss der Animation wird sie jedoch auf ihre ursprüngliche Position zurückgesetzt.

(In xcode-Dokumenten wird ausdrücklich angegeben, dass die Animation den Wert der Eigenschaft nicht aktualisiert.)

Vorschläge, wie dies erreicht werden kann.


1
Dies ist eine dieser seltsamen SO-Fragen, bei denen fast alle Antworten völlig falsch sind - einfach völlig FALSCH . Sie sehen sich ganz einfach .presentation()den "endgültigen, gesehenen" Wert an. Suchen Sie nach den richtigen Antworten, die erklären, wie es mit der Präsentationsebene gemacht wird.
Fattie

Antworten:


285

Hier ist die Antwort, es ist eine Kombination aus meiner Antwort und Krishnans.

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

Der Standardwert ist kCAFillModeRemoved. (Welches ist das Reset-Verhalten, das Sie sehen.)


6
Dies ist wahrscheinlich NICHT die richtige Antwort - siehe den Kommentar zu Krishnans Antwort. Normalerweise brauchst du das UND Krishnans; nur der eine oder andere wird nicht funktionieren.
Adam

19
Dies ist nicht die richtige Antwort, siehe die Antwort von @Leslie Godwin unten.
Mark Ingram

6
Die @ Leslie-Lösung ist besser, da sie den Endwert in der Ebene und nicht in der Animation speichert.
Matthieu Rouif

1
Seien Sie vorsichtig, wenn Sie removeOnCompletion auf NO setzen. Wenn Sie den Animationsdelegierten "self" zuweisen, wird Ihre Instanz von der Animation beibehalten. Wenn Sie also nach dem Aufruf von removeFromSuperview vergessen, die Animation selbst zu entfernen / zu zerstören, wird beispielsweise dealloc nicht aufgerufen. Sie haben ein Speicherleck.
Emrahgunduz

1
Diese Antwort mit mehr als 200 Punkten ist völlig falsch.
Fattie

83

Das Problem mit removeOnCompletion ist, dass das UI-Element keine Benutzerinteraktion zulässt.

Die Technik besteht darin, den FROM-Wert in der Animation und den TO-Wert für das Objekt festzulegen. Die Animation füllt den TO-Wert automatisch, bevor sie gestartet wird. Wenn sie entfernt wird, bleibt das Objekt im richtigen Zustand.

// 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
Funktioniert nicht, wenn Sie eine Startverzögerung einstellen. zB alphaAnimation.beginTime = 1; Auch das fillModewird nicht benötigt, soweit ich das beurteilen kann ...?
Sam

Andernfalls ist dies eine gute Antwort. Mit der akzeptierten Antwort können Sie nach Abschluss der Animation keinen Wert mehr ändern ...
Sam

2
Ich sollte auch beachten, dass beginTimenicht relativ ist, sollten Sie zB verwenden:CACurrentMediaTime()+1;
Sam

Es ist 'es' nicht "es ist" - its-not-its.info. Entschuldigung, wenn es nur ein Tippfehler war.
Fatuhoku

6
Richtige Antwort, aber nicht erforderlich fillMode. Weitere Informationen finden
Sie unter

16

Legen Sie die folgende Eigenschaft fest:

animationObject.removedOnCompletion = NO;

@Nilesh: Akzeptiere deine Antwort. Dies gibt anderen SO-Benutzern Vertrauen in Ihre Antwort.
Krishnan

7
Ich musste sowohl .fillMode = kCAFillModeForwards als auch .removedOnCompletion = NO verwenden. Vielen Dank!
William Rust

12

Fügen Sie es einfach in Ihren Code ein

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

12

Sie können den Schlüssel einfach auf setzen CABasicAnimation, positionwenn Sie ihn der Ebene hinzufügen. Auf diese Weise wird die implizite Animation überschrieben, die an der Position für den aktuellen Durchlauf in der Ausführungsschleife ausgeführt wird.

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

Weitere Informationen zu Apple WWDC Video Session 421 2011 - Core Animation Essentials (Mitte des Videos) finden Sie hier.


1
Dies scheint der richtige Weg zu sein. Die Animation wird natürlich entfernt (Standard), und sobald die Animation abgeschlossen ist, kehrt sie zu den Werten zurück, die vor dem Start der Animation festgelegt wurden, insbesondere zu dieser Zeile : someLayer.position = endPosition;. Und danke für den Hinweis an die WWDC!
bauerMusic

10

Eine CALayer hat eine Modellebene und eine Präsentationsebene. Während einer Animation wird die Präsentationsebene unabhängig vom Modell aktualisiert. Wenn die Animation abgeschlossen ist, wird die Präsentationsebene mit dem Wert aus dem Modell aktualisiert. Wenn Sie nach dem Ende der Animation einen Störsprung vermeiden möchten, müssen Sie die beiden Ebenen synchron halten.

Wenn Sie den Endwert kennen, können Sie das Modell einfach direkt einstellen.

self.view.layer.opacity = 1;

Wenn Sie jedoch eine Animation haben, bei der Sie die Endposition nicht kennen (z. B. eine langsame Überblendung, die der Benutzer anhalten und dann umkehren kann), können Sie die Präsentationsebene direkt abfragen, um den aktuellen Wert zu ermitteln, und dann das Modell aktualisieren.

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

Das Abrufen des Werts aus der Präsentationsebene ist auch besonders nützlich für Skalierungs- oder Rotationsschlüsselpfade. (z transform.scale, transform.rotation)


8

Ohne die removedOnCompletion

Sie können diese Technik ausprobieren:

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
Dies scheint die am besten funktionierende Antwort zu sein
Rambossa

Einverstanden. Beachten Sie den Kommentar von @ ayman, dass Sie die Eigenschaft .fromValue für den Startwert definieren müssen . Andernfalls haben Sie den Startwert im Modell überschrieben, und es wird eine Animation angezeigt.
Jeff Collier

Diese und die Antwort von @ jason-moore sind besser als die akzeptierte Antwort. In der akzeptierten Antwort wird die Animation nur angehalten, aber der neue Wert wird nicht tatsächlich auf die Eigenschaft festgelegt. Dies kann zu unerwünschtem Verhalten führen.
Laka

8

Mein Problem war also, dass ich versuchte, ein Objekt mit einer Schwenkgeste zu drehen, und dass ich bei jeder Bewegung mehrere identische Animationen hatte. Ich hatte beides fillMode = kCAFillModeForwardsund isRemovedOnCompletion = falseaber es hat nicht geholfen. In meinem Fall musste ich sicherstellen, dass der Animationsschlüssel jedes Mal anders ist, wenn ich eine neue Animation hinzufüge :

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

Einfach einstellen fillModeund removedOnCompletionbei mir nicht funktioniert. Ich habe das Problem gelöst, indem ich alle folgenden Eigenschaften auf das CABasicAnimation-Objekt festgelegt habe:

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

Dieser Code wird myViewin 85% seiner Größe umgewandelt (3. Dimension unverändert).


7

Die Kernanimation verwaltet zwei Ebenenhierarchien: die Modellebene und die Präsentationsebene . Während der Animation ist die Modellebene tatsächlich intakt und behält ihren Anfangswert bei. Standardmäßig wird die Animation nach Abschluss entfernt. Dann fällt die Präsentationsschicht auf den Wert der Modellebene zurück.

Einfach einstellen removedOnCompletionaufNO wird die Animation nicht entfernt und Speicherplatz verschwendet. Außerdem sind die Modellebene und die Präsentationsschicht nicht mehr synchron, was zu potenziellen Fehlern führen kann.

Daher wäre es eine bessere Lösung, die Eigenschaft direkt auf der Modellebene auf den Endwert zu aktualisieren.

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

Wenn durch die erste Zeile des obigen Codes implizite Animationen verursacht werden, versuchen Sie, diese zu deaktivieren, wenn:

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

Danke dir! Dies sollte wirklich die akzeptierte Antwort sein, da es diese Funktionalität richtig erklärt, anstatt nur ein
Pflaster

6

Das funktioniert:

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

Die Kernanimation zeigt während der Animation eine Präsentationsebene auf Ihrer normalen Ebene. Stellen Sie die Deckkraft (oder was auch immer) auf das ein, was Sie sehen möchten, wenn die Animation beendet ist und die Präsentationsebene verschwindet. Führen Sie dies in der Zeile aus, bevor Sie die Animation hinzufügen, um ein Flackern nach Abschluss zu vermeiden.

Wenn Sie eine Verzögerung wünschen, gehen Sie wie folgt vor:

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

Wenn Sie schließlich 'removeOnCompletion = false' verwenden, werden CAAnimations durchgesickert, bis die Ebene schließlich entsorgt wird - vermeiden Sie dies.


Tolle Antwort, tolle Tipps. Vielen Dank für den Kommentar zu removeOnCompletion.
iWheelBuy

4

@Leslie Godwins Antwort ist nicht wirklich gut, "self.view.layer.opacity = 1;" Wenn dies sofort erledigt ist (es dauert ungefähr eine Sekunde), korrigieren Sie alphaAnimation.duration auf 10.0, wenn Sie Zweifel haben. Sie müssen diese Zeile entfernen.

Wenn Sie also fillMode auf kCAFillModeForwards korrigieren undOnCompletion auf NO entfernen, bleibt die Animation in der Ebene. Wenn Sie den Animationsdelegierten reparieren und Folgendes versuchen:

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

... die Ebene wird sofort wiederhergestellt, sobald Sie diese Zeile ausführen. Das wollten wir vermeiden.

Sie müssen die Ebeneneigenschaft korrigieren, bevor Sie die Animation daraus entfernen. Versuche dies:

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

Es scheint, dass das Flag "RemoveOnCompletion" auf "false" und "fillMode" auf "kCAFillModeForwards" gesetzt ist.

Nachdem ich eine neue Animation auf eine Ebene angewendet habe, wird ein Animationsobjekt auf seinen Ausgangszustand zurückgesetzt und anschließend aus diesem Zustand animiert. Was zusätzlich getan werden muss, ist, die gewünschte Eigenschaft der Modellebene gemäß der Eigenschaft der Präsentationsebene festzulegen, bevor eine neue Animation wie folgt festgelegt wird:

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

0

Die einfachste Lösung ist die Verwendung impliziter Animationen. Dies wird all diese Probleme für Sie lösen:

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

Wenn Sie z. B. die Dauer anpassen möchten, können Sie Folgendes verwenden NSAnimationContext:

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

Hinweis: Dies wird nur unter macOS getestet.

Dabei habe ich zunächst keine Animation gesehen. Das Problem ist, dass die Ebene einer Ansichtssicherungsschicht keine Animation impliziert. Um dies zu lösen, stellen Sie sicher, dass Sie selbst eine Ebene hinzufügen (bevor Sie die Ansicht auf Ebenen-Backed setzen).

Ein Beispiel dafür wäre:

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

Die Verwendung self.wantsLayermachte keinen Unterschied in meinen Tests, aber es könnte einige Nebenwirkungen haben, die ich nicht kenne.


0

Hier ist ein Beispiel vom Spielplatz:

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. Erstellen Sie ein Animationsmodell
  2. Legen Sie die Startposition der Animation fest (kann übersprungen werden, dies hängt von Ihrem aktuellen Ebenenlayout ab).
  3. Legen Sie die Endposition der Animation fest
  4. Stellen Sie die Animationsdauer ein
  5. Verzögerung der Animation um eine Sekunde
  6. Nicht eingestellt falsezu isRemovedOnCompletion- lassen Core Animation sauber , nachdem die Animation beendet ist
  7. Hier ist der erste Teil des Tricks : Sie sagen zu Core Animation, dass Sie Ihre Animation an der Startposition platzieren sollen (Sie haben sie in Schritt 2 festgelegt), bevor die Animation überhaupt gestartet wurde. Erweitern Sie sie rechtzeitig rückwärts
  8. Kopieren Sie das vorbereitete Animationsobjekt, fügen Sie es der Ebene hinzu und starten Sie die Animation nach der Verzögerung (Sie haben in Schritt 5 festgelegt).
  9. Der zweite Teil besteht darin, die richtige Endposition der Ebene festzulegen. Nachdem die Animation gelöscht wurde, wird Ihre Ebene an der richtigen Stelle angezeigt
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.