CABasicAnimation se restablece al valor inicial una vez que se completa la animación

143

Estoy girando un CALayer e intento detenerlo en su posición final después de completar la animación.

Pero después de que la animación se completa, se restablece a su posición inicial.

(Los documentos de xcode dicen explícitamente que la animación no actualizará el valor de la propiedad).

alguna sugerencia de cómo lograr esto.

Nilesh Ukey
fuente
1
Esta es una de esas extrañas preguntas SO donde casi todas las respuestas son completamente incorrectas, simplemente totalmente INCORRECTAS . Simplemente mira .presentation()para obtener el valor "final, visto". Busque las respuestas correctas a continuación que explican que se hace con la capa de presentación.
Fattie

Respuestas:

285

Aquí está la respuesta, es una combinación de mi respuesta y la de Krishnan.

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

El valor por defecto es kCAFillModeRemoved. (¿Cuál es el comportamiento de reinicio que estás viendo?)

Nilesh Ukey
fuente
66
Probablemente esta NO sea la respuesta correcta; vea el comentario sobre la respuesta de Krishnan. Usualmente necesitas esto Y Krishnan's; solo uno u otro no funcionará.
Adam
19
Esta no es la respuesta correcta, vea la respuesta de @Leslie Godwin a continuación.
Mark Ingram
66
La solución @Leslie es mejor porque almacena el valor final en la capa y no en la animación.
Matthieu Rouif
1
Tenga cuidado cuando esté configurando removeOnCompletion en NO. Si está asignando el delegado de animación a "self", la animación retendrá su instancia. Entonces, después de llamar a removeFromSuperview si olvida eliminar / destruir la animación usted mismo, no se llamará a dealloc, por ejemplo. Tendrás una pérdida de memoria.
emrahgunduz
1
Esta respuesta de más de 200 puntos es totalmente, completamente, completamente incorrecta.
Fattie
83

El problema con removeOnCompletion es que el elemento UI no permite la interacción del usuario.

La técnica es establecer el valor FROM en la animación y el valor TO en el objeto. La animación llenará automáticamente el valor TO antes de que comience, y cuando se elimine dejará el objeto en su estado correcto.

// 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"];
Leslie Godwin
fuente
77
No funciona si establece un retraso de inicio. Por ejemplo alphaAnimation.beginTime = 1; , fillMode¿no es necesario hasta donde puedo decir ...?
Sam
de lo contrario, esta es una buena respuesta, la respuesta aceptada no le permitirá cambiar un valor después de que se complete la animación ...
Sam
2
También debo tener en cuenta que beginTimeno es relativo, debe utilizar, por ejemplo:CACurrentMediaTime()+1;
Sam
Es 'es' no "es" - its-not-its.info. Lo siento si fue solo un error tipográfico.
fatuhoku
66
respuesta correcta, pero no necesaria fillMode, vea más en Evitar que las capas vuelvan a los valores originales cuando se usan
animaciones
16

Establezca la siguiente propiedad:

animationObject.removedOnCompletion = NO;
Krishnan
fuente
@Nilesh: Acepta tu respuesta. Eso le dará a otros usuarios de SO confianza en su respuesta.
Krishnan
77
Tuve que usar .fillMode = kCAFillModeForwards y .removedOnCompletion = NO. ¡Gracias!
William Rust
12

solo ponlo dentro de tu código

CAAnimationGroup *theGroup = [CAAnimationGroup animation];

theGroup.fillMode = kCAFillModeForwards;

theGroup.removedOnCompletion = NO;
msk_sureshkumar
fuente
12

Simplemente puede establecer la clave de CABasicAnimationa positioncuando la agrega a la capa. Al hacer esto, anulará la animación implícita realizada en la posición para el pase actual en el ciclo de ejecución.

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

Puede obtener más información sobre la Sesión de video Apple WWDC 2011 421: Core Animation Essentials (mitad del video)

yageek
fuente
1
Esta parece ser la forma correcta de hacer esto. Animación se elimina de forma natural (por defecto), y una vez que la animación es completa, vuelve a los valores establecidos antes del inicio de animación, específicamente esta línea: someLayer.position = endPosition;. ¡Y gracias por la referencia al WWDC!
bauerMusic
10

Un CALayer tiene una capa modelo y una capa de presentación. Durante una animación, la capa de presentación se actualiza independientemente del modelo. Cuando se completa la animación, la capa de presentación se actualiza con el valor del modelo. Si desea evitar un salto discordante después de que finalice la animación, la clave es mantener las dos capas sincronizadas.

Si conoce el valor final, puede configurar el modelo directamente.

self.view.layer.opacity = 1;

Pero si tiene una animación en la que no conoce la posición final (por ejemplo, un desvanecimiento lento que el usuario puede pausar y luego revertir), puede consultar la capa de presentación directamente para encontrar el valor actual y luego actualizar el modelo.

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

Extraer el valor de la capa de presentación también es particularmente útil para escalar o rotar rutas de teclas. (por ejemplo transform.scale, transform.rotation)

Jason Moore
fuente
8

Sin usar el removedOnCompletion

