CABasicAnimation redefine o valor inicial após a animação ser concluída


143

Estou girando um CALayer e tentando pará-lo em sua posição final após a animação ser concluída.

Mas depois que a animação é concluída, ela é redefinida para sua posição inicial.

(Os documentos do xcode dizem explicitamente que a animação não atualizará o valor da propriedade.)

alguma sugestão de como conseguir isso.


1
Essa é uma daquelas perguntas estranhas da SO em que quase todas as respostas estão totalmente erradas - totalmente totalmente ERRADAS . Você simplesmente olha .presentation()para obter o valor "final, visto". Pesquise abaixo as respostas corretas abaixo, que explicam isso com a camada de apresentação.
Fattie 27/08/19

Respostas:


285

Aqui está a resposta, é uma combinação da minha resposta e da de Krishnan.

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

O valor padrão é kCAFillModeRemoved. (Qual é o comportamento de redefinição que você está vendo.)


6
Esta provavelmente não é a resposta correta - veja o comentário na resposta de Krishnan. Você geralmente precisa disso e de Krishnan; apenas um ou outro não funcionará.
Adam

19
Esta não é a resposta correta, consulte a resposta de @Leslie Godwin abaixo.
Mark Ingram

6
A solução @Leslie é melhor porque armazena o valor final na camada e não na animação.
Matthieu Rouif 7/08/2014

1
Cuidado ao definir removeOnCompletion como NO. Se você estiver atribuindo o delegado da animação a "próprio", sua instância será mantida pela animação. Portanto, depois de chamar removeFromSuperview, se você esquecer de remover / destruir a animação por si próprio, por exemplo, desalocação não será chamada. Você terá um vazamento de memória.
emrahgunduz

1
Esta resposta de mais de 200 pontos é totalmente, completamente, totalmente incorreta.
Fattie 27/08/19

83

O problema com removedOnCompletion é que o elemento da interface do usuário não permite a interação do usuário.

A técnica é definir o valor FROM na animação e o valor TO no objeto. A animação preencherá automaticamente o valor TO antes de iniciar e, quando removida, deixará o objeto no estado correto.

// 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
Não funciona se você definir um atraso de início. por exemplo alphaAnimation.beginTime = 1; , além disso, fillModenão é necessário, tanto quanto eu posso dizer ...?
30513 Sam

caso contrário, este é uma boa resposta, a resposta aceita não vai deixar você mudar um valor, depois da conclusão do animação ...
Sam

2
eu também devo observar que isso beginTimenão é relativo, você deve usar, por exemplo::CACurrentMediaTime()+1;
Sam

É 'não', não é 'é' - não é seu.info. Desculpe se foi apenas um erro de digitação.
Fatuhoku

6
resposta correta, mas não precisa fillMode, veja mais em Impedir que as camadas retornem aos valores originais ao usar CAAnimations explícitas
likid1412

16

Defina a seguinte propriedade:

animationObject.removedOnCompletion = NO;

@Nilesh: aceite sua resposta. Isso dará a outros usuários de SO confiança em sua resposta.
Krishnan

7
Eu tive que usar os modos .fillMode = kCAFillModeForwards e .removedOnCompletion = NO. Obrigado!
William Rust

12

basta colocá-lo dentro do seu código

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;

12

Você pode simplesmente definir a chave de CABasicAnimationque positionquando você adicioná-lo à camada. Ao fazer isso, ele substituirá a animação implícita feita na posição da passagem atual no loop de execução.

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

Você pode obter mais informações sobre a Sessão de vídeo Apple WWDC 421 2011 - Core Animation Essentials (no meio do vídeo)


1
Parece a maneira correta de fazer isso. Animação é naturalmente removido (padrão), e uma vez animação é completa, retorna aos valores definidos antes animação iniciada, especificamente esta linha: someLayer.position = endPosition;. E obrigado pelo juiz da WWDC!
precisa saber é o seguinte

10

Um CALayer possui uma camada de modelo e uma camada de apresentação. Durante uma animação, a camada de apresentação é atualizada independentemente do modelo. Quando a animação é concluída, a camada de apresentação é atualizada com o valor do modelo. Se você deseja evitar um salto estridente após o término da animação, a chave é manter as duas camadas sincronizadas.

Se você conhece o valor final, basta definir o modelo diretamente.

self.view.layer.opacity = 1;

Mas se você tiver uma animação em que não conhece a posição final (por exemplo, um desvanecimento lento que o usuário pode pausar e depois reverter), poderá consultar a camada de apresentação diretamente para encontrar o valor atual e atualizar o modelo.

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

Puxar o valor da camada de apresentação também é particularmente útil para os caminhos-chave de escala ou rotação. (por exemplo transform.scale, transform.rotation)


8

Sem usar o removedOnCompletion

Você pode tentar esta técnica:

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
Esta parece ser a melhor resposta que funciona
Rambossa

Acordado. Observe no comentário do @yyman que você deve definir a propriedade .fromValue para o valor inicial. Caso contrário, você substituirá o valor inicial no modelo e haverá animação.
Jeff Collier

