Disabilitazione delle animazioni implicite in - [CALayer setNeedsDisplayInRect:]


137

Ho un livello con qualche codice di disegno complesso nel suo metodo -drawInContext: Sto cercando di ridurre al minimo la quantità di disegno che devo fare, quindi sto usando -setNeedsDisplayInRect: per aggiornare solo le parti modificate. Funziona magnificamente. Tuttavia, quando il sistema grafico aggiorna il mio livello, sta passando dalla vecchia alla nuova immagine usando una dissolvenza incrociata. Vorrei che passasse all'istante.

Ho provato a utilizzare CATransaction per disattivare le azioni e impostare la durata su zero, e nessuno dei due funziona. Ecco il codice che sto usando:

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

Esiste invece un metodo diverso su CATransaction che dovrei usare (ho anche provato -setValue: forKey: con kCATransactionDisableActions, stesso risultato).


puoi farlo nel prossimo ciclo di esecuzione: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
Hashem Aboonajmi,

1
Ho trovato molte risposte qui sotto per lavorare per me. Inoltre è utile il documento di modifica del comportamento predefinito di Apple di un livello , che descrive in dettaglio il processo decisionale relativo all'azione implicita.
ɲeuroburɳ,

Questa è una domanda duplicata a questa: stackoverflow.com/a/54656717/5067402
Ryan Francesconi,

Risposte:


172

Puoi farlo impostando il dizionario delle azioni sul layer per restituirlo [NSNull null]come animazione per la chiave appropriata. Ad esempio, io uso

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

layer.actions = newActions;

per disabilitare le animazioni di dissolvenza in apertura / chiusura durante l'inserimento o la modifica dei sublayer all'interno di uno dei miei livelli, nonché le modifiche nelle dimensioni e nei contenuti del livello. Credo che la contentschiave sia quella che stai cercando per evitare la dissolvenza incrociata sul disegno aggiornato.


Versione rapida:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]

24
Per evitare movimenti quando si cambia la cornice, utilizzare il @"position"tasto.
MXC

11
Assicurati anche di aggiungere la @"hidden"proprietà anche nel dizionario delle azioni se stai attivando la visibilità di un livello in quel modo e desideri disabilitare l'animazione dell'opacità.
Andrew,

1
@BradLarson che è la stessa idea mi è venuta dopo un po 'in difficoltà (i calpestato actionForKey:invece), scoprendo fontSize, contents, onLayoute bounds. Sembra che tu possa specificare qualsiasi chiave che potresti usare nel setValue:forKey:metodo, in realtà specificando percorsi chiave complessi come bounds.size.
pqnet,

11
In realtà ci sono costanti per queste stringhe "speciali" che non rappresentano una proprietà (ad esempio kCAOnOrderOut per @ "onOrderOut") ben documentate qui: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Patrick Pijnappel

1
@Benjohn Solo le chiavi che non hanno una proprietà corrispondente hanno costanti definite. A proposito, il link sembra essere morto, ecco il nuovo URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Patrick Pijnappel

89

Anche:

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

//foo

[CATransaction commit];

3
È possibile sostituire //foocon [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];per rispondere alla domanda originale.
Karoy Lorentey,

1
Grazie! Questo mi consente di impostare una bandiera animata anche sulla mia vista personalizzata. Comodo per l'uso all'interno di una cella di visualizzazione tabella (in cui il riutilizzo delle celle può portare ad alcune animazioni trippy durante lo scorrimento).
Joe D'Andrea,

3
Porta a problemi di prestazioni per me, l'impostazione delle azioni è più performante
Pascalius

26
Stenografia:[CATransaction setDisableActions:YES]
titaniumdecoy

7
L'aggiunta al commento di @titaniumdecoy, nel caso in cui qualcuno fosse confuso (come me), [CATransaction setDisableActions:YES]è una scorciatoia per la [CATransaction setValue:forKey:]linea. Hai ancora bisogno delle linee begine commit.
Hlung,

31

Quando si modifica la proprietà di un livello, CA solitamente crea un oggetto di transazione implicito per animare la modifica. Se non si desidera animare la modifica, è possibile disabilitare le animazioni implicite creando una transazione esplicita e impostando la proprietà kCATransactionDisableActions su true .

