¿Cancelar una animación UIView?

244

¿Es posible cancelar un UIView animación mientras está en progreso? ¿O tendría que bajar al nivel de CA?

es decir, he hecho algo como esto (tal vez establecer una acción de animación final también):

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

// set view properties

[UIView commitAnimations];

Pero antes de que la animación se complete y obtenga el evento de animación finalizada, quiero cancelarlo (acortarlo). es posible? Buscar en Google encuentra a algunas personas haciendo la misma pregunta sin respuestas, y una o dos personas especulando que no se puede hacer.

philsquared
fuente
¿Te refieres a pausar la animación en el medio y dejarla allí? ¿O volver a donde estaba al principio?
Chris Lundie
2
Ya sea dejándolo exactamente donde está, a mitad de la animación (en la práctica, comenzaré otra animación inmediatamente después de todos modos), o salte directamente al punto final. Me imagino que el primero es más natural, pero cualquiera de los dos me funciona en este caso.
philsquared
3
@ Chris Hanson: agradezco sus ediciones, pero me pregunto cuál es su razón para eliminar la etiqueta de iPhone. Puede estar implicado por el toque de cacao, pero por mi parte tengo un filtro de etiqueta en iPhone y me perdería esto en ese caso.
philsquared
@Boot To The Head (nuevamente): su pregunta, y mi propia respuesta a ella, me impulsaron a probar un poco más de forma aislada y descubrí que si comienza una nueva animación antes de que la primera haya terminado, salta al punto final antes de comenzar el nuevo.
philsquared
- esto satisface mis necesidades inmediatas (y sugiere que tengo algún otro error en mi código real), pero dejaré esta pregunta abierta ya que sé que otros han estado pidiendo lo mismo.
philsquared

Respuestas:

114

La forma en que lo hago es crear una nueva animación para su punto final. Establezca una duración muy corta y asegúrese de utilizar el +setAnimationBeginsFromCurrentState:método para comenzar desde el estado actual. Cuando lo establece en SÍ, la animación actual se acorta. Se parece a esto:

[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
fuente
Gracias Stephen En realidad, si usted lee los comentarios bajo mi pregunta esto es exactamente lo que hice al final - pero ya que has dado una buena, clara cuenta de que aquí estoy marcándolo como aceptado
philsquared
1
@Vojto ¿Cómo es eso? Los documentos dicen que setAnimationBeginsFromCurrentState:se desaconseja el uso de iOS 4, pero todavía está allí y debería funcionar.
Stephen Darlington
55
en iOS4 +, use la opción UIViewAnimationOptionBeginFromCurrentState en animateWithDuration: delay: opciones: animaciones: finalización:
Adam Eberbach
¿UIViewAnimationOptionBeginFromCurrentState cancelará cualquier animación en progreso o solo una animación que apunte a la misma propiedad? Si se trata de una animación, ¿cómo puede hacer que continúe una nueva animación en el mismo punto SIN detener la misma animación en progreso?
zakdances
1
@yourfriendzak, basado en una breve prueba, parece que solo cancela una animación dirigida a la misma propiedad. Intenté cambiar el alfa de un scrollView cuyo contentOffset estaba animado y la animación contentOffset continuó.
arlomedia
360

Utilizar:

#import <QuartzCore/QuartzCore.h>

.......

[myView.layer removeAllAnimations];
Jim Heising
fuente
22
Descubrí que tenía que agregar #import <QuartzCore / QuartzCore.h> para eliminar una advertencia del compilador;)
deanWombourne
44
Tan simple y tan difícil de encontrar. Gracias por esto, acabo de pasar una hora tratando de resolver esto.
MikeyWard
44
El posible problema con esta solución es que (al menos en iOS 5) hay un retraso antes de que surta efecto; no funciona lo suficientemente pronto si lo que quiere decir es "detener la animación ahora, antes de pasar a la siguiente línea de código".
mate el
14
Descubrí que llamar a esto desencadena la finalización: ^ (BOOL terminado) bloque si se usa + (void) animateWithDuration: (NSTimeInterval) animaciones de duración: (void (^) (void)) finalizaciones de animaciones: (vacío (^) (BOOL terminado) ) finalización
JWGS
1
@matt, ¿conoces una solución que detenga de inmediato la animación? También estoy observando un pequeño retraso antes de que la animación realmente se detenga.
tiguero
62

La forma más sencilla de detener todas las animaciones en una vista particular, de inmediato , es esta:

Enlace el proyecto a QuartzCore.framework. Al comienzo de su código:

#import <QuartzCore/QuartzCore.h>

Ahora, cuando desee detener todas las animaciones en una vista muerta en sus pistas, diga esto:

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

La línea media funcionaría por sí sola, pero hay un retraso hasta que finalice el runloop (el "momento de redibujado"). Para evitar ese retraso, envuelva el comando en un bloque de transacción explícito como se muestra. Esto funciona siempre que no se hayan realizado otros cambios en esta capa en el bucle de ejecución actual.

mate
fuente
2
He estado usando [CATransaction flush] después de su código que funciona muy bien, a veces porque no escribe de inmediato. Sin embargo, hay un problema de rendimiento de 0.1 segundos, por ejemplo, se retrasa en ese momento en particular. ¿Alguna solución?
Sidwyn Koh
55
removeAllAnimationstendrá el efecto secundario de llevar la animación a su cuadro final, en lugar de detenerla donde está ahora.
Ilya
3
Normalmente, cuando uno quiere que una animación se detenga, espera que se detenga en su cuadro actual, no en el primer cuadro ni en el último cuadro. Saltar al primer o último fotograma se verá como un movimiento desigual.
Ilya
1
Luego detallaría la respuesta para especificar cuál es el comportamiento predeterminado y cómo se puede cambiar. Obviamente, alguien que está haciendo animación está activamente interesado en lo que sucede en la pantalla después de sus llamadas de método :)
Ilya
1
Lo he detallado en otra parte. Eso no es lo que se pidió, así que no es lo que respondí aquí. Si tienes otra respuesta, ¡definitivamente dala como respuesta!
mate
48

