Restauración de la animación donde se quedó cuando la aplicación se reanuda desde el fondo

Tengo una CABasicAnimation de bucle CABasicAnimation de un mosaico de imagen repetitiva en mi opinión:

 a = [CABasicAnimation animationWithKeyPath:@"position"]; a.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]; a.fromValue = [NSValue valueWithCGPoint:CGPointMake(0, 0)]; a.toValue = [NSValue valueWithCGPoint:CGPointMake(image.size.width, 0)]; a.repeatCount = HUGE_VALF; a.duration = 15.0; [a retain]; 

Intenté “pausar y reanudar” la animación de la capa como se describe en la sección de preguntas y respuestas técnicas QA1673 .

Cuando la aplicación entra en segundo plano, la animación se elimina de la capa. Para compensar, escucho UIApplicationDidEnterBackgroundNotification y llamo stopAnimation y, en respuesta a UIApplicationWillEnterForegroundNotification llamo startAnimation .

 - (void)startAnimation { if ([[self.layer animationKeys] count] == 0) [self.layer addAnimation:a forKey:@"position"]; CFTimeInterval pausedTime = [self.layer timeOffset]; self.layer.speed = 1.0; self.layer.timeOffset = 0.0; self.layer.beginTime = 0.0; CFTimeInterval timeSincePause = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; self.layer.beginTime = timeSincePause; } - (void)stopAnimation { CFTimeInterval pausedTime = [self.layer convertTime:CACurrentMediaTime() fromLayer:nil]; self.layer.speed = 0.0; self.layer.timeOffset = pausedTime; } 

El problema es que comienza de nuevo al principio y hay un salto feo desde la última posición, como se ve en la instantánea de la aplicación que el sistema tomó cuando la aplicación ingresó en el fondo, de vuelta al inicio del ciclo de animación.

No puedo imaginar cómo hacer que comience en la última posición, cuando vuelvo a agregar la animación. Francamente, simplemente no entiendo cómo funciona el código de QA1673: en resumeLayer establece el layer.beginTime dos veces, lo que parece redundante. Pero cuando eliminé el primer set-to-zero, no reanudó la animación donde estaba en pausa. Esto se probó con un simple reconocedor de gestos de pulsación, que alternó la animación; esto no está estrictamente relacionado con mis problemas con la restauración desde el fondo.

