Wyłączanie niejawnych animacji w - [CALayer setNeedsDisplayInRect:]

137

Mam warstwę ze złożonym kodem rysunkowym w metodzie -drawInContext:. Próbuję zminimalizować ilość rysowania, które muszę zrobić, więc używam -setNeedsDisplayInRect: do aktualizacji tylko zmienionych części. To działa świetnie. Jednak gdy system graficzny aktualizuje moją warstwę, przechodzi ze starego do nowego obrazu przy użyciu efektu przenikania. Chciałbym, żeby przełączało się natychmiast.

Próbowałem użyć CATransaction, aby wyłączyć akcje i ustawić czas trwania na zero, ale żadna z nich nie działa. Oto kod, którego używam:

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

Czy istnieje inna metoda na CATransaction, której powinienem użyć zamiast tego (próbowałem również -setValue: forKey: z kCATransactionDisableActions, ten sam wynik).

Ben Gottlieb
źródło
możesz to zrobić w następnej pętli dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
biegowej
1
Poniżej znalazłem wiele odpowiedzi, które działają dla mnie. Pomocny jest również dokument firmy Apple dotyczący zmiany domyślnego zachowania warstwy , który szczegółowo opisuje niejawny proces podejmowania decyzji.
ɲeuroburɳ
To jest zduplikowane pytanie do tego: stackoverflow.com/a/54656717/5067402
Ryan Francesconi

Odpowiedzi:

172

Możesz to zrobić, ustawiając słownik akcji na warstwie, aby powracał [NSNull null]jako animacja dla odpowiedniego klucza. Na przykład używam

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

layer.actions = newActions;

aby wyłączyć animacje pojawiania się / zanikania podczas wstawiania lub zmiany podwarstw w jednej z moich warstw, a także zmiany rozmiaru i zawartości warstwy. Uważam, że contentskluczem jest ten, którego szukasz, aby zapobiec przenikaniu na zaktualizowanym rysunku.


Wersja Swift:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]
Brad Larson
źródło
24
Aby zapobiec przemieszczaniu się podczas zmiany ramy, użyj @"position"klucza.
mxcl
11
Pamiętaj również, aby dodać @"hidden"właściwość również do słownika akcji, jeśli przełączasz w ten sposób widoczność warstwy i chcesz wyłączyć animację krycia.
Andrew,
1
@BradLarson że to ten sam pomysł wpadłem po pewnym trudem (i overrode actionForKey:zamiast), odkrywając fontSize, contents, onLayouti bounds. Wygląda na to, że możesz określić dowolny klucz, którego możesz użyć w setValue:forKey:metodzie, w rzeczywistości określając złożone ścieżki kluczy, takie jak bounds.size.
pqnet
11
W rzeczywistości istnieją stałe dla tych „specjalnych” ciągów, które nie reprezentują właściwości (np. KCAOnOrderOut dla @ „onOrderOut”), dobrze udokumentowane tutaj: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Patrick Pijnappel
1
@Benjohn Tylko klucze, które nie mają odpowiedniej właściwości, mają zdefiniowane stałe. Przy okazji, link wydaje się być martwy. Oto nowy adres URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/ ...
Patrick Pijnappel
89

Również:

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

//foo

[CATransaction commit];
mxcl
źródło
3
Można wymienić //fooz [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];odpowiedzieć na oryginalne pytanie.
Karoy Lorentey
1
Dzięki! To pozwala mi ustawić animowaną flagę również w moim niestandardowym widoku. Przydatny do użytku w komórce widoku tabeli (gdzie ponowne użycie komórki może prowadzić do niektórych dziwnych animacji podczas przewijania).
Joe D'Andrea
3
Prowadzi to do problemów z wydajnością, ustawienie działań jest bardziej wydajne
Pascalius
26
Stenogram:[CATransaction setDisableActions:YES]
titaniumdecoy
7
Dodanie do komentarza @titaniumdecoy, na wypadek gdyby ktoś się pomylił (jak ja), [CATransaction setDisableActions:YES]jest skrótem dla samej [CATransaction setValue:forKey:]linii. Nadal potrzebujesz linii begini commit.
Hlung
31

Po zmianie właściwości warstwy ośrodek CA zwykle tworzy niejawny obiekt transakcji, aby ożywić zmianę. Jeśli nie chcesz animować zmiany, możesz wyłączyć niejawne animacje, tworząc jawną transakcję i ustawiając jej właściwość kCATransactionDisableActions na wartość true .

Cel C

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

Szybki

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()
user3378170
źródło
6
setDisableActions: robi to samo.
Ben Sinclair
3
To było najprostsze rozwiązanie, które otrzymałem, pracując w Swift!
Jambaman
Komentarz @Andy jest zdecydowanie najlepszym i najłatwiejszym sposobem na zrobienie tego!
Aᴄʜᴇʀᴏɴғᴀɪʟ
23

