Deaktivieren impliziter Animationen in - [CALayer setNeedsDisplayInRect:]

137

Ich habe eine Ebene mit komplexem Zeichnungscode in der Methode -drawInContext :. Ich versuche, den Zeichnungsaufwand zu minimieren, daher verwende ich -setNeedsDisplayInRect:, um nur die geänderten Teile zu aktualisieren. Das funktioniert hervorragend. Wenn das Grafiksystem jedoch meine Ebene aktualisiert, wechselt es mithilfe einer Überblendung vom alten zum neuen Bild. Ich möchte, dass es sofort umschaltet.

Ich habe versucht, mit CATransaction Aktionen auszuschalten und die Dauer auf Null zu setzen, und beides funktioniert nicht. Hier ist der Code, den ich verwende:

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

Gibt es eine andere Methode für CATransaction, die ich stattdessen verwenden sollte (ich habe auch versucht -setValue: forKey: mit kCATransactionDisableActions das gleiche Ergebnis).

Ben Gottlieb
quelle
Sie können es in der nächsten dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
Laufschleife
1
Ich habe unten viele Antworten gefunden, um für mich zu arbeiten. Hilfreich ist auch das Dokument zum Ändern des Standardverhaltens einer Ebene von Apple , in dem der Entscheidungsprozess für implizite Aktionen ausführlich beschrieben wird.
Neurobur"
Dies ist eine doppelte Frage zu dieser: stackoverflow.com/a/54656717/5067402
Ryan Francesconi

Antworten:

172

Sie können dies tun, indem Sie das Aktionswörterbuch auf der zurückzugebenden Ebene festlegen [NSNull null] als Animation für die entsprechende Taste zurückgegeben wird. Zum Beispiel benutze ich

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

layer.actions = newActions;

Deaktivieren von Ein- / Ausblendanimationen beim Einfügen oder Ändern von Unterebenen in einer meiner Ebenen sowie von Änderungen der Größe und des Inhalts der Ebene. Ich glaube, der contentsSchlüssel ist der, nach dem Sie suchen, um das Überblenden bei aktualisierten Zeichnungen zu verhindern.


Schnelle Version:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]
Brad Larson
quelle
24
Verwenden Sie die @"position"Taste, um eine Bewegung beim Wechseln des Rahmens zu verhindern .
mxcl
11
Stellen Sie außerdem sicher, dass Sie die @"hidden"Eigenschaft auch im Aktionswörterbuch hinzufügen, wenn Sie die Sichtbarkeit einer Ebene auf diese Weise umschalten und die Deckkraftanimation deaktivieren möchten.
Andrew
1
@BradLarson, die die gleiche Idee i mit nach einigen kam zu kämpfen (i overrode actionForKey:statt), die Entdeckung fontSize, contents, onLayoutund bounds. Es scheint, als könnten Sie jeden Schlüssel angeben, den Sie in der setValue:forKey:Methode verwenden könnten , und tatsächlich komplexe Schlüsselpfade wie angeben bounds.size.
pqnet
11
Es gibt tatsächlich Konstanten für diese 'speziellen' Zeichenfolgen, die keine Eigenschaft darstellen (z. B. kCAOnOrderOut für @ "onOrderOut"), die hier gut dokumentiert sind: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Patrick Pijnappel
1
@Benjohn Nur für die Schlüssel ohne entsprechende Eigenschaft sind Konstanten definiert. Übrigens scheint der Link tot zu sein, hier ist die neue URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Patrick Pijnappel
89

Ebenfalls:

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

//foo

[CATransaction commit];
mxcl
quelle
3
Sie können ersetzen //foomit [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];der ursprünglichen Frage zu beantworten.
Karoy Lorentey
1
Vielen Dank! Auf diese Weise kann ich auch in meiner benutzerdefinierten Ansicht ein animiertes Flag setzen. Praktisch für die Verwendung in einer Zelle mit Tabellenansicht (wobei die Wiederverwendung von Zellen beim Scrollen zu trippigen Animationen führen kann).
Joe D'Andrea
3
Führt zu Leistungsproblemen für mich, das Setzen von Aktionen ist performanter
Pascalius
26
Stenografie:[CATransaction setDisableActions:YES]
titaniumdecoy
7
Das Hinzufügen zu @titaniumdecoy Kommentar, nur für den Fall, dass jemand verwirrt wurde (wie ich), [CATransaction setDisableActions:YES]ist eine Abkürzung für nur die [CATransaction setValue:forKey:]Zeile. Sie brauchen noch die beginund commitZeilen.
Hlung
31

Wenn Sie die Eigenschaft eines Layers ändern, erstellt CA normalerweise ein implizites Transaktionsobjekt, um die Änderung zu animieren. Wenn Sie die Änderung nicht animieren möchten, können Sie implizite Animationen deaktivieren, indem Sie eine explizite Transaktion erstellen und die Eigenschaft kCATransactionDisableActions auf true setzen .

Ziel c

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

