Desativando animações implícitas em - [CALayer setNeedsDisplayInRect:]

137

Eu tenho uma camada com algum código de desenho complexo em seu método -drawInContext:. Estou tentando minimizar a quantidade de desenho que preciso fazer, portanto, estou usando -setNeedsDisplayInRect: para atualizar apenas as partes alteradas. Isso está funcionando esplendidamente. No entanto, quando o sistema gráfico atualiza minha camada, ele está mudando da imagem antiga para a nova imagem usando um cross-fade. Eu gostaria que mudasse instantaneamente.

Eu tentei usar o CATransaction para desativar ações e definir a duração como zero, e nenhum trabalho. Aqui está o código que estou usando:

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

Existe um método diferente no CATransaction que eu deveria usar (também tentei -setValue: forKey: with kCATransactionDisableActions, mesmo resultado).

Ben Gottlieb
fonte
você pode fazê-lo no próximo ciclo de execução: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
Hashem Aboonajmi
1
Encontrei muitas respostas abaixo para trabalhar para mim. Também é útil o documento Alterando o comportamento padrão de uma camada da Apple , que descreve em detalhes o processo de decisão de ação implícita.
ɲeuroburɳ
Esta é uma pergunta duplicado a esta: stackoverflow.com/a/54656717/5067402
Ryan Francesconi

Respostas:

172

Você pode fazer isso definindo o dicionário de ações na camada para retornar [NSNull null]como uma animação para a chave apropriada. Por exemplo, eu uso

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

layer.actions = newActions;

para desativar as animações de desvanecimento / desbotamento na inserção ou alteração de subcamadas em uma das minhas camadas, bem como alterações no tamanho e no conteúdo da camada. Acredito que a contentschave é a que você está procurando para evitar o crossfade no desenho atualizado.


Versão rápida:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]
Brad Larson
fonte
24
Para impedir o movimento ao alterar o quadro, use a @"position"tecla
Mxcl 30/03
11
Também não se esqueça de adicionar a @"hidden"propriedade no dicionário de ações se você estiver alternando a visibilidade de uma camada dessa maneira e desejar desativar a animação de opacidade.
Andrew
1
@BradLarson que é a mesma idéia que eu vim acima com após alguns lutando (i cancelou actionForKey:vez), descobrindo fontSize, contents, onLayoute bounds. Parece que você pode especificar qualquer chave que possa usar no setValue:forKey:método, especificando caminhos de chave complexos como bounds.size.
Pqnet
11
Na verdade, existem constantes para essas strings 'especiais' que não representam uma propriedade (por exemplo, kCAOnOrderOut para @ "onOrderOut") bem documentadas aqui: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Patrick Pijnappel
1
@ Benjohn Apenas as chaves que não possuem uma propriedade correspondente têm constantes definidas. BTW, o link parece estar morto, aqui está o novo URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Patrick Pijnappel 4/14 / 14-
89

Além disso:

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

//foo

[CATransaction commit];
mxcl
fonte
3
Você pode substituir //foopor [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];para responder à pergunta original.
Karoy Lorentey
1
Obrigado! Isso também permite definir um sinalizador animado na minha exibição personalizada. Útil para uso em uma célula de exibição de tabela (onde a reutilização da célula pode levar a algumas animações descontroladas durante a rolagem).
Joe D'Andrea
3
Leva a problemas de desempenho para mim, definindo ações é mais performant
Pascalius
26
Taquigrafia:[CATransaction setDisableActions:YES]
titaniumdecoy
7
Adicionar ao comentário @titaniumdecoy, para o caso de alguém ficar confuso (como eu), [CATransaction setDisableActions:YES]é uma abreviação para apenas a [CATransaction setValue:forKey:]linha. Você ainda precisa das linhas begine commit.
Hlung
31

Quando você altera a propriedade de uma camada, a CA geralmente cria um objeto de transação implícito para animar a alteração. Se você não deseja animar a alteração, pode desativar animações implícitas criando uma transação explícita e configurando sua propriedade kCATransactionDisableActions como true .

Objetivo-C

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

Rápido

