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?
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.
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:
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
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;
}
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"];
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(...)
.
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()
}
}
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
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];
}
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.
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
.