Cancelar uma animação UIView?

244

É possível cancelar uma UIViewanimação enquanto ela está em andamento? Ou eu teria que cair para o nível da CA?

ou seja, eu fiz algo assim (talvez definindo uma ação de animação final também):

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
// other animation properties

// set view properties

[UIView commitAnimations];

Porém, antes que a animação seja concluída e o evento finalize, quero cancelá-la (abrevie). Isso é possível? Ao pesquisar no Google, algumas pessoas fazem a mesma pergunta sem respostas - e uma ou duas pessoas especulando que isso não pode ser feito.

philsquared
fonte
Você quer dizer pausar a animação no meio e deixá-la lá? Ou voltando para onde estava no começo?
31510 Chris Lundie
2
Deixá-lo exatamente onde está, no meio da animação (na prática, começarei outra animação logo depois), ou pular direto para o ponto final. Imagino que o primeiro seja mais natural, mas, nesse caso, funciona para mim.
Philsquared 17/02/09
3
@ Chris Hanson: Agradeço suas edições, mas me pergunto qual é a sua lógica para remover a etiqueta do iPhone. Isso pode estar implícito no cacau-touch, mas eu tenho um filtro de tags no iPhone e, nesse caso, sentiria falta disso.
philsquared 17/02/09
@ Boot To The Head (novamente): sua pergunta e minha própria resposta a ela me levaram a testar um pouco mais o isolamento e descobri que se você iniciar uma nova animação antes que a primeira termine, ela pula para o ponto final antes de iniciar o novo.
Philsquared 17/02/09
- isso atende às minhas necessidades imediatas (e sugere que eu tenha outro bug no meu código real), mas vou deixar essa pergunta em aberto, pois sei que outras pessoas estão pedindo o mesmo.
Philsquared 17/02/09

Respostas:

114

O jeito que eu faço é criar uma nova animação para o seu ponto final. Defina uma duração muito curta e use o +setAnimationBeginsFromCurrentState:método para iniciar a partir do estado atual. Quando você o define como SIM, a animação atual é cortada. Parece algo assim:

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.1];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
// other animation properties

// set view properties

[UIView commitAnimations];
Stephen Darlington
fonte
Obrigado Stephen. Na verdade, se você ler os comentários sob a minha pergunta é exatamente isso que eu fiz no final - mas desde que você deu uma boa, claro, conta-lo aqui estou marcando-o como aceito
philsquared
1
@Vojto Como assim? Os documentos dizem que o uso de setAnimationBeginsFromCurrentState:é desencorajado no iOS 4, mas ainda está lá e ainda deve funcionar.
Stephen Darlington
55
em iOS4 +, use a opção UIViewAnimationOptionBeginFromCurrentState em animateWithDuration: Atraso: opções: animações: conclusão:
Adam Eberbach
UIViewAnimationOptionBeginFromCurrentState cancelará qualquer animação em andamento ou apenas uma animação que tenha como alvo a mesma propriedade? Se houver alguma animação, como você pode continuar com uma nova animação no mesmo ponto SEM parar a mesma animação em andamento?
precisa saber é o seguinte
1
@yourfriendzak, com base em um breve teste, parece que apenas cancela uma animação direcionada à mesma propriedade. Tentei alterar o alfa de um scrollView cujo contentOffset foi animado e a animação contentOffset continuou.
Arlomedia 17/08/2012
360

Usar:

#import <QuartzCore/QuartzCore.h>

.......

[myView.layer removeAllAnimations];
Jim Heising
fonte
22
Eu descobri que eu tinha para adicionar #import <QuartzCore / QuartzCore.h> para remover um aviso do compilador;)
deanWombourne
4
Tão simples e tão difícil de encontrar. Obrigado por isso, passei apenas uma hora tentando descobrir isso.
precisa saber é o seguinte
4
O possível problema com esta solução é que (pelo menos no iOS 5) há um atraso antes que ela entre em vigor - não funcionará tão cedo se o que você quer dizer é "interrompa a animação agora mesmo, antes de prosseguir para a próxima linha de código".
18716
14
Descobri que chamar isso aciona a conclusão: ^ (BOOL concluído) bloco se estiver usando + (void) animateWithDuration: (NSTimeInterval) animações de duração: (void (^) (void)) conclusão de animações: (void (^) (BOOL terminado) ) conclusão
JWGS 04/10/12
1
@matt você conhece uma solução que interrompa imediatamente a animação? Também estou observando um pequeno atraso antes que a animação realmente pare.
usar o seguinte comando
62