Schnell

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()
user3378170
quelle
6
setDisableActions: macht dasselbe.
Ben Sinclair
3
Dies war die einfachste Lösung, die ich in Swift bekommen habe!
Jambaman
Der Kommentar von @Andy ist bei weitem der beste und einfachste Weg, dies zu tun!
Aᴄʜᴇʀᴏɴғᴀɪʟ
23

Zusätzlich zu Brad Larsons Antwort : Für benutzerdefinierte Ebenen (die von Ihnen erstellt wurden) können Sie die Delegierung verwenden, anstatt die Ebenen zu ändernactions Wörterbuch ändern. Dieser Ansatz ist dynamischer und kann performanter sein. Außerdem können alle impliziten Animationen deaktiviert werden, ohne dass alle animierbaren Schlüssel aufgelistet werden müssen.

Leider ist es nicht möglich, UIViews als benutzerdefinierte Layer-Delegaten zu verwenden, da jeder UIViewbereits ein Delegat seiner eigenen Ebene ist. Sie können jedoch eine einfache Hilfsklasse wie diese verwenden:

@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

Verwendung (innerhalb der Ansicht):

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

Manchmal ist es praktisch, den Controller von view als Delegaten für die benutzerdefinierten Unterschichten von view zu haben. In diesem Fall ist keine Hilfsklasse erforderlich. Sie können die actionForLayer:forKey:Methode direkt im Controller implementieren .

Wichtiger Hinweis: Versuchen Sie nicht, den Delegaten der UIViewzugrunde liegenden Ebene zu ändern (z. B. um implizite Animationen zu aktivieren) - es werden schlimme Dinge passieren :)

Hinweis: Wenn Sie Ebenen- Neuzeichnungen animieren (nicht deaktivieren) möchten, ist es sinnlos, einen [CALayer setNeedsDisplayInRect:]Aufruf in eine zu setzen CATransaction, da das tatsächliche Neuzeichnen manchmal (und wahrscheinlich auch später) erfolgen kann. Der gute Ansatz besteht darin, benutzerdefinierte Eigenschaften zu verwenden, wie in dieser Antwort beschrieben .

Skozin
quelle
Das funktioniert bei mir nicht. Siehe hier.
Aleclarson
Hmmm. Ich hatte noch nie Probleme mit diesem Ansatz. Der Code in der verknüpften Frage sieht in Ordnung aus und wahrscheinlich wird das Problem durch einen anderen Code verursacht.
Skozin
Ah, ich sehe, dass Sie bereits geklärt haben, dass es falsch war CALayer, dass es nicht noImplicitAnimationsfunktioniert hat. Vielleicht sollten Sie Ihre eigene Antwort als richtig markieren und erklären, was mit dieser Ebene nicht stimmte?
Skozin
Ich habe einfach mit der falschen CALayerInstanz getestet (ich hatte damals zwei).
Aleclarson
1
Gute Lösung ... aber NSNullimplementiert das CAActionProtokoll nicht und dies ist kein Protokoll, das nur optionale Methoden hat. Auch dieser Code stürzt ab und man kann ihn nicht einmal schnell übersetzen. Bessere Lösung: Passen Sie Ihr Objekt an das CAActionProtokoll an (mit einer leeren runActionForKey:object:arguments:Methode, die nichts bewirkt) und kehren Sie selfstattdessen zurück [NSNull null]. Gleicher Effekt, aber sicher (wird nicht sicher abstürzen) und funktioniert auch in Swift.
Mecki
9

Hier ist eine effizientere Lösung, ähnlich der akzeptierten Antwort, jedoch für Swift . In einigen Fällen ist es besser, als jedes Mal eine Transaktion zu erstellen, wenn Sie den Wert ändern, was ein Leistungsproblem darstellt, wie andere bereits erwähnt haben, z. B. den allgemeinen Anwendungsfall, die Ebenenposition mit 60 fps zu verschieben.

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

Siehe Apple - Dokumentation für wie Schicht Maßnahmen gelöst werden . Das Implementieren des Delegaten würde eine weitere Ebene in der Kaskade überspringen, aber in meinem Fall war dies zu chaotisch, da der Delegat auf die zugehörige UIView eingestellt werden musste .

Bearbeiten: Aktualisiert dank des Kommentators, der darauf hinweist, dass dies NSNullkonform ist CAAction.

Jarrod Smith
quelle
Keine Notwendigkeit, ein NullActionfür Swift zu erstellen , NSNullentspricht CAActionbereits, so dass Sie dasselbe tun können, was Sie in Ziel C tun: layer.actions = ["position": NSNull ()]
user5649358
Ich habe Ihre Antwort mit dieser kombiniert, um meinen animierenden CATextLayer stackoverflow.com/a/5144221/816017
Erik Zivkovic
Dies war eine großartige Lösung für mein Problem, dass die Verzögerung der "Animation" beim Ändern der Farbe der CALayer-Linien in meinem Projekt umgangen werden musste. Vielen Dank!!
PlateReverb
Kurz und bündig! Tolle Lösung!
David H
7

