CABasicAnimation se réinitialise à sa valeur initiale une fois l'animation terminée


143

Je fais tourner un CALayer et j'essaie de l'arrêter à sa position finale une fois l'animation terminée.

Mais une fois l'animation terminée, elle revient à sa position initiale.

(les documents xcode indiquent explicitement que l'animation ne mettra pas à jour la valeur de la propriété.)

toutes les suggestions pour y parvenir.


1
C'est l'une de ces étranges questions SO où presque toutes les réponses sont totalement fausses - tout simplement totalement FAUX . Vous regardez très simplement .presentation()pour obtenir la valeur "finale, vue". Recherchez les bonnes réponses ci-dessous qui expliquent que c'est fait avec la couche de présentation.
Fattie

Réponses:


285

Voici la réponse, c'est une combinaison de ma réponse et de celle de Krishnan.

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

La valeur par défaut est kCAFillModeRemoved. (Quel est le comportement de réinitialisation que vous voyez.)


6
Ce n'est probablement PAS la bonne réponse - voir le commentaire sur la réponse de Krishnan. Vous en avez généralement besoin ET de Krishnan; l'un ou l'autre ne fonctionnera pas.
Adam le

19
Ce n'est pas la bonne réponse, voir la réponse de @Leslie Godwin ci-dessous.
Mark Ingram

6
La solution @Leslie est meilleure car elle stocke la valeur finale dans le calque et non dans l'animation.
Matthieu Rouif

1
Soyez prudent lorsque vous définissez removeOnCompletion sur NO. Si vous affectez le délégué d'animation à «soi», votre instance sera conservée par l'animation. Ainsi, après avoir appelé removeFromSuperview si vous oubliez de supprimer / détruire l'animation par vous-même, dealloc par exemple ne sera pas appelé. Vous aurez une fuite de mémoire.
emrahgunduz

1
Cette réponse de plus de 200 points est totalement, complètement, totalement incorrecte.
Fattie

83

Le problème avec removeOnCompletion est que l'élément d'interface utilisateur ne permet pas l'interaction de l'utilisateur.

La technique consiste à définir la valeur FROM dans l'animation et la valeur TO sur l'objet. L'animation remplira automatiquement la valeur TO avant de commencer et, lorsqu'elle sera supprimée, laissera l'objet dans son état correct.

// 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
Ne fonctionne pas si vous définissez un délai de démarrage. par exemple alphaAnimation.beginTime = 1; , le fillModen'est pas nécessaire pour autant que je sache ...?
Sam

sinon, c'est une bonne réponse, la réponse acceptée ne vous permettra pas de modifier une valeur une fois l'animation terminée ...
Sam

2
je dois également noter que ce beginTimen'est pas relatif, vous devriez utiliser par exemple:CACurrentMediaTime()+1;
Sam

C'est «c'est» pas «c'est» - its-not-its.info. Désolé si ce n'était qu'une faute de frappe.
fatuhoku


16

Définissez la propriété suivante:

animationObject.removedOnCompletion = NO;

@Nilesh: Acceptez votre réponse. Cela donnera confiance aux autres utilisateurs SO sur votre réponse.
Krishnan

7
J'ai dû utiliser à la fois .fillMode = kCAFillModeForwards et .removedOnCompletion = NO. Merci!
William Rust

12

mettez-le simplement dans votre code

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

12

Vous pouvez simplement définir la clé de CABasicAnimationsur positionlorsque vous l'ajoutez au calque. En faisant cela, il remplacera l'animation implicite effectuée sur la position de la passe actuelle dans la boucle d'exécution.

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

Vous pouvez avoir plus d'informations sur 2011 Apple WWDC Video Session 421 - Core Animation Essentials (milieu de la vidéo)


1
Cela ressemble à la bonne façon de procéder. L' animation est naturellement supprimée (par défaut), et une fois l' animation terminée, il retourne aux valeurs définies avant l' animation a commencé, plus précisément cette ligne: someLayer.position = endPosition;. Et merci pour la référence à la WWDC!
bauerMusic

10

Un CALayer a une couche de modèle et une couche de présentation. Lors d'une animation, la couche de présentation est mise à jour indépendamment du modèle. Une fois l'animation terminée, le calque de présentation est mis à jour avec la valeur du modèle. Si vous voulez éviter un saut discordant après la fin de l'animation, la clé est de garder les deux couches synchronisées.

Si vous connaissez la valeur finale, vous pouvez simplement définir le modèle directement.

self.view.layer.opacity = 1;

Mais si vous avez une animation dont vous ne connaissez pas la position finale (par exemple un fondu lent que l'utilisateur peut mettre en pause puis inverser), vous pouvez interroger directement la couche de présentation pour trouver la valeur actuelle, puis mettre à jour le modèle.

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

Extraire la valeur du calque de présentation est également particulièrement utile pour la mise à l'échelle ou la rotation des chemins d'accès. (par exemple transform.scale, transform.rotation)


8

Sans utiliser le removedOnCompletion

Vous pouvez essayer cette technique:

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
Cela semble être la meilleure réponse
rambossa

D'accord. Notez le commentaire de @ ayman selon lequel vous devez définir la propriété .fromValue sur la valeur de départ. Sinon, vous aurez écrasé la valeur de départ dans le modèle et il y aura une animation.
Jeff Collier

Celle-ci et la réponse de @ jason-moore valent mieux que la réponse acceptée. Dans la réponse acceptée, l'animation est simplement mise en pause mais la nouvelle valeur n'est pas réellement définie sur la propriété. Cela peut entraîner un comportement indésirable.
laka

8

Mon problème était donc que j'essayais de faire pivoter un objet sur un mouvement de panoramique et que j'avais donc plusieurs animations identiques à chaque mouvement. J'avais les deux fillMode = kCAFillModeForwardset isRemovedOnCompletion = falsecela n'a pas aidé. Dans mon cas, je devais m'assurer que la clé d'animation est différente à chaque fois que j'ajoute une nouvelle animation :

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

Simplement régler fillModeet removedOnCompletionn'a pas fonctionné pour moi. J'ai résolu le problème en définissant toutes les propriétés ci-dessous sur l'objet 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];

Ce code se transforme myViewà 85% de sa taille (3ème dimension inchangée).


7

L'animation principale conserve deux hiérarchies de couches: la couche de modèle et la couche de présentation . Lorsque l'animation est en cours, le calque du modèle est en fait intact et conserve sa valeur initiale. Par défaut, l'animation est supprimée une fois qu'elle est terminée. Ensuite, la couche de présentation revient à la valeur de la couche de modèle.

Définir simplement removedOnCompletionsur NOsignifie que l'animation ne sera pas supprimée et gaspille de la mémoire. De plus, la couche modèle et la couche présentation ne seront plus synchrones, ce qui peut entraîner des bogues potentiels.

Il serait donc préférable de mettre à jour la propriété directement sur la couche de modèle à la valeur finale.

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

S'il y a une animation implicite causée par la première ligne du code ci-dessus, essayez de la désactiver:

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

Je vous remercie! Cela devrait vraiment être la réponse acceptée, car elle explique correctement cette fonctionnalité au lieu d'utiliser simplement un
pansement

6

Cela marche:

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'animation principale montre un «calque de présentation» au-dessus de votre calque normal pendant l'animation. Définissez donc l'opacité (ou autre) sur ce que vous voulez voir lorsque l'animation se termine et que le calque de présentation disparaît. Faites-le sur la ligne avant d'ajouter l'animation pour éviter un scintillement une fois celle-ci terminée.

Si vous souhaitez avoir un retard, procédez comme suit:

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

Enfin, si vous utilisez 'removedOnCompletion = false', CAAnimations perdra jusqu'à ce que la couche soit finalement éliminée - évitez.


Excellente réponse, bons conseils. Merci pour le commentaire removeOnCompletion.
iWheelBuy le

4

La réponse de @Leslie Godwin n'est pas vraiment bonne, "self.view.layer.opacity = 1;" est fait immédiatement (cela prend environ une seconde), veuillez fixer alphaAnimation.duration à 10.0, si vous avez des doutes. Vous devez supprimer cette ligne.

Ainsi, lorsque vous fixez fillMode à kCAFillModeForwards et removeOnCompletion à NO, vous laissez l'animation rester dans la couche. Si vous corrigez le délégué d'animation et essayez quelque chose comme:

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

... le calque se rétablit immédiatement au moment où vous exécutez cette ligne. C'est ce que nous voulions éviter.

Vous devez corriger la propriété du calque avant d'en supprimer l'animation. Essaye ça:

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

Il semble que l'indicateur removeOnCompletion défini sur false et fillMode sur kCAFillModeForwards ne fonctionne pas non plus pour moi.

Après avoir appliqué une nouvelle animation sur un calque, un objet animé se réinitialise à son état initial, puis s'anime à partir de cet état. Ce qui doit être fait en plus est de définir la propriété souhaitée de la couche de modèle en fonction de la propriété de sa couche de présentation avant de définir une nouvelle animation comme ceci:

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

0

La solution la plus simple consiste à utiliser des animations implicites. Cela gérera tous ces problèmes pour vous:

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

Si vous souhaitez personnaliser par exemple la durée, vous pouvez utiliser NSAnimationContext:

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

Remarque: ceci n'est testé que sur macOS.

Au départ, je n'ai vu aucune animation en faisant cela. Le problème est que le calque d'un calque basé sur la vue ne s'anime pas implicitement. Pour résoudre ce problème, assurez-vous d'ajouter un calque vous-même (avant de définir la vue sur calque).

Un exemple de comment procéder serait:

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

L'utilisation self.wantsLayern'a fait aucune différence dans mes tests, mais cela pourrait avoir des effets secondaires que je ne connais pas.


0

Voici un échantillon du terrain de jeu:

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. Créer un modèle d'animation
  2. Définir la position de départ de l'animation (peut être ignorée, cela dépend de la disposition de votre calque actuelle)
  3. Définir la position finale de l'animation
  4. Définir la durée de l'animation
  5. Retarder l'animation d'une seconde
  6. Ne pas définir falsesur isRemovedOnCompletion- laissez Core Animation nettoyer une fois l'animation terminée
  7. Voici la première partie de l'astuce - vous dites à Core Animation de placer votre animation à la position de départ (que vous avez définie à l'étape 2) avant même que l'animation n'ait été lancée - prolongez-la dans le temps
  8. Copiez l'objet d'animation préparé, ajoutez-le au calque et démarrez l'animation après le délai (que vous avez défini à l'étape 5)
  9. La deuxième partie consiste à définir la position finale correcte du calque - une fois l'animation supprimée, votre calque sera affiché au bon endroit
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.