Annuler une animation UIView?

244

Est-il possible d'annuler une UIViewanimation en cours? Ou devrais-je passer au niveau CA?

c'est-à-dire que j'ai fait quelque chose comme ça (peut-être en définissant une action d'animation de fin aussi):

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationDuration:duration];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
// other animation properties

// set view properties

[UIView commitAnimations];

Mais avant que l'animation ne se termine et que j'obtienne l'événement de fin d'animation, je veux l'annuler (couper court). Est-ce possible? Googler autour trouve quelques personnes posant la même question sans réponses - et une ou deux personnes spéculant que cela ne peut pas être fait.

philsquared
la source
Voulez-vous dire suspendre l'animation au milieu et la laisser là? Ou revenir à l'endroit où il était au départ?
Chris Lundie
2
Soit en le laissant exactement là où il se trouve, à mi-animation (en pratique, je commencerai une autre animation tout de suite après), soit en sautant directement au point final. J'imagine que le premier est plus naturel, mais l'un ou l'autre fonctionne pour moi dans ce cas.
philsquared
3
@Chris Hanson: J'apprécie vos modifications, mais je me demande quelle est la justification de la suppression de la balise iPhone. Cela peut être impliqué par cacao-touch, mais pour ma part, j'ai un filtre d'étiquette sur iPhone et je manquerais cela dans ce cas.
philsquared
@Boot To The Head (encore une fois): votre question et ma propre réponse m'ont incité à tester un peu plus isolément et j'ai découvert que si vous démarrez une nouvelle animation avant la fin de la première, elle saute au point final avant de commencer le nouveau.
philsquared
- cela répond à mes besoins immédiats (et suggère que j'ai un autre bug dans mon vrai code), mais je vais laisser cette question ouverte car je sais que d'autres ont demandé la même chose.
philsquared

Réponses:

114

La façon dont je le fais est de créer une nouvelle animation à votre point final. Définissez une durée très courte et assurez-vous d'utiliser la +setAnimationBeginsFromCurrentState:méthode pour démarrer à partir de l'état actuel. Lorsque vous le définissez sur OUI, l'animation actuelle est raccourcie. Ressemble à ceci:

[UIView beginAnimations:nil context:NULL];
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView setAnimationDuration:0.1];
[UIView setAnimationCurve: UIViewAnimationCurveLinear];
// other animation properties

// set view properties

[UIView commitAnimations];
Stephen Darlington
la source
Merci Stephen. En fait, si vous lisez les commentaires sous ma question, c'est exactement ce que j'ai fait à la fin - mais puisque vous en avez donné un bon et clair compte ici, je le marque comme accepté
philsquared
1
@Vojto Comment ça? Les docs disent que l'utilisation de setAnimationBeginsFromCurrentState:est déconseillée à partir d'iOS 4, mais elle est toujours là et devrait toujours fonctionner.
Stephen Darlington
55
dans iOS4 +, utilisez l'option UIViewAnimationOptionBeginFromCurrentState sur animateWithDuration: délai: options: animations: achèvement:
Adam Eberbach
UIViewAnimationOptionBeginFromCurrentState annulera-t-il une animation en cours ou simplement une animation qui cible la même propriété? S'il s'agit d'une animation, comment pouvez-vous lui faire continuer une nouvelle animation au même point SANS arrêter la même animation en cours?
zakdances
1
@yourfriendzak, sur la base d'un bref test, il semble qu'il n'annule qu'une animation ciblant la même propriété. J'ai essayé de changer l'alpha d'un scrollView dont contentOffset était animé et l'animation contentOffset s'est poursuivie.
arlomedia
360

Utilisation:

#import <QuartzCore/QuartzCore.h>

.......

[myView.layer removeAllAnimations];
Jim Heising
la source
22
J'ai trouvé que je devais ajouter #import <QuartzCore / QuartzCore.h> pour supprimer un avertissement du compilateur;)
deanWombourne
4
Si simple et pourtant si difficile à trouver. Merci pour cela, je viens de passer une heure à essayer de comprendre cela.
MikeyWard
4
Le problème possible avec cette solution est que (dans iOS 5 au moins) il y a un délai avant qu'elle ne prenne effet - cela ne fonctionnera pas assez tôt si vous voulez dire "arrêtez l'animation maintenant, avant de passer à la ligne suivante de code".
mat du
14
J'ai trouvé que l'appel de ce paramètre déclenche l'achèvement: ^ (BOOL terminé) bloc si vous utilisez + (void) animateWithDuration: (NSTimeInterval) durée animations: (void (^) (void)) animations achèvement: (void (^) (BOOL terminé) ) achèvement
JWGS
1
@matt connais-tu une solution qui arrête tout de suite l'animation? j'observe également un petit délai avant que l'animation ne s'arrête vraiment.
tiguero
62