Essa e a resposta de @ jason-moore são melhores que a resposta aceita. Na resposta aceita, a animação está pausada, mas o novo valor não está realmente definido para a propriedade. Isso pode resultar em comportamento indesejado.
laka

8

Então, meu problema era que eu estava tentando girar um objeto no gesto de panorâmica e, portanto, tinha várias animações idênticas em cada movimento. Eu tinha os dois fillMode = kCAFillModeForwardse isRemovedOnCompletion = falsemas não ajudou. No meu caso, eu tinha que ter certeza de que a chave da animação é diferente cada vez que adiciono uma nova animação :

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

Simplesmente configurar fillModee removedOnCompletionnão funcionou para mim. Resolvi o problema definindo todas as propriedades abaixo para o objeto 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];

Esse código se transforma myViewem 85% de seu tamanho (terceira dimensão inalterada).


7

A animação principal mantém duas hierarquias de camada: a camada do modelo e a camada de apresentação . Quando a animação está em andamento, a camada do modelo está realmente intacta e mantém seu valor inicial. Por padrão, a animação é removida quando concluída. Em seguida, a camada de apresentação volta ao valor da camada do modelo.

Simplesmente definir removedOnCompletioncomo NOsignifica que a animação não será removida e desperdiçará memória. Além disso, a camada do modelo e a camada de apresentação não serão mais síncronas, o que pode levar a possíveis erros.

Portanto, seria uma solução melhor atualizar a propriedade diretamente na camada do modelo para o valor final.

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

Se houver alguma animação implícita causada pela primeira linha do código acima, tente desativar se:

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

Obrigado! Isso realmente deve ser a resposta aceita, como corretamente explica esta funcionalidade em vez de apenas usando um band-aid para fazer o trabalho
bplattenburg

6

Isso funciona:

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

A animação principal mostra uma 'camada de apresentação' no topo da sua camada normal durante a animação. Portanto, defina a opacidade (ou o que for) para o que você deseja que seja visto quando a animação terminar e a camada de apresentação desaparecer. Faça isso na linha antes de adicionar a animação para evitar tremulações quando ela for concluída.

Se você deseja atrasar, faça o seguinte:

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

Por fim, se você usar 'removedOnCompletion = false', ele vazará CAAnimations até que a camada seja descartada - evite.


Ótima resposta, ótimas dicas. Obrigado pelo comentário removedOnCompletion.
iWheelBuy

4

A resposta de @Leslie Godwin não é realmente boa ", self.view.layer.opacity = 1;" for feito imediatamente (leva cerca de um segundo), corrija alphaAnimation.duration para 10.0, se você tiver dúvidas. Você precisa remover esta linha.

Portanto, quando você fixa fillMode em kCAFillModeForwards e removeOnCompletion em NO, deixa a animação permanecer na camada. Se você corrigir o delegado da animação e tentar algo como:

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

... a camada é restaurada imediatamente no momento em que você executa esta linha. É o que queríamos evitar.

Você deve corrigir a propriedade da camada antes de remover a animação. Tente o seguinte:

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

Parece que o sinalizador removedOnCompletion definido como false e fillMode definido como kCAFillModeForwards também não funciona para mim.

Depois de aplicar uma nova animação em uma camada, um objeto de animação é redefinido para seu estado inicial e, em seguida, é animado a partir desse estado. O que precisa ser feito adicionalmente é definir a propriedade desejada da camada de modelo de acordo com a propriedade da camada de apresentação antes de definir uma nova animação da seguinte forma:

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

0

A solução mais fácil é usar animações implícitas. Isso resolverá todo esse problema para você:

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

Se você deseja personalizar, por exemplo, a duração, pode usar NSAnimationContext:

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

Nota: Isso é testado apenas no macOS.

Inicialmente, não vi nenhuma animação ao fazer isso. O problema é que a camada de uma camada com suporte de exibição não é animada implicitamente. Para resolver isso, adicione você mesmo uma camada (antes de definir a exibição como suportada por camada).

Um exemplo de como fazer isso seria:

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

O uso self.wantsLayernão fez nenhuma diferença nos meus testes, mas pode ter alguns efeitos colaterais que eu não conheço.


0

Aqui está uma amostra do playground:

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. Crie um modelo de animação
  2. Defina a posição inicial da animação (pode ser ignorada, depende do layout da camada atual)
  3. Definir posição final da animação
  4. Definir duração da animação
  5. Atraso na animação por um segundo
  6. Não defina falsecomo isRemovedOnCompletion- deixe a Animação principal limpa após o término da animação
  7. Aqui está a primeira parte do truque - você diz à Core Animation para colocar sua animação na posição inicial (você define na etapa # 2) antes mesmo de a animação ser iniciada - estenda-a para trás no tempo
  8. Copie o objeto de animação preparado, adicione-o à camada e inicie a animação após o atraso (você define na etapa 5)
  9. A segunda parte é definir a posição final correta da camada - após a animação ser excluída, sua camada será exibida no local correto
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.