CATransaction.begin()
CATransaction.setValue(kCFBooleanTrue, forKey: kCATransactionDisableActions)
// change properties here without animation
CATransaction.commit()
user3378170
fonte
6
setDisableActions: faz o mesmo.
Ben Sinclair
3
Essa foi a solução mais simples que consegui trabalhar no Swift!
Jambaman
O comentário de @Andy é de longe a melhor e mais fácil maneira de fazer isso!
2525
23

Além da resposta de Brad Larson : para camadas personalizadas (criadas por você), você pode usar a delegação em vez de modificar o actionsdicionário da camada . Essa abordagem é mais dinâmica e pode ter melhor desempenho. E permite desabilitar todas as animações implícitas sem precisar listar todas as chaves animáveis.

Infelizmente, é impossível usar UIViews como delegados de camadas personalizadas, porque cada UIViewum já é um delegado de sua própria camada. Mas você pode usar uma classe auxiliar simples como esta:

@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

Uso (dentro da 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

Às vezes, é conveniente ter o controlador do view como um representante das subcamadas personalizadas do view; Nesse caso, não há necessidade de uma classe auxiliar, você pode implementar o actionForLayer:forKey:método dentro do controlador.

Nota importante: não tente modificar o delegado da UIViewcamada subjacente (por exemplo, para ativar animações implícitas) - coisas ruins acontecerão :)

Nota: se você deseja animar (e não desativar a animação para) redesenhos de camadas, é inútil colocar uma [CALayer setNeedsDisplayInRect:]chamada dentro de a CATransaction, porque o redesenho real pode (e provavelmente ocorrerá) algumas vezes mais tarde. A boa abordagem é usar propriedades personalizadas, conforme descrito nesta resposta .

skozin
fonte
Isso não está funcionando para mim. Veja aqui.
aleclarson 14/09/14
Hummm. Eu nunca tive problemas com essa abordagem. O código na pergunta vinculada parece ok e provavelmente o problema é causado por outro código.
Skokin #
Ah, vejo que você já resolveu que era errado CALayerimpedir o noImplicitAnimationstrabalho. Talvez você deva marcar sua própria resposta como correta e explicar o que havia de errado com essa camada?
Skozin 14/11/14
Eu estava simplesmente testando com a CALayerinstância errada (eu tinha duas na época).
Aleclarson
1
Solução agradável ... mas NSNullnão implementa o CAActionprotocolo e este não é um protocolo que possui apenas métodos opcionais. Este código também falha e você não pode nem traduzir isso rapidamente. Melhor solução: faça seu objeto estar em conformidade com o CAActionprotocolo (com um runActionForKey:object:arguments:método vazio que não faz nada) e retorne em selfvez de [NSNull null]. O mesmo efeito, mas seguro (não trava, com certeza) e também funciona no Swift.
Mecki 27/05
9

Aqui está uma solução mais eficiente, semelhante à resposta aceita, mas para Swift . Em alguns casos, será melhor do que criar uma transação toda vez que você modificar o valor, o que é uma preocupação de desempenho, como outros já mencionaram, por exemplo, casos de uso comuns de arrastar a posição da camada a 60fps.

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

Consulte os documentos da apple para saber como as ações da camada são resolvidas . A implementação do delegado pularia mais um nível na cascata, mas, no meu caso, isso era muito confuso devido à advertência de que o delegado precisava ser definido para o UIView associado .

Editar: atualizado graças ao comentarista apontando que NSNullestá em conformidade CAAction.

Jarrod Smith
fonte
Não há necessidade de criar um NullActionpara o Swift, já NSNullestá em conformidade , para CAActionque você possa fazer o mesmo que você faz no objetivo C: layer.actions = ["position": NSNull ()]
user5649358
Eu combinei a sua resposta com um presente para corrigir o meu animador CATextLayer stackoverflow.com/a/5144221/816017
Erik Zivkovic
Esta foi uma excelente correção para o meu problema de ignorar o atraso da "animação" ao alterar a cor das linhas do CALayer no meu projeto. Obrigado!!
precisa saber é o seguinte
Curto e grosso! Ótima solução!
18719 David David H
7

