web-dev-qa-db-fra.com

Restauration de l'animation là où elle s'était arrêtée lorsque l'application reprend à 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 "suspendre et reprendre" l'animation de la couche comme décrit dans QA technique QA167 .

Lorsque l'application entre en arrière-plan, l'animation est supprimée du calque. Pour compenser, j'écoute UIApplicationDidEnterBackgroundNotification et j'appelle stopAnimation et en réponse à UIApplicationWillEnterForegroundNotification j'appelle 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'il recommence au début et qu'il y a un laid saut depuis la dernière position, comme le montre l'instantané de l'application que le système a pris lorsque l'application est entrée en arrière-plan, jusqu'au début de la boucle d'animation.

Je n'arrive pas à comprendre comment le faire démarrer à la dernière position, lorsque je rajoute l'animation. Franchement, je ne comprends tout simplement pas comment ce code de QA1673 fonctionne: dans resumeLayer il définit le layer.beginTime deux fois, ce qui semble redondant. Mais quand j'ai supprimé le premier mis à zéro, il n'a pas repris l'animation là où elle a été interrompue. Cela a été testé avec un simple reconnaisseur de gestes de touche, qui a fait basculer l'animation - ce n'est pas strictement lié à mes problèmes de restauration à partir de l'arrière-plan.

Quel état dois-je me rappeler avant de supprimer l'animation et comment restaurer l'animation à partir de cet état, lorsque je l'ajoute plus tard?

56
Palimondo

Après pas mal de recherches et de discussions avec les gourous du développement iOS, il semble que QA167 n'aide pas quand il s'agit de faire une pause, d'arrière-plan, puis de passer au premier plan. Mon expérimentation montre même que les méthodes déléguées qui se déclenchent à partir des animations, telles que animationDidStop deviennent peu fiables.

Parfois, ils tirent, parfois non.

Cela crée beaucoup de problèmes car cela signifie que non seulement vous regardez un écran différent que vous étiez lorsque vous vous êtes arrêté, mais aussi la séquence des événements actuellement en mouvement peut être perturbée.

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

Lorsque l'animation démarre, j'obtiens l'heure de début:

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

Lorsque l'utilisateur clique sur le bouton pause, je supprime l'animation du CALayer:

[layer removeAnimationForKey:key];

J'obtiens le temps absolu en utilisantCACurrentMediaTime():

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

En utilisant mStartTime et stopTime, je calcule un temps de décalage:

mTimeOffset = stopTime - mStartTime;

J'ai également défini les valeurs de modèle de l'objet comme celles de presentationLayer. Donc, ma méthode 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;
}

À la reprise, je recalcule ce qui reste de l'animation interrompue en fonction du mTimeOffset. C'est un peu compliqué parce que j'utilise CAKeyframeAnimation. Je détermine quelles images clés sont en suspens sur la base du mTimeOffset. De plus, je prends en compte que la pause peut avoir eu lieu au milieu du cadre, par ex. à mi-chemin entre f1 et f2. Ce temps est déduit du temps de cette image clé.

J'ajoute ensuite cette animation au calque:

[layer addAnimation:animationGroup forKey:key];

L'autre chose à retenir est que vous devrez vérifier l'indicateur dans animationDidStop et supprimer uniquement le calque animé du parent avec removeFromSuperlayer si l'indicateur est YES. Cela signifie que le calque est toujours visible pendant la pause.

Cette méthode semble très laborieuse. Ça marche quand même! J'adorerais pouvoir simplement faire cela en utilisant QA167 . Mais pour le moment, cela ne fonctionne pas et cela semble être la seule solution.

17
Max MacLeod

Hé, je suis tombé sur la même chose dans mon jeu et j'ai fini par trouver une solution quelque peu différente de la vôtre, que vous aimerez peut-être :) J'ai pensé que je devrais partager la solution de contournement que j'ai trouvée ...

Mon cas utilise des animations UIView/UIImageView, mais c'est fondamentalement toujours CAAnimations à sa base ... L'essentiel de ma méthode est que je copie/stocke l'animation actuelle sur une vue, puis laisse la pause/reprise d'Apple fonctionner toujours, mais avant en reprenant j'ajoute mon animation. Alors laissez-moi vous présenter cet exemple simple:

