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.

4voto

Grzegorz Krukowski Points 3972

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 à partir de cette classe. Elle persiste et reprend toujours toutes les animations sur sa couche.

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

2voto

Warpling Points 191

J'ai pu restaurer l'animation (mais pas la position de l'animation) en sauvegardant une copie de l'animation actuelle et en la rajoutant lors de la reprise. J'ai appelé startAnimation au chargement et lors de l'entrée au premier plan et pause lors de l'entrée 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];
}

1voto

BTGunner Points 52

J'utilise la solution de cclogg avec beaucoup d'efficacité. Je voulais également partager quelques 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 tournent en boucle, d'autres qui ne s'exécutent qu'une fois et sont générées de manière aléatoire. La solution de cclogg a fonctionné pour moi, mais lorsque 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 lorsqu'elle était en pause. J'ai donc ajouté un drapeau (une variable membre de ma classe UIImageView personnalisée) et l'ai mis à YES dans la section où vous reprenez toutes les animations de la couche ( resumeLayer dans cclogg's, analogue à la solution QA1673 d'Apple) pour éviter que cela ne se produise. Je fais cela pour chaque UIImageView qui reprend. Ensuite, dans le fichier animationDidStop n'exécute le code de gestion de l'animation unique que lorsque cet indicateur est NO. S'il est YES, ignorez le code de gestion. Remettez le drapeau sur NO dans les deux cas. Ainsi, lorsque l'animation se termine vraiment, votre code de gestion s'exécutera. Donc, comme ceci :

- (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 aidera quelqu'un.

0voto

George_E_2 Points 622

Je reconnaissais l'état du geste comme ça :

// 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 j'avais à faire était de changer le .ended cas à .ended, .cancelled .

0voto

sash Points 1189

IOS supprime toutes les animations lorsque la vue disparaît de la zone visible (pas seulement lorsque l'application passe en arrière-plan). Pour résoudre ce problème, j'ai créé des CALayer et j'ai surchargé deux méthodes pour que le système ne supprime pas les animations. removeAnimation et removeAllAnimations :

class CustomCALayer: CALayer {

    override func removeAnimation(forKey key: String) {

        // prevent iOS to clear animation when view is not visible
    }

    override func removeAllAnimations() {

        // prevent iOS to clear animation when view is not visible
    }

    func forceRemoveAnimation(forKey key: String) {

        super.removeAnimation(forKey: key)
    }
}

Dans la vue où vous voulez que ce calque soit utilisé comme calque principal, remplacez layerClass propriété :

override class var layerClass: AnyClass {

    return CustomCALayer.self
}

Pour mettre en pause et reprendre l'animation :

extension CALayer {

    func pause() {

        guard self.isPaused() == false else {

            return
        }

        let pausedTime: CFTimeInterval = self.convertTime(CACurrentMediaTime(), from: nil)
        self.speed = 0.0
        self.timeOffset = pausedTime
    }

    func resume() {

        guard self.isPaused() else {

            return
        }
        let pausedTime: CFTimeInterval = self.timeOffset
        self.speed = 1.0
        self.timeOffset = 0.0
        self.beginTime = 0.0
        self.beginTime = self.convertTime(CACurrentMediaTime(), from: nil) - pausedTime
    }

    func isPaused() -> Bool {

        return self.speed == 0.0
    }
}

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