61 votes

Restauration de l'animation là où elle s'est arrêtée lorsque l'application reprend depuis l'arrière-plan

J'ai une boucle sans fin CABasicAnimation d'une tuile d'image répétitive à mon avis :

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

J'ai essayé de "mettre en pause et de reprendre" l'animation des couches comme décrit dans le document suivant Q&A technique QA1673 .

Lorsque l'application passe en arrière-plan, l'animation est supprimée de la couche. Pour compenser, j'écoute UIApplicationDidEnterBackgroundNotification et appeler stopAnimation et en réponse à UIApplicationWillEnterForegroundNotification appelez 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;    
}

Le problème est qu'elle recommence au début et qu'il y a un saut horrible de la dernière position, comme on peut le voir sur l'instantané de l'application que le système a pris lorsque l'application est entrée en arrière-plan, au début de la boucle d'animation.

Je n'arrive pas à trouver comment faire en sorte qu'elle commence à la dernière position, lorsque je réintroduis l'animation. Franchement, je ne comprends pas comment fonctionne ce code de QA1673 : dans resumeLayer il définit deux fois le layer.beginTime, ce qui semble redondant. Mais lorsque j'ai supprimé le premier set-to-zero, l'animation n'a pas repris là où elle avait été mise en pause. Ceci a été testé avec une simple reconnaissance de geste de tapotement, qui a fait basculer l'animation - ceci n'est pas strictement lié à mes problèmes de restauration à partir de l'arrière-plan.

Quel état dois-je retenir avant que l'animation ne soit supprimée et comment puis-je restaurer l'animation à partir de cet état, lorsque je la réintroduis plus tard ?

1 votes

Vous avez eu de la chance avec ça ? J'ai des animations qui se mettent en pause et reprennent dans ma pause de jeu. Cependant, lorsque je sors de l'arrière-plan, toutes les animations interrompues semblent être terminées. Je sais que je peux le faire en capturant l'état de la couche de présentation. Mais c'est un vrai casse-tête. Il doit y avoir un moyen plus simple !

1 votes

Pas de chance. Je garde aussi le [layer presentationLayer] comme une solution de rechange dans ma poche arrière, si le besoin de réparer cela devient critique. Comme vous le dites, cela semble être un véritable PITA.

1 votes

J'ai réussi à faire fonctionner l'Apple QA1673 et c'était génial pour mettre en pause et reprendre le jeu. Mais quel est l'intérêt si cela ne fonctionne pas pour l'arrière-plan ? Pour moi, l'intérêt du QA1673 était d'éviter d'avoir à le faire à la manière de PresentationLayer.

53voto

cclogg Points 281

J'ai rencontré le même problème dans mon jeu, et j'ai fini par trouver une solution quelque peu différente de la tienne, qui pourrait te plaire :) Je me suis dit que je devais partager la solution que j'ai trouvée...

Mon cas utilise des animations UIView/UIImageView, mais c'est toujours CAAnimations à la base... L'essentiel de ma méthode consiste à copier/stocker l'animation actuelle dans une vue, puis à laisser la fonction pause/reprise d'Apple fonctionner, mais avant de reprendre, je rajoute mon animation. Laissez-moi donc vous présenter cet exemple simple :

Disons que j'ai un UIView appelé vue mobile . Le centre de l'UIView est animé par la fonction standard [ UIView animateWithDuration... appel. En utilisant les QA1673 code, il fonctionne très bien en pause/reprise (quand on ne quitte pas l'application)... mais quoi qu'il en soit, je me suis vite rendu compte qu'à la sortie, que je fasse une pause ou non, l'animation était complètement supprimée... et me voilà dans votre situation.

Donc avec cet exemple, voici ce que j'ai fait :

  • Ayez une variable dans votre fichier d'en-tête appelée quelque chose comme animationViewPosition de type *CAAnimation**.
  • Quand l'application sort en arrière-plan, je fais ça :

    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
    • Note : Ces 2 ^ appels sont dans une méthode qui est le gestionnaire de l'appel à l'aide. Notification UIApplicationDidEnterBackgroundNotification (semblable à vous)
    • Note 2 : Si vous ne savez pas quelle est la clé (de votre animation), vous pouvez parcourir en boucle les couches de la vue ' animationKeys et les déconnecter (au milieu de l'animation probablement).
  • Maintenant, dans mon Notification UIApplicationWillEnterForegroundNotification manipulateur :

    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

Et c'est à peu près tout ! Cela a fonctionné pour moi jusqu'à présent :)