Disons que j'ai une UIView appelée movingView . Le centre de l'UIView est animé via l'appel standard [ UIView animateWithDuration ... ]. En utilisant le code mentionné QA167, cela fonctionne très bien en pause/reprise (lorsque vous ne quittez pas l'application) ... mais quoi qu'il en soit, je me suis vite rendu compte qu'à la sortie, que je m'arrête ou non, l'animation était complètement supprimée ... et j'étais ici à votre place.

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 **.
  • Lorsque l'application sort en arrière-plan, je fais ceci:

    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
    
    • Remarque: Ces 2 ^ appels se trouvent dans une méthode qui est le gestionnaire de UIApplicationDidEnterBackgroundNotification (similaire à vous)
    • Remarque 2: si vous ne savez pas quelle est la clé (de votre animation), vous pouvez parcourir la propriété et le journal ' animationKeys ' du calque de la vue ceux-là (animation moyenne vraisemblablement).
  • Maintenant dans mon gestionnaire 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
    

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

Vous pouvez facilement l'étendre pour plus d'animations ou de vues en répétant simplement ces étapes pour chaque animation. Il fonctionne même pour mettre en pause/reprendre les animations UIImageView, c'est-à-dire le standard [ imageView startAnimating ]. La clé d'animation de couche pour cela (soit dit en passant) est "contenu".

Liste 1 animations de pause et de 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;
}
50
cclogg

Il est surprenant de voir que ce n'est pas plus simple. J'ai créé une catégorie, basée sur l'approche de cclogg, qui devrait en faire un one-liner.

CALayer + MBAnimationPersistence

Appelez simplement MB_setCurrentAnimationsPersistent sur votre calque après avoir configuré les animations souhaitées.

[movingView.layer MB_setCurrentAnimationsPersistent];

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

movingView.layer.MB_persistentAnimationKeys = @[@"position"];
14
Matej Bukovinski

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

J'ai utilisé la solution de cclogg mais mon application se bloquait lorsque la vue de l'animation a été supprimée de sa vue d'ensemble, ajoutée à nouveau, puis en arrière-plan.

L'animation a été rendue infinie en définissant animation.repeatCount Sur Float.infinity.
La solution que j'avais était de régler animation.removedOnCompletion Sur false.

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

Autre astuce: si vous supprimez la vue de sa vue d'ensemble. N'oubliez pas de supprimer l'observateur en appelant NSNotificationCenter.defaultCenter().removeObserver(...).

7
Pepijn

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

Gist complet 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()
    }
}
4
ArtFeel

Juste au cas où quelqu'un aurait besoin d'une solution Swift 3 pour ce problème:

Tout ce que vous avez à faire est de sous-classer votre vue animée de cette classe. Il persiste toujours et reprend toutes les animations sur son calque.

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.restoreAnimations(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 //restore 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 restoreAnimations(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
    }
}

Sur Gist: https://Gist.github.com/grzegorzkrukowski/a5ed8b38bec548f9620bb95665c06128

3

J'ai pu restaurer l'animation (mais pas la position de l'animation) en enregistrant une copie de l'animation actuelle et en l'ajoutant à la reprise. J'ai appelé startAnimation à la charge et en entrant au premier plan et en pause lors de la saisie en arrière-plan.

- (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/restored
    self.myAnimation = [[self.layer animationForKey:@"myAnimation"] copy];
}
2
Warpling

J'utilise la solution de cclogg à bon escient. Je voulais également partager des informations supplémentaires qui pourraient aider quelqu'un d'autre, car cela m'a frustré pendant un certain temps.

Dans mon application, j'ai un certain nombre d'animations, certaines qui bouclent pour toujours, certaines qui ne s'exécutent qu'une seule fois et sont générées de manière aléatoire. La solution de cclogg a fonctionné pour moi, mais quand j'ai ajouté du code à

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

afin de faire quelque chose lorsque seules les animations ponctuelles étaient terminées, ce code se déclencherait lorsque je reprendrais mon application (en utilisant la solution de cclogg) chaque fois que ces animations ponctuelles spécifiques étaient en cours d'exécution lors de sa pause. J'ai donc ajouté un indicateur (une variable membre de ma classe UIImageView personnalisée) et je l'ai défini sur YES dans la section où vous reprenez toutes les animations de couche (resumeLayer dans cclogg, analogue à la solution Apple QA1673) pour empêcher cela de se produire. Je le fais pour chaque UIImageView qui reprend. Ensuite, dans la méthode animationDidStop, exécutez uniquement le code de gestion d'animation unique lorsque cet indicateur est NO. Si c'est OUI, ignorez le code de gestion. Basculez le drapeau sur NON dans les deux cas. De cette façon, lorsque l'animation se termine vraiment, votre code de gestion s'exécute. Alors comme ça:

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

J'espère que cela aide quelqu'un.

1
BTGunner

Je reconnaissais l'état du geste comme ceci:

// Perform action depending on the state
switch gesture.state {
case .changed:
    // Some action
case .ended:
    // Another action

// Ignore any other state
default:
    break
}

Tout ce que je devais faire était de changer le .ended cas à .ended, .cancelled.

0
George_E