La manière la plus simple d'arrêter immédiatement toutes les animations sur une vue particulière est la suivante:

Reliez le projet à QuartzCore.framework. Au début de votre code:

#import <QuartzCore/QuartzCore.h>

Maintenant, lorsque vous voulez arrêter toutes les animations sur une vue morte sur leurs traces, dites ceci:

[CATransaction begin];
[theView.layer removeAllAnimations];
[CATransaction commit];

La ligne médiane fonctionnerait toute seule, mais il y a un délai jusqu'à la fin de la boucle d'exécution (le "moment de redessin"). Pour éviter ce délai, encapsulez la commande dans un bloc de transaction explicite, comme indiqué. Cela fonctionne à condition qu'aucune autre modification n'ait été effectuée sur cette couche dans la boucle d'exécution actuelle.

mat
la source
2
J'ai utilisé [CATransaction flush] après votre code qui fonctionne très bien, parfois parce qu'il n'écrit pas immédiatement. Mais cependant, il y a un problème de performances de 0,1 seconde, par exemple, il est à la traîne pour ce moment particulier. Des solutions?
Sidwyn Koh
5
removeAllAnimationsaura pour effet secondaire de ramener l'animation à sa dernière image, plutôt que de l'arrêter là où elle se trouve actuellement.
Ilya
3
Normalement, quand on veut qu'une animation s'arrête, ils s'attendent à ce qu'elle s'arrête à son image actuelle - pas la première image ni la dernière image. Passer à la première ou à la dernière image ressemblera à un mouvement saccadé.
Ilya
1
Je détaillerais ensuite la réponse pour spécifier quel est le comportement par défaut et comment il peut être modifié. Évidemment, quelqu'un qui fait de l'animation s'intéresse activement à ce qui se passe à l'écran après ses appels de méthode :)
Ilya
1
Je l'ai détaillé ailleurs. Ce n'est pas ce qui a été demandé donc ce n'est pas ce que j'ai répondu ici. Si vous avez une autre réponse, donnez-la définitivement comme réponse!
mat
48

Sur iOS 4 et supérieur, utilisez l' UIViewAnimationOptionBeginFromCurrentStateoption sur la deuxième animation pour raccourcir la première animation.

Par exemple, supposons que vous ayez une vue avec un indicateur d'activité. Vous souhaitez faire apparaître progressivement l'indicateur d'activité pendant le début d'une activité potentiellement longue et le faire disparaître lorsque l'activité est terminée. Dans le code ci-dessous, la vue avec l'indicateur d'activité est appelée activityView.