En iOS 4 y superior, use la UIViewAnimationOptionBeginFromCurrentStateopción en la segunda animación para acortar la primera animación.

Como ejemplo, suponga que tiene una vista con un indicador de actividad. Desea desvanecerse en el indicador de actividad mientras comienza una actividad que puede llevar mucho tiempo, y desvanecerse cuando la actividad haya terminado. En el siguiente código, se llama a la vista con el indicador de actividad 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
fuente
41

Para cancelar una animación, simplemente necesita establecer la propiedad que se está animando actualmente, fuera de la animación UIView. Eso detendrá la animación donde sea que esté, y la UIView saltará a la configuración que acaba de definir.

tomtaylor
fuente
99
Esto sólo es parcialmente cierto. Detiene la animación, pero no impide que se activen los métodos de delegado, sobre todo, el controlador animationComplete, por lo que si tiene lógica allí, verá problemas.
M. Ryan
33
Para eso está el argumento 'terminado' -animationDidStop:finished:context:. Si [finished boolValue] == NO, entonces sabes que tu animación fue cancelada de alguna manera.
Nick Forge
¡Este es un gran consejo de hace 7 años! tenga en cuenta que hay un comportamiento extraño: digamos que está animando alfa de un lado a otro, y lo hace "ir a 0". para DETENER la animación, no puede establecer alfa en cero; lo configuras para decir 1.
Fattie
26

Lamento resucitar esta respuesta, pero en iOS 10 las cosas cambiaron un poco, y ahora es posible cancelar e incluso puedes cancelar con gracia.

¡Después de iOS 10, puede cancelar las animaciones con UIViewPropertyAnimator !

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

Si pasa verdadero, cancela la animación y se detiene justo donde la canceló. No se llamará al método de finalización. Sin embargo, si pasa falso, usted es responsable de terminar la animación:

animator.finishAnimation(.start)

Puede finalizar su animación y permanecer en el estado actual (.current) o ir al estado inicial (.start) o al estado final (.end)

Por cierto, incluso puedes pausar y reiniciar más tarde ...

animator.pauseAnimation()
animator.startAnimation()

Nota: ¡Si no desea una cancelación abrupta, puede revertir su animación o incluso cambiar su animación después de pausarla!

Tiago Almeida
fuente
2
Este es definitivamente el más relevante para las animaciones modernas.
Doug
10