Oprócz odpowiedzi Brada Larsona : dla warstw niestandardowych (które są tworzone przez Ciebie) możesz użyć delegowania zamiast modyfikowania actionssłownika warstwy . To podejście jest bardziej dynamiczne i może być bardziej wydajne. I umożliwia wyłączenie wszystkich niejawnych animacji bez konieczności wyszczególniania wszystkich animowanych klawiszy.

Niestety, nie można używać UIViews jako delegatów warstwy niestandardowej, ponieważ każda z nich UIViewjest już delegatem swojej własnej warstwy. Ale możesz użyć prostej klasy pomocniczej, takiej jak ta:

@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

Zastosowanie (wewnątrz widoku):

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

Czasami wygodnie jest mieć kontroler widoku jako delegata dla niestandardowych podwarstw widoku; w tym przypadku nie ma potrzeby stosowania klasy pomocniczej, można zaimplementować actionForLayer:forKey:metodę bezpośrednio w kontrolerze.

Ważna uwaga: nie próbuj modyfikować delegata UIViewwarstwy bazowej (np. Aby włączyć niejawne animacje) - zdarzają się złe rzeczy :)

Uwaga: jeśli chcesz animować (nie wyłączać animacji) przerysowania warstw, nie ma sensu umieszczać [CALayer setNeedsDisplayInRect:]wywołania wewnątrz a CATransaction, ponieważ faktyczne przerysowanie może (i prawdopodobnie nastąpi) później. Dobrym podejściem jest użycie właściwości niestandardowych, jak opisano w tej odpowiedzi .

skozin
źródło
To nie działa na mnie. Spójrz tutaj.
aleclarson
Hmmm. Nigdy nie miałem żadnych problemów z tym podejściem. Kod w pytaniu, do którego prowadzi link, wygląda dobrze i prawdopodobnie przyczyną problemu jest inny kod.
skozin
Ach, widzę, że już ustaliliście, że to było złe, CALayerco uniemożliwiło noImplicitAnimationspracę. Może powinieneś oznaczyć swoją odpowiedź jako poprawną i wyjaśnić, co było nie tak z tą warstwą?
skozin
Po prostu testowałem z niewłaściwą CALayerinstancją (miałem wtedy dwie).
aleclarson
1
Niezłe rozwiązanie ... ale NSNullnie implementuje CAActionprotokołu i nie jest to protokół, który ma tylko opcjonalne metody. Ten kod również ulega awarii i nie można tego nawet przetłumaczyć na szybkość. Lepsze rozwiązanie: spraw, aby obiekt był zgodny z CAActionprotokołem (z pustą runActionForKey:object:arguments:metodą, która nic nie robi) i zwróć selfzamiast [NSNull null]. Ten sam efekt, ale bezpieczny (na pewno nie ulegnie awarii) i działa również w Swift.
Mecki
9

Oto bardziej wydajne rozwiązanie, podobne do akceptowanej odpowiedzi, ale dla Swift . W niektórych przypadkach będzie to lepsze niż tworzenie transakcji za każdym razem, gdy modyfikujesz wartość, która ma wpływ na wydajność, jak wspominali inni, np. Częsty przypadek użycia przeciągania pozycji warstwy przy 60 klatkach na sekundę.

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

Zobacz dokumentację Apple, aby dowiedzieć się, jak rozwiązuje się działania warstw . Zaimplementowanie delegata spowodowałoby pominięcie jeszcze jednego poziomu w kaskadzie, ale w moim przypadku było to zbyt bałagan z powodu zastrzeżenia dotyczącego delegata, który musi być ustawiony na skojarzony UIView .

Edycja: Zaktualizowano dzięki komentatorowi wskazującemu, że NSNulljest zgodny z CAAction.

Jarrod Smith
źródło
Nie ma potrzeby tworzenia NullActionfor Swift, NSNulljest CAActionjuż zgodne, więc możesz zrobić to samo, co w celu C: layer.actions = ["position": NSNull ()]
user5649358
Połączyłem twoją odpowiedź z tą, aby naprawić mój animowany stackoverflow
Erik Zivkovic
To była świetna poprawka dla mojego problemu z pominięciem opóźnienia "animacji" przy zmianie koloru linii CALayer w moim projekcie. Dzięki!!
PlateReverb
Krótkie i słodkie! Świetne rozwiązanie!
David H
7

W oparciu o odpowiedź Sama i trudności Simona ... dodaj odwołanie delegata po utworzeniu CSShapeLayer:

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