Objective-C

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

veloce

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()

6
setDisableActions: fa lo stesso.
Ben Sinclair,

3
Questa è stata la soluzione più semplice con cui ho lavorato in Swift!
Jambaman,

Il commento di @Andy è di gran lunga il modo migliore e più semplice per farlo!
Aᴄʜᴇʀᴏɴғᴀɪʟ

23

Oltre alla risposta di Brad Larson : per i livelli personalizzati (creati da te) puoi usare la delega invece di modificare il actionsdizionario dei livelli . Questo approccio è più dinamico e può essere più performante. E consente di disabilitare tutte le animazioni implicite senza dover elencare tutte le chiavi animabili.

Sfortunatamente, è impossibile usare UIViews come delegati di layer personalizzati, perché ognuno UIViewè già un delegato del proprio layer. Ma puoi usare una semplice classe di supporto come questa:

@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

Utilizzo (all'interno della vista):

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

A volte è conveniente avere il controller di view come delegato per i sublayer personalizzati di view; in questo caso non è necessaria una classe helper, è possibile implementare il actionForLayer:forKey:metodo direttamente all'interno del controller.

Nota importante: non tentare di modificare il delegato del UIViewlivello sottostante (ad esempio per abilitare animazioni implicite) - accadranno cose brutte :)

Nota: se si desidera animare (non disabilitare l'animazione per) i ridisegni dei layer, è inutile inserire la [CALayer setNeedsDisplayInRect:]chiamata all'interno di un CATransaction, poiché il ridisegno effettivo può (e probabilmente accadrà) a volte più tardi. Il buon approccio è utilizzare proprietà personalizzate, come descritto in questa risposta .


Questo non funziona per me. Vedere qui.
aleclarson,

Hmmm. Non ho mai avuto problemi con questo approccio. Il codice nella domanda collegata sembra ok e probabilmente il problema è causato da qualche altro codice.
skozin,

Ah, vedo che hai già risolto che era sbagliato CALayerche ha impedito noImplicitAnimationsdi funzionare. Forse dovresti contrassegnare la tua risposta come corretta e spiegare cosa c'era di sbagliato in quel livello?
skozin,

Stavo semplicemente testando con l' CALayeristanza sbagliata (ne avevo due in quel momento).
aleclarson,

1
Bella soluzione ... ma NSNullnon implementa il CAActionprotocollo e questo non è un protocollo che ha solo metodi opzionali. Anche questo codice si blocca e non puoi nemmeno tradurlo in rapido. Soluzione migliore: rendere l'oggetto conforme al CAActionprotocollo (con un runActionForKey:object:arguments:metodo vuoto che non fa nulla) e restituire selfinvece di [NSNull null]. Stesso effetto ma sicuro (non si arresta in modo sicuro) e funziona anche in Swift.
Mecki,

9

Ecco una soluzione più efficiente, simile alla risposta accettata ma per Swift . In alcuni casi sarà meglio che creare una transazione ogni volta che si modifica il valore che è un problema di prestazioni come altri hanno menzionato, ad esempio un caso d'uso comune di trascinare la posizione del livello a 60fps.

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

Consulta i documenti di Apple per come vengono risolte le azioni di livello . L'implementazione del delegato salterebbe un altro livello nella cascata, ma nel mio caso era troppo disordinato a causa delle avvertenze sul delegato che doveva essere impostato sull'UIView associato .

Modifica: aggiornato grazie al commentatore sottolineando che è NSNullconforme CAAction.


Non c'è bisogno di creare un NullActionper Swift, NSNullè CAActiongià conforme a così puoi fare lo stesso che fai nell'obiettivo C: layer.actions = ["position": NSNull ()]
user5649358

Ho unito la vostra risposta con questo per risolvere il mio animatore CATextLayer stackoverflow.com/a/5144221/816017
Erik Zivkovic

Questa è stata un'ottima soluzione per il mio problema di necessità di bypassare il ritardo "animazione" quando si cambia il colore delle linee CALayer nel mio progetto. Grazie!!
PlateReverb,

Breve e dolce! Ottima soluzione!
David H,

7

Sulla base della risposta di Sam e delle difficoltà di Simon ... aggiungi il riferimento delegato dopo aver creato CSShapeLayer:

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

... altrove nel file "m" ...

Essenzialmente uguale a quello di Sam senza la possibilità di alternare tramite la disposizione variabile personalizzata "disableImplicitAnimations". Più di un approccio "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];

}

