Désactivation des animations implicites dans - [CALayer setNeedsDisplayInRect:]

137

J'ai une couche avec du code de dessin complexe dans sa méthode -drawInContext :. J'essaie de minimiser la quantité de dessin que je dois faire, donc j'utilise -setNeedsDisplayInRect: pour mettre à jour uniquement les pièces modifiées. Cela fonctionne à merveille. Cependant, lorsque le système graphique met à jour mon calque, il passe de l'ancienne à la nouvelle image en utilisant un fondu enchaîné. J'aimerais que cela change instantanément.

J'ai essayé d'utiliser CATransaction pour désactiver les actions et définir la durée sur zéro, et aucun des deux ne fonctionne. Voici le code que j'utilise:

[CATransaction begin];
[CATransaction setDisableActions: YES];
[self setNeedsDisplayInRect: rect];
[CATransaction commit];

Y a-t-il une méthode différente sur CATransaction que je devrais utiliser à la place (j'ai également essayé -setValue: forKey: avec kCATransactionDisableActions, même résultat).

Ben Gottlieb
la source
vous pouvez le faire dans la prochaine boucle de course: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
Hashem Aboonajmi
1
J'ai trouvé de nombreuses réponses ci-dessous pour travailler pour moi. Le document Changer le comportement par défaut d'un calque d' Apple est également utile , qui décrit en détail le processus de décision d'action implicite.
ɲeuroburɳ
Ceci est une question en double pour celle-ci: stackoverflow.com/a/54656717/5067402
Ryan Francesconi

Réponses:

172

Vous pouvez le faire en définissant le dictionnaire d'actions sur le calque pour qu'il renvoie [NSNull null]une animation pour la clé appropriée. Par exemple, j'utilise

NSDictionary *newActions = @{
    @"onOrderIn": [NSNull null],
    @"onOrderOut": [NSNull null],
    @"sublayers": [NSNull null],
    @"contents": [NSNull null],
    @"bounds": [NSNull null]
};

layer.actions = newActions;

pour désactiver les animations de fondu entrant / sortant lors de l'insertion ou du changement de sous-couches dans l'un de mes calques, ainsi que des changements dans la taille et le contenu du calque. Je pense que la contentsclé est celle que vous recherchez pour éviter le fondu enchaîné sur le dessin mis à jour.


Version Swift:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]
Brad Larson
la source
24
Pour éviter tout mouvement lors du changement de cadre, utilisez la @"position"clé.
mxcl
11
Assurez-vous également d'ajouter la @"hidden"propriété dans le dictionnaire d'actions si vous basculez la visibilité d'un calque de cette manière et souhaitez désactiver l'animation d'opacité.
Andrew
1
@BradLarson qui est la même idée que je suis venu avec après quelques prises (i l' emportaient sur actionForKey:place), découvrir fontSize, contents, onLayoutet bounds. Il semble que vous puissiez spécifier n'importe quelle clé que vous pourriez utiliser dans la setValue:forKey:méthode, en spécifiant en fait des chemins de clés complexes comme bounds.size.
pqnet
11
Il existe en fait des constantes pour ces chaînes 'spéciales' ne représentant pas une propriété (par exemple kCAOnOrderOut pour @ "onOrderOut") bien documentées ici: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Patrick Pijnappel
1
@Benjohn Seules les clés qui n'ont pas de propriété correspondante ont des constantes définies. BTW, le lien semble mort, voici la nouvelle URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Patrick Pijnappel
89

Aussi:

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];

//foo

[CATransaction commit];
mxcl
la source
3
Vous pouvez remplacer //foopar [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];pour répondre à la question d'origine.
Karoy Lorentey
1
Merci! Cela me permet également de définir un indicateur animé sur ma vue personnalisée. Pratique pour une utilisation dans une cellule de vue de tableau (où la réutilisation de cellule peut conduire à des animations trippantes lors du défilement).
Joe D'Andrea le
3
Mène à des problèmes de performances pour moi, définir des actions est plus performant
Pascalius
26
Sténographie:[CATransaction setDisableActions:YES]
titaniumdecoy
7
Ajouter au commentaire @titaniumdecoy, juste au cas où quelqu'un serait confus (comme moi), [CATransaction setDisableActions:YES]est un raccourci pour juste la [CATransaction setValue:forKey:]ligne. Vous avez toujours besoin des lignes beginet commit.
Hlung
31