- (void)showActivityIndicator {
    activityView.alpha = 0.0;
    activityView.hidden = NO;
    [UIView animateWithDuration:0.5
                 animations:^(void) {
                     activityView.alpha = 1.0;
                 }];

- (void)hideActivityIndicator {
    [UIView animateWithDuration:0.5
                 delay:0 options:UIViewAnimationOptionBeginFromCurrentState
                 animations:^(void) {
                     activityView.alpha = 0.0;
                 }
                 completion:^(BOOL completed) {
                     if (completed) {
                         activityView.hidden = YES;
                     }
                 }];
}
Ville Laurikari
la source
41

Pour annuler une animation, il vous suffit de définir la propriété en cours d'animation, en dehors de l'animation UIView. Cela arrêtera l'animation où qu'elle soit et l'UIView passera au paramètre que vous venez de définir.

tomtaylor
la source
9
Ce n'est que partiellement vrai. Il arrête l'animation, mais il ne l'empêche pas de lancer des méthodes déléguées, notamment le gestionnaire animationComplete, donc si vous avez de la logique, vous verrez des problèmes.
M. Ryan
33
C'est à cela que -animationDidStop:finished:context:sert l' argument «fini» . Si [finished boolValue] == NO, alors vous savez que votre animation a été en quelque sorte annulée.
Nick Forge
c'est un bon conseil d'il y a 7 ans! notez qu'il y a un comportement étrange: disons que vous animez l'alpha d'avant en arrière et que vous le faites "aller à 0". pour ARRÊTER l'animation, vous ne pouvez pas mettre alpha à zéro; vous le définissez pour dire 1.
Fattie
26

Désolé de ressusciter cette réponse, mais dans iOS 10, les choses ont un peu changé, et maintenant l'annulation est possible et vous pouvez même annuler gracieusement!

Après iOS 10, vous pouvez annuler des animations avec UIViewPropertyAnimator !

UIViewPropertyAnimator(duration: 2, dampingRatio: 0.4, animations: {
    view.backgroundColor = .blue
})
animator.stopAnimation(true)

Si vous passez vrai, il annule l'animation et s'arrête là où vous l'avez annulée. La méthode d'achèvement ne sera pas appelée. Cependant, si vous passez false, vous êtes responsable de terminer l'animation:

animator.finishAnimation(.start)

Vous pouvez terminer votre animation et rester dans l'état actuel (.current) ou passer à l'état initial (.start) ou à l'état final (.end)

Au fait, vous pouvez même mettre en pause et redémarrer plus tard ...

animator.pauseAnimation()
animator.startAnimation()

Remarque: Si vous ne voulez pas une annulation brusque, vous pouvez inverser votre animation ou même changer votre animation après l'avoir mise en pause!

Tiago Almeida
la source
2
C'est certainement le plus pertinent pour les animations modernes.
Doug
10

Si vous animez une contrainte en modifiant la constante au lieu d'une propriété de vue, aucune des autres méthodes ne fonctionne sur iOS 8.

Exemple d'animation:

self.constraint.constant = 0;
[self.view updateConstraintsIfNeeded];
[self.view layoutIfNeeded];
[UIView animateWithDuration:1.0f
                      delay:0.0f
                    options:UIViewAnimationOptionCurveLinear
                 animations:^{
                     self.constraint.constant = 1.0f;
                     [self.view layoutIfNeeded];
                 } completion:^(BOOL finished) {

                 }];

Solution:

Vous devez supprimer les animations des calques de toutes les vues affectées par le changement de contrainte et leurs sous-couches.

[self.constraintView.layer removeAllAnimations];
for (CALayer *l in self.constraintView.layer.sublayers)
{
    [l removeAllAnimations];
}
Nikola Lajic
la source
1
self.constraintView.layer.removeAllAnimations()
Fonctionne
Aucune des autres solutions n'a fonctionné. Merci, cela a fonctionné pour moi.
swapnilagarwal
5

Rien de tout cela ne l'a résolu pour moi, mais cela a aidé: l' UIViewanimation définit immédiatement la propriété, puis l'anime. Il arrête l'animation lorsque la couche de présentation correspond au modèle (la propriété set).

J'ai résolu mon problème, qui était "Je veux animer à partir de l'endroit où vous avez l'air d'apparaître" ("vous" signifiant la vue). Si vous voulez CELA, alors:

  1. Ajoutez QuartzCore.
  2. CALayer * pLayer = theView.layer.presentationLayer;

définir la position sur la couche de présentation

J'utilise quelques options dont UIViewAnimationOptionOverrideInheritedDuration

Mais parce que la documentation d'Apple est vague, je ne sais pas si elle remplace vraiment les autres animations lorsqu'elles sont utilisées, ou réinitialise simplement les temporisateurs.

[UIView animateWithDuration:blah... 
                    options: UIViewAnimationOptionBeginFromCurrentState ... 
                    animations: ^ {
                                   theView.center = CGPointMake( pLayer.position.x + YOUR_ANIMATION_OFFSET, pLayer.position.y + ANOTHER_ANIMATION_OFFSET);
                   //this only works for translating, but you get the idea if you wanna flip and scale it. 
                   } completion: ^(BOOL complete) {}];

Et cela devrait être une solution décente pour l'instant.

NazCodezz
la source
5
[UIView setAnimationsEnabled:NO];
// your code here
[UIView setAnimationsEnabled:YES];
ideawu
la source
2

Version rapide de la solution de Stephen Darlington

UIView.beginAnimations(nil, context: nil)
UIView.setAnimationBeginsFromCurrentState(true)
UIView.setAnimationDuration(0.1)
// other animation properties

// set view properties
UIView.commitAnimations()
lucamegh
la source
1

J'ai le même problème; les API n'ont rien pour annuler une animation spécifique. le

+ (void)setAnimationsEnabled:(BOOL)enabled

désactive TOUTES les animations et ne fonctionne donc pas pour moi. Il y a deux solutions:

1) faites de votre objet animé une sous-vue. Ensuite, lorsque vous souhaitez annuler les animations de cette vue, supprimez la vue ou masquez-la. Très simple, mais vous devez recréer la sous-vue sans animations si vous devez la garder en vue.

2) répétez l'anim un seul, et faites un sélecteur de délégué pour redémarrer l'anim si nécessaire, comme ceci:

-(void) startAnimation {
NSLog(@"startAnim alpha:%f", self.alpha);
[self setAlpha:1.0];
[UIView beginAnimations:nil context:nil];
[UIView setAnimationDuration:1.0];
[UIView setAnimationRepeatCount:1];
[UIView setAnimationRepeatAutoreverses:YES];
[UIView setAnimationDelegate:self];
[UIView setAnimationDidStopSelector:@selector(pulseAnimationDidStop:finished:context:)];
[self setAlpha:0.1];
[UIView commitAnimations];
}