Com base na resposta de Sam e nas dificuldades de Simon ... adicione a referência de delegado depois de criar o CSShapeLayer:

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

... em outro lugar no arquivo "m" ...

Essencialmente o mesmo que o de Sam, sem a capacidade de alternar através do arranjo variável personalizado "disableImplicitAnimations". Mais uma abordagem "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];

}
prumo
fonte
7

Na verdade, não encontrei nenhuma das respostas certas. O método que resolve o problema para mim foi o seguinte:

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

Em seguida, você pode qualquer lógica nele, para desativar uma animação específica, mas como eu queria removê-las, retornei nulo.

Simon
fonte
5

Para desativar animações de camada implícitas no Swift

CATransaction.setDisableActions(true)
pawpoise
fonte
Obrigado por esta resposta. Tentei usar pela primeira disableActions()vez, pois parece que faz a mesma coisa, mas na verdade é para obter o valor atual. Eu acho que também está marcado @discardable, dificultando a identificação. Fonte: developer.apple.com/documentation/quartzcore/catransaction/…
Austin
5

Descobriu um método mais simples para desativar a ação dentro de um CATransactionque chama internamente setValue:forKey:a kCATransactionDisableActionschave:

[CATransaction setDisableActions:YES];

Rápido:

CATransaction.setDisableActions(true)
rounak
fonte
2

Adicione isso à sua classe personalizada na qual você está implementando o método -drawRect (). Faça alterações no código para atender às suas necessidades, para mim, a 'opacidade' fez o truque para interromper a animação de desbotamento.

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

Se você precisar de uma correção muito rápida (mas reconhecidamente hacky), pode valer a pena fazer (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999
Martin CR
fonte
3
Por favor, nunca faça isso
ffs
@ m1h4 obrigado por isso - por favor, explicar por que isso é uma má idéia
Martin CR
3
Porque se for necessário desativar animações implícitas, existe um mecanismo para fazer isso (uma transação ca com ações temporariamente desabilitadas ou a definição explícita de ações vazias em uma camada). Apenas definir a velocidade da animação para algo esperançosamente alto o suficiente para parecer instantâneo causa uma sobrecarga desnecessária de desempenho (que o autor original menciona é relevante para ele) e potencial para várias condições de corrida (o desenho ainda é feito em um buffer separado para ser animado para a exibição em um momento posterior - para ser mais preciso, para o seu caso acima, em 0,25 / 999 s depois).
M1h4 11/09/19
É realmente uma pena que view.layer?.actions = [:]realmente não funciona. Definir a velocidade é feio, mas funciona.
precisa saber é
1

Atualizado para agilizar e desabilitar apenas uma animação de propriedade implícita no iOS, não no 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)
}

Outro exemplo, neste caso, eliminando duas animações implícitas.

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

No iOS 7, existe um método de conveniência que faz exatamente isso:

[UIView performWithoutAnimation:^{
    // apply changes
}];
Warpling
fonte
1
Não acredito que esse método bloqueie as animações do CALayer .
Benjohn 19/02/16
1
@ Benjohn Ah, eu acho que você está certo. Não sabia tanto em agosto. Devo excluir esta resposta?
Warpling
:-) Eu também nunca tenho certeza, desculpe! Os comentários comunicam a incerteza de qualquer maneira, então provavelmente está tudo bem.
22716 Benjohn
0

Para desativar a animação irritante (embaçada) ao alterar a propriedade de seqüência de caracteres de um CATextLayer, você pode fazer isso:

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 use-o assim (não esqueça de configurar seu CATextLayer corretamente, por exemplo, a fonte correta, etc.):

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

Você pode ver minha configuração completa do CATextLayer aqui:

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"

Agora você pode atualizar o caTextLayer.string o quanto quiser =)

Inspirado por isso e por esta resposta.

Erik Zivkovic
fonte
0

Tente isso.

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

Aviso

Se você definir o delegado da instância do UITableView, às vezes ocorrerá uma falha (provavelmente o scrollview é o mais avançado chamado recursivamente).

Tueno
fonte