Lorsque vous modifiez la propriété d'un calque, CA crée généralement un objet de transaction implicite pour animer la modification. Si vous ne souhaitez pas animer la modification, vous pouvez désactiver les animations implicites en créant une transaction explicite et en définissant sa propriété kCATransactionDisableActions sur true .

Objectif c

[CATransaction begin];
[CATransaction setValue:(id)kCFBooleanTrue forKey:kCATransactionDisableActions];
// change properties here without animation
[CATransaction commit];

Rapide

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()
user3378170
la source
6
setDisableActions: fait de même.
Ben Sinclair
3
Celui-ci était la solution la plus simple pour laquelle j'ai travaillé dans Swift!
Jambaman
Le commentaire de @Andy est de loin le moyen le meilleur et le plus simple de le faire!
Aᴄʜᴇʀᴏɴғᴀɪʟ
23

En plus de la réponse de Brad Larson : pour les couches personnalisées (que vous avez créées), vous pouvez utiliser la délégation au lieu de modifier le actionsdictionnaire des couches . Cette approche est plus dynamique et peut être plus performante. Et il permet de désactiver toutes les animations implicites sans avoir à lister toutes les clés animables.

Malheureusement, il est impossible d'utiliser les UIViews comme délégués de couche personnalisés, car chacun UIViewest déjà un délégué de sa propre couche. Mais vous pouvez utiliser une classe d'assistance simple comme celle-ci:

@interface MyLayerDelegate : NSObject
    @property (nonatomic, assign) BOOL disableImplicitAnimations;
@end

@implementation MyLayerDelegate

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event
{
    if (self.disableImplicitAnimations)
         return (id)[NSNull null]; // disable all implicit animations
    else return nil; // allow implicit animations

    // you can also test specific key names; for example, to disable bounds animation:
    // if ([event isEqualToString:@"bounds"]) return (id)[NSNull null];
}

@end