A maneira mais simples de interromper imediatamente todas as animações em uma exibição específica é:

Vincule o projeto ao QuartzCore.framework. No início do seu código:

#import <QuartzCore/QuartzCore.h>

Agora, quando você quiser interromper todas as animações em uma exibição morta nas faixas, diga o seguinte:

[CATransaction begin];
[theView.layer removeAllAnimations];
[CATransaction commit];

A linha do meio funcionaria por si só, mas há um atraso até que o runloop termine (o "momento de redesenho"). Para evitar esse atraso, agrupe o comando em um bloco de transação explícito, como mostrado. Isso funciona, desde que nenhuma outra alteração tenha sido executada nessa camada no loop de execução atual.

mate
fonte
2
Eu tenho usado [CATransaction flush] após o seu código que funciona muito bem, às vezes porque ele não grava imediatamente. Mas, no entanto, há um problema de desempenho de 0,1 segundo, por exemplo, fica para esse tempo específico. Alguma solução alternativa?
Sidwyn Koh
5
removeAllAnimationsterá o efeito colateral de trazer a animação para o quadro final, em vez de parar onde está no momento.
Ilya
3
Normalmente, quando alguém deseja que uma animação pare, espera que ela pare no quadro atual - não no primeiro ou no último quadro. Pular para o primeiro ou o último quadro parecerá um movimento brusco.
Ilya
1
Eu detalharia a resposta para especificar qual é o comportamento padrão e como ele pode ser alterado. Obviamente alguém fazendo animação está interessado ativamente no que acontece na tela seguindo suas chamadas de método :)
Ilya
1
Eu o detalhei em outro lugar. Não foi isso que foi solicitado, então não foi o que eu respondi aqui. Se você tiver outra resposta, definitivamente dê-a como resposta!
Matt
48

No iOS 4 e superior, use a UIViewAnimationOptionBeginFromCurrentStateopção na segunda animação para encurtar a primeira.

Como exemplo, suponha que você tenha uma visão com um indicador de atividade. Você deseja desvanecer-se no indicador de atividade enquanto alguma atividade potencialmente demorada começa e desaparecer quando a atividade for concluída. No código abaixo, a visualização com o indicador de atividade é chamada activityView.