Si está animando una restricción cambiando la propiedad constante en lugar de una vista, ninguno de los otros métodos funciona en iOS 8.

Ejemplo de animación:

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

                 }];

Solución:

Debe eliminar las animaciones de las capas de las vistas afectadas por el cambio de restricción y sus subcapas.

[self.constraintView.layer removeAllAnimations];
for (CALayer *l in self.constraintView.layer.sublayers)
{
    [l removeAllAnimations];
}
Nikola Lajic
fuente
1
También solo self.constraintView.layer.removeAllAnimations()funciona (Swift)
Lachtan
Ninguna de las otras soluciones funcionó. Gracias, funcionó para mí.
swapnilagarwal
5

Ninguno de los anteriores me lo resolvió, pero esto ayudó: la UIViewanimación establece la propiedad de inmediato, luego la anima. Detiene la animación cuando la capa de presentación coincide con el modelo (la propiedad establecida).

Resolví mi problema, que era "Quiero animar desde donde pareces" ("tú" significa la vista). Si quieres ESO, entonces:

  1. Añadir QuartzCore.
  2. CALayer * pLayer = theView.layer.presentationLayer;

establecer la posición a la capa de presentación

Yo uso algunas opciones incluyendo UIViewAnimationOptionOverrideInheritedDuration

Pero debido a que la documentación de Apple es vaga, no sé si realmente anula las otras animaciones cuando se usa, o simplemente restablece los 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) {}];

Y esa debería ser una solución decente por ahora.

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

Versión rápida de la solución de Stephen Darlington

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

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

Tengo el mismo problema; las API no tienen nada para cancelar alguna animación específica. los

+ (void)setAnimationsEnabled:(BOOL)enabled

desactiva TODAS las animaciones y, por lo tanto, no funciona para mí. Hay dos soluciones:

1) convierte tu objeto animado en una subvista. Luego, cuando desee cancelar las animaciones para esa vista, elimine la vista u oculte. Muy simple, pero necesita recrear la subvista sin animaciones si necesita mantenerla a la vista.

2) repita el anim solo, y haga un selector de delegado para reiniciar el anim si es necesario, de esta manera:

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

El problema con 2) es que siempre hay un retraso para detener el anim cuando termina el ciclo de animación actual.

Espero que esto ayude.


fuente
1

Incluso si cancela la animación de la manera anterior, la animación didStopSelectoraún se ejecuta. Entonces, si tiene estados lógicos en su aplicación impulsados ​​por animaciones, tendrá problemas. Por esta razón, con las formas descritas anteriormente, uso la variable de contexto de las UIViewanimaciones. Si pasa el estado actual de su programa por el parámetro de contexto a la animación, cuando la animación se detiene, su didStopSelectorfunción puede decidir si debe hacer algo o simplemente regresar según el estado actual y el valor de estado pasado como contexto.

Gorki
fuente
NickForge explica esto perfectamente en el comentario debajo de la respuesta correcta de TomTaylor arriba ...
Fattie
Muchas gracias por esta nota! De hecho, tengo una situación en la que se activa una próxima animación cuando se llama a animationDidStop. Si simplemente elimina una animación e inmediatamente vuelve a agregar una nueva para la misma clave, creo que no funcionará. Debe esperar, por ejemplo, hasta que animationDidStop se active para otra animación u otra cosa. La animación que se acaba de eliminar ya no activará animationDidStop.
RickJansen
0
CALayer * pLayer = self.layer.presentationLayer;
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView animateWithDuration:0.001 animations:^{
    self.frame = pLayer.frame;
}];
Thunder Rabbit
fuente
0

Para pausar una animación sin revertir el estado a original o final:

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

Cuando trabajo con UIStackViewanimación, además removeAllAnimations()necesito establecer algunos valores en uno inicial porque removeAllAnimations()puedo establecerlos en un estado impredecible. Tengo stackViewcon view1y view2dentro, y una vista debe ser visible y otra 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.
fuente
0

Ninguna de las soluciones respondidas funcionó para mí. Resolví mis problemas de esta manera (¿no sé si es la forma correcta?), Porque tuve problemas al llamar a esto demasiado rápido (cuando la animación anterior aún no estaba terminada). Paso mi animación deseada con el bloque 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
fuente