Puedes probar 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)
}
Ayman Ibrahim
fuente
3
Esta parece ser la mejor respuesta funcional
rambossa
Convenido. Tenga en cuenta el comentario de @ ayman de que debe definir la propiedad .fromValue en el valor inicial. De lo contrario, habrá sobrescrito el valor de inicio en el modelo, y habrá animación.
Jeff Collier el
Esta y la respuesta de @ jason-moore son mejores que la respuesta aceptada. En la respuesta aceptada, la animación se detiene pero el nuevo valor no se establece realmente en la propiedad. Esto puede provocar un comportamiento no deseado.
laka
8

Entonces, mi problema era que estaba tratando de rotar un objeto en un gesto panorámico y tenía múltiples animaciones idénticas en cada movimiento. Tenía ambas fillMode = kCAFillModeForwardsy isRemovedOnCompletion = falseno me ayudó. En mi caso, tuve que asegurarme de que la clave de animación es diferente cada vez que agrego una nueva animación :

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

Simplemente configurando fillModey removedOnCompletionno funcionó para mí. Resolví el problema estableciendo todas las propiedades a continuación en el 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];

Este código se transforma myViewal 85% de su tamaño (tercera dimensión sin alteraciones).

Paula Vasconcelos Gueiros
fuente
7

La animación principal mantiene dos jerarquías de capa: la capa del modelo y la capa de presentación . Cuando la animación está en progreso, la capa del modelo está realmente intacta y mantiene su valor inicial. Por defecto, la animación se elimina una vez que se completa. Luego, la capa de presentación vuelve al valor de la capa del modelo.

Simplemente establecer removedOnCompletionen NOsignifica que la animación no se eliminará y desperdicia memoria. Además, la capa del modelo y la capa de presentación ya no estarán sincronizadas, lo que puede generar posibles errores.

Por lo tanto, sería una mejor solución actualizar la propiedad directamente en la capa del modelo al 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];

Si hay alguna animación implícita causada por la primera línea del código anterior, intente desactivarla:

[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];
Lizhen Hu
fuente
¡Gracias! Esto debería ser la respuesta aceptada, ya que explica correctamente esta funcionalidad en lugar de usar una tirita para que funcione
bplattenburg
6

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

La animación principal muestra una 'capa de presentación' sobre su capa normal durante la animación. Por lo tanto, configure la opacidad (o lo que sea) a lo que desea que se vea cuando la animación finalice y la capa de presentación desaparezca. Haga esto en la línea antes de agregar la animación para evitar un parpadeo cuando se complete.

Si desea tener un retraso, haga lo siguiente:

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

Finalmente, si usa 'removeOnCompletion = false', filtrará CAAnimations hasta que la capa finalmente se elimine; evite.

Chris
fuente
Gran respuesta, buenos consejos. Gracias por eliminarOnCompletion comentario.
iWheelBuy
4

La respuesta de @Leslie Godwin no es realmente buena, "self.view.layer.opacity = 1;" se realiza de inmediato (toma alrededor de un segundo), por favor arregle alphaAnimation.duration a 10.0, si tiene dudas. Tienes que eliminar esta línea.

Entonces, cuando arreglas fillMode en kCAFillModeForwards y removeOnCompletion en NO, dejas que la animación permanezca en la capa. Si arregla el delegado de animación y prueba algo como:

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

... la capa se restaura inmediatamente en el momento en que ejecuta esta línea. Es lo que queríamos evitar.

Debe corregir la propiedad de la capa antes de quitarle la animación. Prueba esto:

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

Parece que el indicador removeOnCompletion establecido en falso y fillMode establecido en kCAFillModeForwards tampoco funciona para mí.

Después de aplicar una nueva animación en una capa, un objeto de animación se restablece a su estado inicial y luego se anima desde ese estado. Lo que debe hacerse adicionalmente es establecer la propiedad deseada de la capa del modelo de acuerdo con la propiedad de su capa de presentación antes de configurar una nueva animación de esta manera:

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

La solución más fácil es usar animaciones implícitas. Esto manejará todos esos problemas para ti:

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

Si desea personalizar, por ejemplo, la duración, puede usar NSAnimationContext:

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

Nota: Esto solo se prueba en macOS.

Inicialmente no vi ninguna animación al hacer esto. El problema es que la capa de una capa respaldada por la vista no se anima de forma implícita. Para resolver esto, asegúrese de agregar una capa usted mismo (antes de configurar la vista con respaldo de capa).

Un ejemplo de cómo hacer esto sería:

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

El uso self.wantsLayerno hizo ninguna diferencia en mis pruebas, pero podría tener algunos efectos secundarios que desconozco.

paxos
fuente
0

Aquí hay una muestra del patio de recreo:

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. Crea un modelo de animación
  2. Establecer la posición de inicio de la animación (se puede omitir, depende del diseño de la capa actual)
  3. Establecer la posición final de la animación.
  4. Establecer duración de animación
  5. Retrasa la animación por un segundo
  6. No lo configure falseen isRemovedOnCompletion: deje que Core Animation se limpie una vez que la animación haya finalizado
  7. Aquí está la primera parte del truco : le dice a Core Animation que coloque su animación en la posición inicial (que configuró en el paso 2) incluso antes de que la animación haya comenzado, extiéndala hacia atrás en el tiempo
  8. Copie el objeto de animación preparado, agréguelo a la capa e inicie la animación después del retraso (que configuró en el paso 5)
  9. La segunda parte es establecer la posición final correcta de la capa : después de eliminar la animación, la capa se mostrará en el lugar correcto
adnako
fuente