... w innym miejscu w pliku „m” ...

Zasadniczo to samo, co u Sama, bez możliwości przełączania się za pomocą niestandardowego układu zmiennych „disableImplicitAnimations”. Bardziej podejście „sztywnego drutu”.

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

}
pion
źródło
7

Właściwie nie znalazłem żadnej odpowiedzi jako właściwej. Metoda, która rozwiązuje ten problem była dla mnie następująca:

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

Wtedy możesz dowolną logikę, aby wyłączyć określoną animację, ale ponieważ chciałem je wszystkie usunąć, zwróciłem zero.

Szymon
źródło
5

Aby wyłączyć niejawne animacje warstw w Swift

CATransaction.setDisableActions(true)
pawpoise
źródło
Dzięki za tę odpowiedź. Najpierw próbowałem użyć, disableActions()bo wygląda na to, że robi to samo, ale tak naprawdę jest to, aby uzyskać bieżącą wartość. Myślę, że to też jest zaznaczone @discardable, przez co trudniej to zauważyć. Źródło: developer.apple.com/documentation/quartzcore/catransaction/…
Austin,
5

Dowiedział się prostszą metodę do działania wyłączyć Wewnątrz CATransactionże wewnętrznie wzywa setValue:forKey:do kCATransactionDisableActionsklucza:

[CATransaction setDisableActions:YES];

Szybki:

CATransaction.setDisableActions(true)
rounak
źródło
2

Dodaj to do swojej klasy niestandardowej, w której implementujesz metodę -drawRect (). Wprowadź zmiany w kodzie, aby dostosować go do swoich potrzeb, dla mnie „krycie” wystarczyło, aby zatrzymać animację przechodzenia.

-(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
źródło
1

Jeśli kiedykolwiek będziesz potrzebować bardzo szybkiej (ale wprawdzie hakerskiej) poprawki, warto po prostu zrobić (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999
Martin CR
źródło
3
Proszę, nigdy nie rób tego ffs
m1h4
@ m1h4 dzięki za to - proszę wyjaśnić, dlaczego to zły pomysł
Martin CR
3
Ponieważ jeśli trzeba wyłączyć niejawne animacje, istnieje mechanizm umożliwiający to (transakcja ca z tymczasowo wyłączonymi akcjami lub jawne ustawienie pustych akcji na warstwie). Samo ustawienie szybkości animacji na coś, miejmy nadzieję, wystarczająco wysokiej, aby wydawała się natychmiastowa, powoduje mnóstwo niepotrzebnych narzutów wydajnościowych (które pierwotny autor wspomina, że ​​są dla niego istotne) i potencjał dla różnych warunków wyścigu (rysunek jest nadal wykonywany w oddzielnym buforze, aby być animowane na wyświetlaczu w późniejszym momencie - a dokładniej, w powyższym przypadku, 0,25 / 999 sekundy później).
m1h4
To naprawdę szkoda, że view.layer?.actions = [:]tak naprawdę nie działa. Ustawienie prędkości jest brzydkie, ale działa.
tcurdt
1

Zaktualizowano w celu szybkiego i wyłączenia tylko jednej niejawnej animacji właściwości w systemie iOS, a nie w systemie 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)
}

Inny przykład, w tym przypadku eliminacja dwóch niejawnych animacji.

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
źródło
0

Od iOS 7 istnieje wygodna metoda, która robi to:

[UIView performWithoutAnimation:^{
    // apply changes
}];
Wypaczanie
źródło
1
Nie wierzę, że ta metoda blokuje animacje CALayer .
Benjohn,
1
@Benjohn Ah, myślę, że masz rację. Nie wiedział tyle w sierpniu. Czy mam usunąć tę odpowiedź?
Warpling
:-) Ja też nigdy nie jestem pewien, przepraszam! Komentarze i tak przekazują niepewność, więc prawdopodobnie jest w porządku.
Benjohn,
0

Aby wyłączyć irytującą (rozmytą) animację podczas zmiany właściwości ciągu CATextLayer, możesz to zrobić:

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

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

a następnie użyj go w ten sposób (nie zapomnij odpowiednio ustawić CATextLayer, np. poprawnej czcionki itp.):

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

Możesz zobaczyć moją pełną konfigurację CATextLayer tutaj:

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"

Teraz możesz aktualizować caTextLayer.string tyle, ile chcesz =)

Zainspirowany tym i odpowiedzią.

Erik Zivkovic
źródło
0

Spróbuj tego.

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

Ostrzeżenie

Jeśli ustawisz delegata instancji UITableView, czasami zdarzają się awarie (prawdopodobnie najczęstszy przewijany widok nazywa się rekurencyjnie).

Tueno
źródło