7

In realtà, non ho trovato nessuna delle risposte come quella giusta. Il metodo che risolve il problema per me è stato questo:

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

Quindi è possibile qualsiasi logica in esso, per disabilitare un'animazione specifica, ma poiché volevo rimuoverli tutti, sono tornato a zero.


5

Per disabilitare animazioni di livello implicite in Swift

CATransaction.setDisableActions(true)

Grazie per questa risposta Prima ho provato a usare disableActions()come sembra fare la stessa cosa, ma in realtà è per ottenere il valore corrente. Penso che sia segnato @discardableanche, rendendo questo più difficile da individuare. Fonte: developer.apple.com/documentation/quartzcore/catransaction/…
Austin,

5

Ho scoperto un metodo più semplice per disabilitare l'azione all'interno di una CATransactionche richiede internamente setValue:forKey:la kCATransactionDisableActionschiave:

[CATransaction setDisableActions:YES];

Swift:

CATransaction.setDisableActions(true)

2

Aggiungi questo alla tua classe personalizzata in cui stai implementando il metodo -drawRect (). Apporta modifiche al codice per soddisfare le tue esigenze, per me 'l'opacità' ha fatto il trucco per fermare l'animazione a dissolvenza incrociata.

-(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];
}

1

Se hai mai bisogno di una soluzione molto rapida (ma per lo meno confusa) potrebbe valere la pena di fare solo (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999

3
Per favore, non fare mai questo ffs
m1h4,

@ m1h4 grazie per questo - ti prego di spiegare perché questa è una cattiva idea
Martin CR

3
Perché se è necessario disattivare le animazioni implicite esiste un meccanismo per farlo (o una transazione ca con azioni temporaneamente disabilitate o l'impostazione esplicita di azioni vuote su un livello). Basta impostare la velocità dell'animazione su qualcosa che si spera sia abbastanza alto da far sembrare istantaneo causare un sovraccarico di prestazioni non necessarie (che l'autore originale menziona è rilevante per lui) e il potenziale per varie condizioni di gara (il disegno è ancora fatto in un buffer separato per essere animato sul display in un secondo momento - per essere precisi, per il caso sopra, a 0,25 / 999 secondi dopo).
m1h4,

È davvero un peccato che view.layer?.actions = [:]non funzioni davvero. L'impostazione della velocità è brutta ma funziona.
tcurdt,

1

Aggiornato per velocizzare e disabilitare solo una animazione di proprietà implicita in iOS non 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)
}

Un altro esempio, in questo caso eliminando due animazioni implicite.

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)
    }
}

0

A partire da iOS 7 c'è un metodo pratico che fa proprio questo:

[UIView performWithoutAnimation:^{
    // apply changes
}];

1
Non credo che questo metodo blocchi le animazioni di CALayer .
Benjohn,

1
@Benjohn Ah penso che tu abbia ragione. Non lo sapevo molto ad agosto. Devo eliminare questa risposta?
Warpling,

:-) Non ne sono mai sicuro, scusa! I commenti comunicano comunque l'incertezza, quindi probabilmente va bene.
Benjohn,

0

Per disabilitare la fastidiosa animazione (sfocata) quando si modifica la proprietà della stringa di un CATextLayer, è possibile effettuare questa operazione:

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

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

e quindi usalo in questo modo (non dimenticare di impostare correttamente CATextLayer, ad esempio il carattere corretto, ecc.):

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

Puoi vedere la mia configurazione completa di CATextLayer qui:

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"

Ora puoi aggiornare caTextLayer.string quanto vuoi =)

Ispirato da questo e da questa risposta.


0

Prova questo.

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

avvertimento

Se imposti il ​​delegato dell'istanza UITableView, a volte si verifica un arresto anomalo (probabilmente il più basso di scrollview chiamato ricorsivamente).

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.