Basierend auf Sams Antwort und Simons Schwierigkeiten ... fügen Sie die Delegatenreferenz nach dem Erstellen des CSShapeLayer hinzu:

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

... an anderer Stelle in der "m" -Datei ...

Im Wesentlichen dasselbe wie bei Sam ohne die Möglichkeit, über die benutzerdefinierte Variablenanordnung "disableImplicitAnimations" umzuschalten. Eher ein "Hard-Wire" -Ansatz.

- (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
quelle
7

Eigentlich fand ich keine der Antworten richtig. Die Methode, die das Problem für mich löst, war folgende:

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

Dann können Sie jede Logik darin verwenden, um eine bestimmte Animation zu deaktivieren, aber da ich sie alle entfernen wollte, gab ich null zurück.

Simon
quelle
5

So deaktivieren Sie implizite Ebenenanimationen in Swift

CATransaction.setDisableActions(true)
Pawpoise
quelle
Danke für diese Antwort. Ich habe zuerst versucht, es zu verwenden, disableActions()da es so klingt, als ob es dasselbe tut, aber es ist tatsächlich, um den aktuellen Wert zu erhalten. Ich denke, es ist auch markiert @discardable, was es schwieriger macht, dies zu erkennen. Quelle: developer.apple.com/documentation/quartzcore/catransaction/…
Austin
5

Es wurde eine einfachere Methode zum Deaktivieren der Aktion in einem System gefunden CATransaction, die intern setValue:forKey:den kCATransactionDisableActionsSchlüssel anfordert:

[CATransaction setDisableActions:YES];

Schnell:

CATransaction.setDisableActions(true)
rounak
quelle
2

Fügen Sie dies Ihrer benutzerdefinierten Klasse hinzu, in der Sie die Methode -drawRect () implementieren. Nehmen Sie Änderungen am Code vor, um Ihren Anforderungen zu entsprechen. Für mich hat 'Opazität' den Trick getan, um Cross-Fade-Animationen zu stoppen.

-(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
quelle
1

Wenn Sie jemals eine sehr schnelle (aber zugegebenermaßen hackige) Lösung benötigen, lohnt es sich möglicherweise, nur (Swift) zu tun:

let layer = CALayer()

// set other properties
// ...

layer.speed = 999
Martin CR
quelle
3
Bitte mach das nie ffs
m1h4
@ m1h4 danke dafür - bitte erklären Sie, warum dies eine schlechte Idee ist
Martin CR
3
Denn wenn implizite Animationen deaktiviert werden müssen, gibt es dafür einen Mechanismus (entweder eine ca-Transaktion mit vorübergehend deaktivierten Aktionen oder das explizite Festlegen leerer Aktionen auf einer Ebene). Das Einstellen der Animationsgeschwindigkeit auf etwas, das hoffentlich hoch genug ist, um es sofort erscheinen zu lassen, verursacht eine Menge unnötigen Leistungsaufwands (den der ursprüngliche Autor erwähnt, ist für ihn relevant) und Potenzial für verschiedene Rennbedingungen (die Zeichnung wird immer noch in einem separaten Puffer erstellt) zu einem späteren Zeitpunkt in das Display animiert werden - genauer gesagt für Ihren obigen Fall um 0,25 / 999 Sekunden später).
m1h4
Es ist wirklich eine Schande, view.layer?.actions = [:]die nicht wirklich funktioniert. Das Einstellen der Geschwindigkeit ist hässlich, funktioniert aber.
tcurdt
1

Aktualisiert für schnelles und Deaktivieren nur einer impliziten Eigenschaftsanimation unter iOS und nicht unter 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)
}

Ein weiteres Beispiel: In diesem Fall werden zwei implizite Animationen entfernt.

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
quelle
0

Ab iOS 7 gibt es eine praktische Methode, die genau dies tut:

[UIView performWithoutAnimation:^{
    // apply changes
}];
Warpling
quelle
1
Ich glaube nicht, dass diese Methode CALayer-Animationen blockiert .
Benjohn
1
@ Benjamin Ah ich denke du hast recht. Wusste im August nicht so viel. Soll ich diese Antwort löschen?
Warpling
:-) Ich bin mir auch nie sicher, sorry! Die Kommentare kommunizieren die Unsicherheit trotzdem, also ist es wahrscheinlich okay.
Benjohn
0

So deaktivieren Sie die nervige (verschwommene) Animation beim Ändern der Zeichenfolgeeigenschaft eines CATextLayer:

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

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

und verwenden Sie es dann wie folgt (vergessen Sie nicht, Ihren CATextLayer ordnungsgemäß einzurichten, z. B. die richtige Schriftart usw.):

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

Sie können mein komplettes Setup von CATextLayer hier sehen:

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"

Jetzt können Sie caTextLayer.string so oft aktualisieren, wie Sie möchten =)

Inspiriert von dieser und dieser Antwort.

Erik Zivkovic
quelle
0

Versuche dies.

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

Warnung

Wenn Sie den Delegaten der UITableView-Instanz festlegen, kommt es manchmal zu einem Absturz.

Tueno
quelle