Utilisation (à l'intérieur de la vue):

MyLayerDelegate *delegate = [[MyLayerDelegate alloc] init];

// assign to a strong property, because CALayer's "delegate" property is weak
self.myLayerDelegate = delegate;

self.myLayer = [CALayer layer];
self.myLayer.delegate = delegate;

// ...

self.myLayerDelegate.disableImplicitAnimations = YES;
self.myLayer.position = (CGPoint){.x = 10, .y = 42}; // will not animate

// ...

self.myLayerDelegate.disableImplicitAnimations = NO;
self.myLayer.position = (CGPoint){.x = 0, .y = 0}; // will animate

Parfois, il est pratique d'avoir le contrôleur de vue comme délégué pour les sous-couches personnalisées de vue; dans ce cas, il n'y a pas besoin d'une classe d'assistance, vous pouvez implémenter la actionForLayer:forKey:méthode directement dans le contrôleur.

Note importante: n'essayez pas de modifier le délégué de UIViewla couche sous-jacente (par exemple pour activer des animations implicites) - de mauvaises choses vont arriver :)

Remarque: si vous souhaitez animer (et non désactiver l'animation pour) les redessins de calques, il est inutile de mettre un [CALayer setNeedsDisplayInRect:]appel à l'intérieur de a CATransaction, car un redessinage réel peut (et se produira probablement) parfois plus tard. La bonne approche consiste à utiliser des propriétés personnalisées, comme décrit dans cette réponse .

Skozin
la source
Cela ne fonctionne pas pour moi. Vois ici.
aleclarson le
Hmmm. Je n'ai jamais eu de problèmes avec cette approche. Le code de la question liée semble correct et le problème est probablement causé par un autre code.
skozin
Ah, je vois que vous avez déjà réglé le fait que c'était mal CALayerqui empêchait noImplicitAnimationsde travailler. Peut-être devriez-vous marquer votre propre réponse comme correcte et expliquer ce qui n'allait pas avec cette couche?
skozin
Je testais simplement avec la mauvaise CALayerinstance (j'en avais deux à l'époque).
aleclarson
1
Belle solution ... mais NSNulln'implémente pas le CAActionprotocole et ce n'est pas un protocole qui n'a que des méthodes optionnelles. Ce code plante également et vous ne pouvez même pas le traduire rapidement. Meilleure solution: rendre votre objet conforme au CAActionprotocole (avec une runActionForKey:object:arguments:méthode vide qui ne fait rien) et renvoyer à la selfplace de [NSNull null]. Même effet mais sûr (ne plantera pas à coup sûr) et fonctionne également dans Swift.
Mecki
9

Voici une solution plus efficace, similaire à la réponse acceptée mais pour Swift . Dans certains cas, ce sera mieux que de créer une transaction à chaque fois que vous modifiez la valeur, ce qui est un problème de performance comme d'autres l'ont mentionné, par exemple, un cas d'utilisation courant consistant à faire glisser la position du calque à 60 ips.

// Disable implicit position animation.
layer.actions = ["position": NSNull()]      

Consultez la documentation d'Apple pour savoir comment les actions de calque sont résolues . La mise en œuvre du délégué sauterait un niveau de plus dans la cascade, mais dans mon cas, c'était trop compliqué en raison de la mise en garde concernant le délégué devant être défini sur l'UIView associé .

Edit: mis à jour grâce au commentateur soulignant que se NSNullconforme à CAAction.

Jarrod Smith
la source
Pas besoin de créer un NullActionpour Swift, NSNullconforme à CAActiondéjà afin que vous puissiez faire la même chose que vous faites dans l'objectif C: layer.actions = ["position": NSNull ()]
user5649358
J'ai combiné votre réponse avec celle-ci pour corriger mon animation CATextLayer stackoverflow.com/a/5144221/816017
Erik Zivkovic
C'était une excellente solution pour mon problème de nécessité de contourner le délai "d'animation" lors du changement de couleur des lignes CALayer dans mon projet. Merci!!
PlateReverb
Court et doux! Excellente solution!
David H
7

Sur la base de la réponse de Sam et des difficultés de Simon ... ajoutez la référence du délégué après avoir créé le CSShapeLayer:

CAShapeLayer *myLayer = [CAShapeLayer layer];
myLayer.delegate = self; // <- set delegate here, it's magic.

... ailleurs dans le fichier "m" ...

Essentiellement le même que celui de Sam sans la possibilité de basculer via l'arrangement de variable personnalisé "disableImplicitAnimations". Plus d'une approche "hard-wire".

- (id<CAAction>)actionForLayer:(CALayer *)layer forKey:(NSString *)event {

    // disable all implicit animations
    return (id)[NSNull null];

    // allow implicit animations
    // return nil;

    // you can also test specific key names; for example, to disable bounds animation:
    // if ([event isEqualToString:@"bounds"]) return (id)[NSNull null];

}
bob
la source
7

En fait, je n'ai trouvé aucune des réponses comme étant la bonne. La méthode qui résout le problème pour moi était la suivante:

- (id<CAAction>)actionForKey:(NSString *)event {   
    return nil;   
}

Ensuite, vous pouvez quelle que soit la logique qui s'y trouve, pour désactiver une animation spécifique, mais comme je voulais les supprimer toutes, j'ai renvoyé zéro.

Simon
la source
5

Pour désactiver les animations de calques implicites dans Swift

CATransaction.setDisableActions(true)
pawpoise
la source
Merci pour cette réponse. J'ai d'abord essayé d'utiliser disableActions()car il semble que cela fasse la même chose, mais c'est en fait pour obtenir la valeur actuelle. Je pense que c'est marqué @discardableaussi, ce qui rend cela plus difficile à repérer. Source: developer.apple.com/documentation/quartzcore/catransaction/...
Austin
5

Découvrez une méthode plus simple pour désactiver l'action à l'intérieur d'un CATransactionqui appelle en interne setValue:forKey:la kCATransactionDisableActionsclé:

[CATransaction setDisableActions:YES];

Rapide:

CATransaction.setDisableActions(true)
Rounak
la source
2

Ajoutez ceci à votre classe personnalisée où vous implémentez la méthode -drawRect (). Apportez des modifications au code pour répondre à vos besoins, pour moi, l'opacité a fait l'affaire pour arrêter l'animation en fondu enchaîné.

-(id<CAAction>) actionForLayer:(CALayer *)layer forKey:(NSString *)key
{
    NSLog(@"key: %@", key);
    if([key isEqualToString:@"opacity"])
    {
        return (id<CAAction>)[NSNull null];
    }

    return [super actionForLayer:layer forKey:key];
}
Kamran Khan
la source
1

Si jamais vous avez besoin d'une solution très rapide (mais certes piratée), cela vaut peut-être la peine de le faire (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999
Martin CR
la source
3
Merci de ne jamais faire ça ffs
m1h4
@ m1h4 merci pour cela - veuillez expliquer pourquoi c'est une mauvaise idée
Martin CR
3
Parce que si l'on a besoin de désactiver les animations implicites, il existe un mécanisme pour le faire (soit une transaction ca avec des actions temporairement désactivées, soit la définition explicite d'actions vides sur un calque). Le simple fait de régler la vitesse de l'animation sur quelque chose, espérons-le, assez élevé pour le faire paraître instantané, entraîne des charges de performances inutiles (ce que l'auteur original mentionne comme étant pertinent pour lui) et un potentiel pour diverses conditions de course (le dessin est toujours fait dans un tampon séparé pour être animé dans l'affichage à un moment ultérieur - pour être précis, pour votre cas ci-dessus, à 0,25 / 999 sec plus tard).
m1h4 du
C'est vraiment dommage que view.layer?.actions = [:]ça ne marche pas vraiment. Le réglage de la vitesse est moche mais fonctionne.
tcurdt
1

Mise à jour pour accélérer et désactiver une seule animation de propriété implicite sous iOS et non sous MacOS

// Disable the implicit animation for changes to position
override open class func defaultAction(forKey event: String) -> CAAction? {
    if event == #keyPath(position) {
        return NSNull()
    }
    return super.defaultAction(forKey: event)
}