- (void)showActivityIndicator {
    activityView.alpha = 0.0;
    activityView.hidden = NO;
    [UIView animateWithDuration:0.5
                 animations:^(void) {
                     activityView.alpha = 1.0;
                 }];

- (void)hideActivityIndicator {
    [UIView animateWithDuration:0.5
                 delay:0 options:UIViewAnimationOptionBeginFromCurrentState
                 animations:^(void) {
                     activityView.alpha = 0.0;
                 }
                 completion:^(BOOL completed) {
                     if (completed) {
                         activityView.hidden = YES;
                     }
                 }];
}
Ville Laurikari
fonte
41

Para cancelar uma animação, basta definir a propriedade que está sendo animada atualmente, fora da animação UIView. Isso interromperá a animação onde quer que esteja, e o UIView irá para a configuração que você acabou de definir.

tomtaylor
fonte
9
Isso é apenas parcialmente verdade. Ele interrompe a animação, mas não impede o acionamento de métodos delegados, principalmente o manipulador animationComplete, portanto, se você tiver lógica, verá problemas.
M. Ryan
33
É para isso que -animationDidStop:finished:context:serve o argumento 'terminado' . Se [finished boolValue] == NO, você sabe que sua animação foi de alguma forma cancelada.
Nick Forge
Esta é uma ótima dica de 7 anos atrás! observe que há um comportamento estranho: digamos que você esteja animando o alfa para frente e para trás e faça com que ele "vá para 0". para parar a animação, você não pode definir alfa como zero; você configurá-lo para dizer 1.
Fattie
26

Desculpe ressuscitar esta resposta, mas no iOS 10 as coisas meio que mudaram, e agora o cancelamento é possível e você pode até cancelar graciosamente!

Após o iOS 10, você pode cancelar animações com o UIViewPropertyAnimator !

UIViewPropertyAnimator(duration: 2, dampingRatio: 0.4, animations: {
    view.backgroundColor = .blue
})
animator.stopAnimation(true)

Se você passar verdadeiro, a animação é cancelada e para exatamente onde você a cancelou. O método de conclusão não será chamado. No entanto, se você passar falso, é responsável por terminar a animação:

animator.finishAnimation(.start)

Você pode terminar sua animação e permanecer no estado atual (.current) ou ir para o estado inicial (.start) ou para o estado final (.end)

A propósito, você pode até pausar e reiniciar mais tarde ...

animator.pauseAnimation()
animator.startAnimation()

Nota: Se você não deseja um cancelamento abrupto, pode reverter sua animação ou até mesmo alterá-la após fazer uma pausa!

Tiago Almeida
fonte
2
Este é definitivamente o mais relevante para animações modernas.
Doug
10

Se você estiver animando uma restrição alterando a propriedade constante em vez de uma visualização, nenhum dos outros métodos funcionará no iOS 8.

Exemplo de animação:

self.constraint.constant = 0;
[self.view updateConstraintsIfNeeded];
[self.view layoutIfNeeded];
[UIView animateWithDuration:1.0f
                      delay:0.0f
                    options:UIViewAnimationOptionCurveLinear
                 animations:^{
                     self.constraint.constant = 1.0f;
                     [self.view layoutIfNeeded];
                 } completion:^(BOOL finished) {

                 }];

Solução:

Você precisa remover as animações das camadas de qualquer visualização afetada pela alteração de restrição e suas subcamadas.

[self.constraintView.layer removeAllAnimations];
for (CALayer *l in self.constraintView.layer.sublayers)
{
    [l removeAllAnimations];
}
Nikola Lajic
fonte
1
Também self.constraintView.layer.removeAllAnimations()funciona (Swift)
Lachtan
Nenhuma das outras soluções funcionou. Obrigado, funcionou para mim.
swapnilagarwal
5

Nenhuma das opções acima resolveu isso para mim, mas isso ajudou: A UIViewanimação define a propriedade imediatamente e a anima. Ele interrompe a animação quando a camada de apresentação corresponde ao modelo (a propriedade definida).

Resolvi meu problema, que era "Quero animar de onde você parece" ('você' significa a exibição). Se você quiser ISSO, então:

  1. Adicione QuartzCore.
  2. CALayer * pLayer = theView.layer.presentationLayer;

defina a posição para a camada de apresentação

Eu uso algumas opções, incluindo UIViewAnimationOptionOverrideInheritedDuration

Mas como a documentação da Apple é vaga, não sei se ela realmente substitui as outras animações quando usada ou apenas redefine os temporizadores.

[UIView animateWithDuration:blah... 
                    options: UIViewAnimationOptionBeginFromCurrentState ... 
                    animations: ^ {
                                   theView.center = CGPointMake( pLayer.position.x + YOUR_ANIMATION_OFFSET, pLayer.position.y + ANOTHER_ANIMATION_OFFSET);
                   //this only works for translating, but you get the idea if you wanna flip and scale it. 
                   } completion: ^(BOOL complete) {}];

E essa deve ser uma solução decente por enquanto.

NazCodezz
fonte
5
[UIView setAnimationsEnabled:NO];
// your code here
[UIView setAnimationsEnabled:YES];
ideawu
fonte
2

Versão rápida da solução de Stephen Darlington

UIView.beginAnimations(nil, context: nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(0.1)
// other animation properties

// set view properties
UIView.commitAnimations()
lucamegh
fonte
1

Eu tenho o mesmo problema; as APIs não têm nada para cancelar algumas animações específicas. o

+ (void)setAnimationsEnabled:(BOOL)enabled

desativa TODAS as animações e, portanto, não funciona para mim. Existem duas soluções:

1) faça do seu objeto animado uma sub-visualização. Então, quando você desejar cancelar as animações para essa exibição, remova-a ou oculte-a. Muito simples, mas você precisa recriar a subvisão sem animações se precisar mantê-la à vista.

2) repita o anim apenas um e faça um seletor de delegado para reiniciar o anim, se necessário, assim:

-(void) startAnimation {
NSLog(@"startAnim alpha:%f", self.alpha);
[self setAlpha:1.0];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(pulseAnimationDidStop:finished:context:)];
[self setAlpha:0.1];
[UIView commitAnimations];
}

- (void)pulseAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
if(hasFocus) {
    [self startAnimation];
} else {
    self.alpha = 1.0;
}
}

-(void) setHasFocus:(BOOL)_hasFocus {
hasFocus = _hasFocus;
if(hasFocus) {
    [self startAnimation];
}
}

O problema com 2) é que sempre há atraso para parar o anim, pois termina o ciclo de animação atual.

Espero que isto ajude.


fonte
1

Mesmo se você cancelar a animação das maneiras acima, a animação didStopSelectorainda será executada. Portanto, se você tiver estados lógicos em seu aplicativo orientados por animações, terá problemas. Por esse motivo, com as formas descritas acima, uso a variável de contexto das UIViewanimações. Se você passar o estado atual do seu programa pelo parâmetro context para a animação, quando a animação parar, sua didStopSelectorfunção poderá decidir se deve fazer algo ou apenas retornar com base no estado atual e no valor do estado passado como contexto.

Gorky
fonte
NickForge explica isso perfeitamente no comentário abaixo resposta correta do TomTaylor acima ...
Fattie
Muito obrigado por esta nota! Na verdade, eu tenho essa situação, em que uma próxima animação é acionada quando o animationDidStop é chamado. Se você simplesmente remover uma animação e imediatamente adicionar novamente uma nova para a mesma chave, acho que ela não funcionará. Você precisa esperar, por exemplo, até que animationDidStop seja acionado para outra animação ou outra coisa. A animação que acabou de ser removida não dispara mais animationDidStop.
RickJansen
0
CALayer * pLayer = self.layer.presentationLayer;
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView animateWithDuration:0.001 animations:^{
    self.frame = pLayer.frame;
}];
Coelho Trovão
fonte
0

Para pausar uma animação sem reverter o estado para o original ou final:

CFTimeInterval paused_time = [myView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
myView.layer.speed = 0.0;
myView.layer.timeOffset = paused_time;
tommybananas
fonte
0

Quando trabalho com UIStackViewanimação, além disso removeAllAnimations(), preciso definir alguns valores para o inicial porque removeAllAnimations()pode configurá-los para um estado imprevisível. Eu tenho stackViewcom view1e por view2dentro, e uma visão deve ser visível e uma oculta:

public func configureStackView(hideView1: Bool, hideView2: Bool) {
    let oldHideView1 = view1.isHidden
    let oldHideView2 = view2.isHidden
    view1.layer.removeAllAnimations()
    view2.layer.removeAllAnimations()
    view.layer.removeAllAnimations()
    stackView.layer.removeAllAnimations()
    // after stopping animation the values are unpredictable, so set values to old
    view1.isHidden = oldHideView1 //    <- Solution is here
    view2.isHidden = oldHideView2 //    <- Solution is here

    UIView.animate(withDuration: 0.3,
                   delay: 0.0,
                   usingSpringWithDamping: 0.9,
                   initialSpringVelocity: 1,
                   options: [],
                   animations: {
                    view1.isHidden = hideView1
                    view2.isHidden = hideView2
                    stackView.layoutIfNeeded()
    },
                   completion: nil)
}
Paul T.
fonte
0

Nenhuma das soluções respondidas funcionou para mim. Resolvi meus problemas dessa maneira (não sei se é o caminho correto?), Porque tive problemas ao chamar isso muito rápido (quando a animação anterior ainda não estava concluída). Passei minha animação desejada com o bloco customAnim .

extension UIView
{

    func niceCustomTranstion(
        duration: CGFloat = 0.3,
        options: UIView.AnimationOptions = .transitionCrossDissolve,
        customAnim: @escaping () -> Void
        )
    {
        UIView.transition(
            with: self,
            duration: TimeInterval(duration),
            options: options,
            animations: {
                customAnim()
        },
            completion: { (finished) in
                if !finished
                {
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    self.layer.removeAllAnimations()
                    customAnim()
                }
        })

    }

}
sabiland
fonte