¿Qué estado debo recordar antes de que se elimine la animación y cómo restaurar la animación desde ese estado, cuando la vuelva a agregar más tarde?

    Después de una gran cantidad de búsquedas y conversaciones con los gurús de desarrollo de iOS, parece que QA1673 no ayuda cuando se trata de pausar, crear fondo y luego pasar al primer plano. Mi experimentación incluso muestra que los métodos delegates que se activan a partir de animaciones, como animationDidStop no son confiables.

    A veces disparan, a veces no lo hacen.

    Esto crea muchos problemas porque significa que no solo está mirando una pantalla diferente a la que tenía cuando hizo una pausa, sino que también puede interrumpirse la secuencia de eventos actualmente en movimiento.

    Mi solución hasta ahora ha sido la siguiente:

    Cuando comienza la animación, obtengo la hora de inicio:

    mStartTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

    Cuando el usuario pulsa el botón de pausa, CALayer la animación de CALayer :

    [layer removeAnimationForKey:key];

    Obtengo el tiempo absoluto usando CACurrentMediaTime() :

    CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil];

    Usando mStartTime y stopTime , calculo un tiempo de compensación:

     mTimeOffset = stopTime - mStartTime; 

    También establecí los valores de modelo del objeto para que sean los de presentationLayer . Por lo tanto, mi método de stop se ve así:

     //-------------------------------------------------------------------------------------------------- - (void)stop { const CALayer *presentationLayer = layer.presentationLayer; layer.bounds = presentationLayer.bounds; layer.opacity = presentationLayer.opacity; layer.contentsRect = presentationLayer.contentsRect; layer.position = presentationLayer.position; [layer removeAnimationForKey:key]; CFTimeInterval stopTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; mTimeOffset = stopTime - mStartTime; } 

    En el currículum vitae, recalculo lo que queda de la animación pausada basada en mTimeOffset . Eso es un poco complicado porque estoy usando CAKeyframeAnimation . mTimeOffset qué fotogtwigs clave están pendientes en función de mTimeOffset . Además, tomo en cuenta que la pausa puede haber ocurrido a mitad del cuadro, por ejemplo, a medio camino entre f1 y f2 . Ese tiempo se deduce del momento de ese fotogtwig clave.

    Luego agrego esta animación a la capa de nuevo:

    [layer addAnimation:animationGroup forKey:key];

    Otra cosa que debes recordar es que deberás verificar el indicador en animationDidStop y solo eliminar la capa animada del elemento primario con removeFromSuperlayer si el indicador es YES . Eso significa que la capa aún es visible durante la pausa.

    Este método parece muy laborioso. ¡Sin embargo funciona! Me encantaría poder hacer esto simplemente usando QA1673 . Pero en este momento para el fondo, no funciona y esta parece ser la única solución.

    Oye, me encontré con lo mismo en mi juego, y terminé encontrando una solución algo diferente a la tuya, que te puede gustar 🙂 Pensé que debería compartir la solución alternativa que encontré …

    Mi caso es el uso de animaciones UIView / UIImageView, pero básicamente sigue siendo CAAnimations en su núcleo … La esencia de mi método es copiar / almacenar la animación actual en una vista, y luego dejar que la pausa / reanudación de Apple funcione, pero antes reanudando vuelvo a agregar mi animación. Entonces permítanme presentarles este simple ejemplo:

    Digamos que tengo una UIView llamada movingView . El centro de UIView está animado a través de la llamada estándar [ UIView animateWithDuration … ]. Usando el código QA1673 mencionado, funciona una gran pausa / reanudación (cuando no salga de la aplicación) … pero independientemente, pronto me di cuenta de que al salir, ya sea que pause o no, la animación fue eliminada por completo … y aquí estaba yo en tu posición

    Entonces con este ejemplo, esto es lo que hice:

    • Tenga una variable en su archivo de encabezado llamada algo así como animationViewPosition , de tipo * CAAnimation **.
    • Cuando la aplicación sale a segundo plano, hago esto:

       animationViewPosition = [[movingView.layer animationForKey:@"position"] copy]; // I know position is the key in this case... [self pauseLayer:movingView.layer]; // this is the Apple method from QA1673 
      • Nota: Esas 2 ^ llamadas están en un método que es el manejador de UIApplicationDidEnterBackgroundNotification (similar a usted)
      • Nota 2: si no sabe cuál es la clave (de su animación), puede recorrer la propiedad ‘ animationKeys ‘ de la capa de la vista y desconectarlas (presumiblemente a mitad de la animación).
    • Ahora en mi controlador UIApplicationWillEnterForegroundNotification :

       if (animationViewPosition != nil) { [movingView.layer addAnimation:animationViewPosition forKey:@"position"]; // re-add the core animation to the view [animationViewPosition release]; // since we 'copied' earlier animationViewPosition = nil; } [self resumeLayer:movingView.layer]; // Apple's method, which will resume the animation at the position it was at when the app exited 

    ¡Y eso es más o menos! Hasta ahora me ha funcionado 🙂

    Puede ampliarlo fácilmente para obtener más animaciones o vistas simplemente repitiendo esos pasos para cada animación. Incluso funciona para pausar / reanudar animaciones UIImageView, es decir, el estándar [ imageView startAnimating ]. La clave de animación de capa para eso (por cierto) es “contenido”.

    Listado 1 Pausa y reanudar animaciones.

     -(void)pauseLayer:(CALayer*)layer { CFTimeInterval pausedTime = [layer convertTime:CACurrentMediaTime() fromLayer:nil]; layer.speed = 0.0; layer.timeOffset = pausedTime; } -(void)resumeLayer:(CALayer*)layer { CFTimeInterval pausedTime = [layer timeOffset]; layer.speed = 1.0; layer.timeOffset = 0.0; layer.beginTime = 0.0; CFTimeInterval timeSincePause = [layer convertTime:CACurrentMediaTime() fromLayer:nil] - pausedTime; layer.beginTime = timeSincePause; } 

    Es sorprendente ver que esto no es más sencillo. Creé una categoría, basada en el enfoque de cclogg, que debería hacer que esto sea de una sola línea.

    CALayer + MBAnimationPersistence

    Simplemente invoque MB_setCurrentAnimationsPersistent en su capa después de configurar las animaciones deseadas.

     [movingView.layer MB_setCurrentAnimationsPersistent]; 

    O especifique las animaciones que deben persistir explícitamente.

     movingView.layer.MB_persistentAnimationKeys = @[@"position"]; 

    No puedo comentar, así que lo agregaré como respuesta.

    Utilicé la solución de cclogg, pero mi aplicación se bloqueó cuando la vista de la animación se eliminó de su supervista, se volvió a agregar y luego a fondo.

    La animación se hizo infinita al establecer animation.repeatCount en Float.infinity .
    La solución que tenía era establecer animation.removedOnCompletion en false .

    Es muy extraño que funcione porque la animación nunca se completa. Si alguien tiene una explicación, me gusta escucharla.

    Otro consejo: si elimina la vista de su supervista. No se olvide de eliminar al observador llamando a NSNotificationCenter.defaultCenter().removeObserver(...) .

    En caso de que alguien necesite una solución Swift 3 para este problema:

    Todo lo que tienes que hacer es crear una subclase de tu vista animada de esta clase. Siempre persiste y reanuda todas las animaciones en su capa.

     class ViewWithPersistentAnimations : UIView { private var persistentAnimations: [String: CAAnimation] = [:] private var persistentSpeed: Float = 0.0 override init(frame: CGRect) { super.init(frame: frame) self.commonInit() } required init?(coder aDecoder: NSCoder) { super.init(coder: aDecoder) self.commonInit() } func commonInit() { NotificationCenter.default.addObserver(self, selector: #selector(didBecomeActive), name: NSNotification.Name.UIApplicationWillEnterForeground, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willResignActive), name: NSNotification.Name.UIApplicationDidEnterBackground, object: nil) } deinit { NotificationCenter.default.removeObserver(self) } func didBecomeActive() { self.restreAnimations(withKeys: Array(self.persistentAnimations.keys)) self.persistentAnimations.removeAll() if self.persistentSpeed == 1.0 { //if layer was plaiyng before backgorund, resume it self.layer.resume() } } func willResignActive() { self.persistentSpeed = self.layer.speed self.layer.speed = 1.0 //in case layer was paused from outside, set speed to 1.0 to get all animations self.persistAnimations(withKeys: self.layer.animationKeys()) self.layer.speed = self.persistentSpeed //restre original speed self.layer.pause() } func persistAnimations(withKeys: [String]?) { withKeys?.forEach({ (key) in if let animation = self.layer.animation(forKey: key) { self.persistentAnimations[key] = animation } }) } func restreAnimations(withKeys: [String]?) { withKeys?.forEach { key in if let persistentAnimation = self.persistentAnimations[key] { self.layer.add(persistentAnimation, forKey: key) } } } } extension CALayer { func pause() { if self.isPaused() == false { let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) self.speed = 0.0 self.timeOffset = pausedTime } } func isPaused() -> Bool { return self.speed == 0.0 } func resume() { let pausedTime: CFTimeInterval = self.timeOffset self.speed = 1.0 self.timeOffset = 0.0 self.beginTime = 0.0 let timeSincePause: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime self.beginTime = timeSincePause } } 

    En Gist: https://gist.github.com/grzegorzkrukowski/a5ed8b38bec548f9620bb95665c06128

    Pude restaurar la animación (pero no la posición de la animación) guardando una copia de la animación actual y volviéndola a agregar en la hoja de vida. Llamé a StartAnimation con carga y al ingresar al primer plano y pause al ingresar el fondo.

     - (void) startAnimation { // On first call, setup our ivar if (!self.myAnimation) { self.myAnimation = [CABasicAnimation animationWithKeyPath:@"transform"]; /* Finish setting up myAnimation */ } // Add the animation to the layer if it hasn't been or got removed if (![self.layer animationForKey:@"myAnimation"]) { [self.layer addAnimation:self.spinAnimation forKey:@"myAnimation"]; } } - (void) pauseAnimation { // Save the current state of the animation // when we call startAnimation again, this saved animation will be added/restred self.myAnimation = [[self.layer animationForKey:@"myAnimation"] copy]; } 

    Uso la solución de cclogg con gran efecto. También quería compartir información adicional que pudiera ayudar a otra persona, porque me frustraba por un tiempo.

    En mi aplicación, tengo una serie de animaciones, algunas que se repiten para siempre, otras que se ejecutan solo una vez y se engendran aleatoriamente. La solución de cclogg funcionó para mí, pero cuando agregué un código para

     - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag 

    para hacer algo cuando solo se terminaban las animaciones de un solo uso, este código se activaba cuando reanudaba mi aplicación (usando la solución de cclogg) siempre que esas animaciones específicas de un tiempo se ejecutaban cuando estaba en pausa. Así que agregué un indicador (una variable miembro de mi clase UIImageView personalizada) y lo configuré en SÍ en la sección donde se reanudan todas las animaciones de capa ( resumeLayer en cclogg, análoga a la solución Apple QA1673) para evitar que esto suceda. Lo hago por cada UIImageView que se está reanudando. Luego, en el método animationDidStop , solo ejecute el código de manejo de la animación por única vez cuando ese indicador sea NO. Si es SÍ, ignore el código de manejo. Cambia de nuevo la bandera a NO. De esta forma, cuando la animación realmente finalice, se ejecutará su código de manejo. Así que así:

     - (void)animationDidStop:(CAAnimation *)theAnimation finished:(BOOL)flag if (!resumeFlag) { // do something now that the animation is finished for reals } resumeFlag = NO; } 

    Espero que ayude a alguien.

    Escribo una extensión de la versión Swift 4 basada en las respuestas @cclogg y @Matej Bukovinski. Todo lo que necesitas es llamar a layer.makeAnimationsPersistent()

    GI completo aquí: CALayer + AnimationPlayback.swift, CALayer + PersistentAnimations.swift

    Parte central:

     public extension CALayer { static private var persistentHelperKey = "CALayer.LayerPersistentHelper" public func makeAnimationsPersistent() { var object = objc_getAssociatedObject(self, &CALayer.persistentHelperKey) if object == nil { object = LayerPersistentHelper(with: self) let nonatomic = objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN_NONATOMIC objc_setAssociatedObject(self, &CALayer.persistentHelperKey, object, nonatomic) } } } public class LayerPersistentHelper { private var persistentAnimations: [String: CAAnimation] = [:] private var persistentSpeed: Float = 0.0 private weak var layer: CALayer? public init(with layer: CALayer) { self.layer = layer addNotificationObservers() } deinit { removeNotificationObservers() } } private extension LayerPersistentHelper { func addNotificationObservers() { let center = NotificationCenter.default let enterForeground = NSNotification.Name.UIApplicationWillEnterForeground let enterBackground = NSNotification.Name.UIApplicationDidEnterBackground center.addObserver(self, selector: #selector(didBecomeActive), name: enterForeground, object: nil) center.addObserver(self, selector: #selector(willResignActive), name: enterBackground, object: nil) } func removeNotificationObservers() { NotificationCenter.default.removeObserver(self) } func persistAnimations(with keys: [String]?) { guard let layer = self.layer else { return } keys?.forEach { (key) in if let animation = layer.animation(forKey: key) { persistentAnimations[key] = animation } } } func restreAnimations(with keys: [String]?) { guard let layer = self.layer else { return } keys?.forEach { (key) in if let animation = persistentAnimations[key] { layer.add(animation, forKey: key) } } } } @objc extension LayerPersistentHelper { func didBecomeActive() { guard let layer = self.layer else { return } restreAnimations(with: Array(persistentAnimations.keys)) persistentAnimations.removeAll() if persistentSpeed == 1.0 { // if layer was playing before background, resume it layer.resumeAnimations() } } func willResignActive() { guard let layer = self.layer else { return } persistentSpeed = layer.speed layer.speed = 1.0 // in case layer was paused from outside, set speed to 1.0 to get all animations persistAnimations(with: layer.animationKeys()) layer.speed = persistentSpeed // restre original speed layer.pauseAnimations() } }