Autre exemple, dans ce cas, l'élimination de deux animations implicites.

class RepairedGradientLayer: CAGradientLayer {

    // Totally ELIMINATE idiotic implicit animations, in this example when
    // we hide or move the gradient layer

    override open class func defaultAction(forKey event: String) -> CAAction? {
        if event == #keyPath(position) {
            return NSNull()
        }
        if event == #keyPath(isHidden) {
            return NSNull()
        }
        return super.defaultAction(forKey: event)
    }
}
GayleDDS
la source
0

À partir d' iOS 7, il existe une méthode pratique qui fait exactement cela:

[UIView performWithoutAnimation:^{
    // apply changes
}];
Warpling
la source
1
Je ne pense pas que cette méthode bloque les animations CALayer .
Benjohn
1
@Benjohn Ah je pense que tu as raison. Je n'en savais pas autant en août. Dois-je supprimer cette réponse?
Warpling
:-) Je ne suis jamais sûr non plus, désolé! Les commentaires communiquent de toute façon l'incertitude, donc ça va probablement.
Benjohn
0

Pour désactiver l'animation ennuyeuse (floue) lors de la modification de la propriété de chaîne d'un CATextLayer, vous pouvez le faire:

class CANullAction: CAAction {
    private static let CA_ANIMATION_CONTENTS = "contents"

    @objc
    func runActionForKey(event: String, object anObject: AnyObject, arguments dict: [NSObject : AnyObject]?) {
        // Do nothing.
    }
}

puis utilisez-le comme tel (n'oubliez pas de configurer correctement votre CATextLayer, par exemple la police correcte, etc.):

caTextLayer.actions = [CANullAction.CA_ANIMATION_CONTENTS: CANullAction()]

Vous pouvez voir ma configuration complète de CATextLayer ici:

private let systemFont16 = UIFont.systemFontOfSize(16.0)

caTextLayer = CATextLayer()
caTextLayer.foregroundColor = UIColor.blackColor().CGColor
caTextLayer.font = CGFontCreateWithFontName(systemFont16.fontName)
caTextLayer.fontSize = systemFont16.pointSize
caTextLayer.alignmentMode = kCAAlignmentCenter
caTextLayer.drawsAsynchronously = false
caTextLayer.actions = [CANullAction.CA_ANIMATION_CONTENTS: CANullAction()]
caTextLayer.contentsScale = UIScreen.mainScreen().scale
caTextLayer.frame = CGRectMake(playbackTimeImage.layer.bounds.origin.x, ((playbackTimeImage.layer.bounds.height - playbackTimeLayer.fontSize) / 2), playbackTimeImage.layer.bounds.width, playbackTimeLayer.fontSize * 1.2)

uiImageTarget.layer.addSublayer(caTextLayer)
caTextLayer.string = "The text you want to display"

Vous pouvez maintenant mettre à jour caTextLayer.string autant que vous le souhaitez =)

Inspiré par ceci et par cette réponse.

Erik Zivkovic
la source
0

Essaye ça.

let layer = CALayer()
layer.delegate = hoo // Same lifecycle UIView instance.

avertissement

Si vous définissez le délégué de l'instance UITableView, il se produit parfois un crash (probablement le plus fort de scrollview appelé récursivement).

Tueno
la source