- (void)pulseAnimationDidStop:(NSString *)animationID finished:(NSNumber *)finished context:(void *)context {
if(hasFocus) {
    [self startAnimation];
} else {
    self.alpha = 1.0;
}
}

-(void) setHasFocus:(BOOL)_hasFocus {
hasFocus = _hasFocus;
if(hasFocus) {
    [self startAnimation];
}
}

Le problème avec 2) est qu'il y a toujours un délai d'arrêt de l'animation à la fin du cycle d'animation en cours.

J'espère que cela t'aides.


la source
1

Même si vous annulez l'animation de la manière ci-dessus, l'animation didStopSelectors'exécute toujours. Donc, si vous avez des états logiques dans votre application pilotés par des animations, vous aurez des problèmes. Pour cette raison, avec les méthodes décrites ci-dessus, j'utilise la variable de contexte des UIViewanimations. Si vous transmettez l'état actuel de votre programme par le paramètre de contexte à l'animation, lorsque l'animation s'arrête, votre didStopSelectorfonction peut décider si elle doit faire quelque chose ou simplement revenir en fonction de l'état actuel et de la valeur d'état transmise en tant que contexte.

Gorky
la source
NickForge l'explique parfaitement dans le commentaire ci-dessous, la bonne réponse de TomTaylor ci-dessus ...
Fattie
Merci beaucoup pour cette note! J'ai en effet une telle situation, où une prochaine animation est déclenchée lorsque animationDidStop est appelé. Si vous supprimez simplement une animation et en ajoutez immédiatement une nouvelle pour la même clé, je trouve que cela ne fonctionnera pas. Vous devez attendre, par exemple, jusqu'à ce que animationDidStop se déclenche pour une autre animation ou autre chose. L'animation qui vient d'être supprimée ne déclenchera plus animationDidStop.
RickJansen
0
CALayer * pLayer = self.layer.presentationLayer;
[UIView setAnimationBeginsFromCurrentState:YES];
[UIView animateWithDuration:0.001 animations:^{
    self.frame = pLayer.frame;
}];
Lapin tonnerre
la source
0

Pour suspendre une animation sans rétablir l'état d'origine ou final:

CFTimeInterval paused_time = [myView.layer convertTime:CACurrentMediaTime() fromLayer:nil];
myView.layer.speed = 0.0;
myView.layer.timeOffset = paused_time;
tommybananas
la source
0

Lorsque je travaille avec l' UIStackViewanimation, removeAllAnimations()je dois en outre définir certaines valeurs à l'initiale, car je removeAllAnimations()peux les définir dans un état imprévisible. J'ai stackViewavec view1et à l' view2intérieur, et une vue doit être visible et une cachée:

public func configureStackView(hideView1: Bool, hideView2: Bool) {
    let oldHideView1 = view1.isHidden
    let oldHideView2 = view2.isHidden
    view1.layer.removeAllAnimations()
    view2.layer.removeAllAnimations()
    view.layer.removeAllAnimations()
    stackView.layer.removeAllAnimations()
    // after stopping animation the values are unpredictable, so set values to old
    view1.isHidden = oldHideView1 //    <- Solution is here
    view2.isHidden = oldHideView2 //    <- Solution is here

    UIView.animate(withDuration: 0.3,
                   delay: 0.0,
                   usingSpringWithDamping: 0.9,
                   initialSpringVelocity: 1,
                   options: [],
                   animations: {
                    view1.isHidden = hideView1
                    view2.isHidden = hideView2
                    stackView.layoutIfNeeded()
    },
                   completion: nil)
}
Paul T.
la source
0

Aucune des solutions répondues n'a fonctionné pour moi. J'ai résolu mes problèmes de cette façon (je ne sais pas si c'est une bonne façon?), Car j'ai eu des problèmes lors de l'appel trop rapide (lorsque l'animation précédente n'était pas encore terminée). Je passe mon animation souhaitée avec le bloc customAnim .

extension UIView
{

    func niceCustomTranstion(
        duration: CGFloat = 0.3,
        options: UIView.AnimationOptions = .transitionCrossDissolve,
        customAnim: @escaping () -> Void
        )
    {
        UIView.transition(
            with: self,
            duration: TimeInterval(duration),
            options: options,
            animations: {
                customAnim()
        },
            completion: { (finished) in
                if !finished
                {
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    // NOTE: This fixes possible flickering ON FAST TAPPINGS
                    self.layer.removeAllAnimations()
                    customAnim()
                }
        })

    }

}
sabiland
la source