Vous pouvez facilement l'étendre à d'autres animations ou vues en répétant simplement ces étapes pour chaque animation. Cela fonctionne même pour mettre en pause ou reprendre les animations UIImageView, c'est-à-dire la fonction standard [ imageView startAnimating ]. La clé d'animation des couches pour cela (d'ailleurs) est "contents".

Listing 1 Animations Pause et Reprise.

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

3 votes

Excellente solution ! Ça a marché comme sur des roulettes. Merci.

0 votes

Génial, content que ça ait aidé quelqu'un. Je dois préciser que si vous mettez en pause une vue et sa sous-vue (par exemple, une vue conteneur qui change de position, avec une vue image à l'intérieur qui s'anime), vous devez copier et réajuster les DEUX animations, mais vous devez seulement mettre en pause/reprendre (en utilisant QA1673) la vue principale... et cela s'appliquera à toutes les sous-vues... si cela a un sens.

0 votes

Cela m'a vraiment aidé, et cela fonctionne même pour CAAnimationGroups (étant donné qu'il n'y a pas de 'animationGroupForKey', mais seulement 'animationForKey').

18voto

Max MacLeod Points 8425

Après de nombreuses recherches et discussions avec les gourous du développement d'iOS, il apparaît que QA1673 n'aide pas lorsqu'il s'agit de faire une pause, de passer en arrière-plan, puis de passer au premier plan. Mes expériences montrent même que les méthodes de délégués qui sont déclenchées par des animations, telles que animationDidStop deviennent peu fiables.

Parfois ils tirent, parfois non.

Cela crée de nombreux problèmes car cela signifie que, non seulement vous regardez un écran différent de celui que vous aviez lorsque vous avez fait une pause, mais aussi que la séquence d'événements en cours peut être interrompue.

Jusqu'à présent, ma solution a été la suivante :

Lorsque l'animation démarre, je reçois l'heure de début :

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

Lorsque l'utilisateur appuie sur le bouton pause, je supprime l'animation de l'élément CALayer :

[layer removeAnimationForKey:key];

J'obtiens le temps absolu en utilisant CACurrentMediaTime() :

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

Utilisation de la mStartTime et stopTime Je calcule un temps de décalage :

mTimeOffset = stopTime - mStartTime;

J'ai également défini les valeurs du modèle de l'objet pour qu'elles soient celles de l'objet de l'UE. presentationLayer . Donc, mon stop ressemble à ceci :

//--------------------------------------------------------------------------------------------------

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

Au moment de la reprise, je recalcule ce qu'il reste de l'animation en pause en fonction de la valeur de l'image. mTimeOffset . C'est un peu confus parce que j'utilise CAKeyframeAnimation . Je détermine quelles images clés sont en suspens en me basant sur les données suivantes mTimeOffset . De plus, je prends en compte le fait que la pause peut avoir eu lieu au milieu de l'image, par exemple à mi-chemin entre f1 et f2 . Ce temps est déduit du temps de cette image clé.

J'ajoute ensuite cette animation au calque à nouveau :

[layer addAnimation:animationGroup forKey:key];

L'autre chose à retenir est que vous devrez vérifier le drapeau en animationDidStop et ne retirer le calque animé du parent qu'avec removeFromSuperlayer si le drapeau est YES . Cela signifie que la couche est toujours visible pendant la pause.

Cette méthode semble en effet très laborieuse. Mais elle fonctionne ! J'aimerais bien pouvoir le faire simplement en utilisant QA1673 . Mais pour l'instant, pour l'arrière-plan, cela ne fonctionne pas et cela semble être la seule solution.

0 votes

Je suis curieux de savoir comment vous avez procédé au recalcul pour reprendre l'animation. En supposant que l'animation soit linéaire, je pense que vous pouvez simplement calculer la distance entre votre offset et le point final et c'est tout.

14voto

Matej Bukovinski Points 3519

Il est surprenant de voir que ce n'est pas plus simple. J'ai créé une catégorie, en me basant sur l'approche de cclogg, qui devrait rendre cette opération plus simple.

CALayer+MBAnimationPersistance

Il suffit d'invoquer MB_setCurrentAnimationsPersistent sur votre couche après avoir configuré les animations souhaitées.

[movingView.layer MB_setCurrentAnimationsPersistent];

Ou spécifier les animations qui doivent être persistées explicitement.

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

1 votes

Cela fonctionne très bien ! J'ai dû ajouter #import <UIKit/UIKit.h> dans le fichier .m pour qu'il soit construit.

0 votes

Il y a quelques problèmes ici. Si vous mettez les animations en pause depuis l'extérieur de cette classe et que vous définissez layer.speed = 0.0, alors [layer animationForKey:key] ne renverra aucune animation puisqu'elles ne sont pas présentes. La solution consiste à définir la vitesse à 1.0 pendant que vous persistez les animations, et à revenir à la valeur précédente juste après. De même, votre code force la reprise à la vitesse = 1.0 au lieu de stocker la vitesse précédente lorsqu'il revient de l'arrière-plan - il appelle également la pause même si la couche est déjà en pause, ce qui entraîne un "saut" dans l'animation. Juste si vous voulez améliorer :) Vérifiez la réponse pour la logique qui gère tout.

0 votes

Je viens d'obtenir quelques plantages signalés à la toute dernière ligne de l'instruction MBPersistentAnimationContainer initWithLayer méthode. Les plantages ont été signalés sur iOS 11.3.1. L'un des appareils avait 4 % de RAM libre, et l'autre 13 %. Je me demande si c'était juste une situation de faible RAM.

9voto

Pepijn Points 184

Je ne peux pas commenter, alors je vais l'ajouter comme réponse.

J'ai utilisé la solution de cclogg mais mon application se plantait lorsque la vue de l'animation était retirée de sa vue supérieure, ajoutée à nouveau, puis mise en arrière-plan.

L'animation a été rendue infinie en fixant animation.repeatCount à Float.infinity .
La solution que j'ai trouvée est de définir animation.removedOnCompletion à false .

C'est très bizarre que cela fonctionne car l'animation n'est jamais terminée. Si quelqu'un a une explication, j'aimerais l'entendre.

Un autre conseil : si vous retirez la vue de sa vue supérieure. N'oubliez pas de retirer l'observateur en appelant NSNotificationCenter.defaultCenter().removeObserver(...) .

6voto

ArtFeel Points 3750

J'écris un Swift 4.2 extension de la version basée sur les réponses de @cclogg et @Matej Bukovinski. Il suffit d'appeler layer.makeAnimationsPersistent()

L'essentiel ici : CALayer+AnimationPlayback.swift, CALayer+PersistentAnimations.swift

Partie centrale :

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 = UIApplication.willEnterForegroundNotification
        let enterBackground = UIApplication.didEnterBackgroundNotification
        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 restoreAnimations(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 }
        restoreAnimations(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 // restore original speed
        layer.pauseAnimations()
    }
}

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X