Deshabilitar animaciones implícitas en - [CALayer setNeedsDisplayInRect:]

137

Tengo una capa con un código de dibujo complejo en su método -drawInContext:. Estoy tratando de minimizar la cantidad de dibujo que necesito hacer, así que estoy usando -setNeedsDisplayInRect: para actualizar solo las partes cambiadas. Esto está funcionando espléndidamente. Sin embargo, cuando el sistema gráfico actualiza mi capa, está pasando de la imagen antigua a la nueva usando un fundido cruzado. Me gustaría que cambiara al instante.

Intenté usar CATransaction para desactivar las acciones y establecer la duración en cero, y tampoco funciona. Aquí está el código que estoy usando:

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

¿Hay un método diferente en CATransaction que debería usar en su lugar (también probé -setValue: forKey: with kCATransactionDisableActions, mismo resultado).

Ben Gottlieb
fuente
puedes hacerlo en el próximo ciclo de ejecución: dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{ });
Hashem Aboonajmi
1
Encontré muchas respuestas a continuación para trabajar para mí. También es útil el documento Cambiar el comportamiento predeterminado de una capa de Apple , que describe el proceso de decisión de acción implícita en detalle.
uroeuroburɳ
Esta es una pregunta duplicada a esta: stackoverflow.com/a/54656717/5067402
Ryan Francesconi

Respuestas:

172

Puede hacer esto configurando el diccionario de acciones en la capa para que regrese [NSNull null]como una animación para la clave adecuada. Por ejemplo, yo uso

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

layer.actions = newActions;

para deshabilitar las animaciones de fundido de entrada / salida al insertar o cambiar las subcapas dentro de una de mis capas, así como los cambios en el tamaño y el contenido de la capa. Creo que la contentsclave es la que está buscando para evitar el fundido cruzado en el dibujo actualizado.


Versión rápida:

let newActions = [
        "onOrderIn": NSNull(),
        "onOrderOut": NSNull(),
        "sublayers": NSNull(),
        "contents": NSNull(),
        "bounds": NSNull(),
    ]
Brad Larson
fuente
24
Para evitar el movimiento al cambiar el marco, use la @"position"tecla.
mxcl
11
También asegúrese de agregar la @"hidden"propiedad en el diccionario de acción si está alternando la visibilidad de una capa de esa manera y desea deshabilitar la animación de opacidad.
Andrew
1
@BradLarson que es la misma idea que subí después de algunas dificultades (i hizo caso omiso de actionForKey:su lugar), el descubrimiento fontSize, contents, onLayouty bounds. Parece que puede especificar cualquier clave que pueda usar en el setValue:forKey:método, en realidad especificando rutas de teclas complejas como bounds.size.
pqnet
11
En realidad, hay constantes para estas cadenas 'especiales' que no representan una propiedad (por ejemplo, kCAOnOrderOut para @ "onOrderOut") bien documentadas aquí: developer.apple.com/library/mac/#documentation/Cocoa/Conceptual/…
Patrick Pijnappel
1
@Benjohn Solo las claves que no tienen una propiedad correspondiente tienen constantes definidas. Por cierto, el enlace parece estar muerto, aquí está la nueva URL: developer.apple.com/library/mac/documentation/Cocoa/Conceptual/…
Patrick Pijnappel
89

También:

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

//foo

[CATransaction commit];
mxcl
fuente
3
Puede reemplazar //foocon [self setNeedsDisplayInRect: rect]; [self displayIfNeeded];para responder la pregunta original.
Karoy Lorentey
1
¡Gracias! Esto también me permite establecer una bandera animada en mi vista personalizada. Práctico para usar dentro de una celda de vista de tabla (donde la reutilización de la celda puede conducir a algunas animaciones triples mientras se desplaza).
Joe D'Andrea
3
Conduce a problemas de rendimiento para mí, establecer acciones es más
eficiente
26
Taquigrafía:[CATransaction setDisableActions:YES]
titaniumdecoy
77
Agregar al comentario de @titaniumdecoy, en caso de que alguien se confunda (como yo), [CATransaction setDisableActions:YES]es una abreviatura de solo la [CATransaction setValue:forKey:]línea. Todavía necesitas elbegincommit líneas y .
Hlung
31

Cuando cambia la propiedad de una capa, CA generalmente crea un objeto de transacción implícito para animar el cambio. Si no desea animar el cambio, puede deshabilitar las animaciones implícitas creando una transacción explícita y estableciendo su propiedad kCATransactionDisableActions en true .

C objetivo

[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()
usuario3378170
fuente
66
setDisableActions: hace lo mismo.
Ben Sinclair
3
¡Esta fue la solución más simple que obtuve trabajando en Swift!
Jambaman
¡El comentario de @Andy es, con mucho, la mejor y más fácil manera de hacer esto!
Aᴄʜᴇʀᴏɴғᴀɪʟ
23

Además de la respuesta de Brad Larson : para capas personalizadas (creadas por usted) puede usar la delegación lugar de modificar el actionsdiccionario de la capa . Este enfoque es más dinámico y puede ser más eficaz. Y permite deshabilitar todas las animaciones implícitas sin tener que enumerar todas las teclas animables.

Desafortunadamente, es imposible usar UIViews como delegados de capa personalizados, porque cada uno UIViewya es un delegado de su propia capa. Pero puede usar una clase auxiliar simple 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 de la 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 veces es conveniente tener el controlador de vista como delegado para las subcapas personalizadas de vista; en este caso no hay necesidad de una clase auxiliar, puede implementar el actionForLayer:forKey:método directamente dentro del controlador.

Nota importante: no intente modificar el delegado de UIViewla capa subyacente (por ejemplo, para habilitar animaciones implícitas), sucederán cosas malas :)

Nota: si desea animar (no deshabilitar la animación para) los redibujos de capa, es inútil poner la [CALayer setNeedsDisplayInRect:]llamada dentro de un CATransaction, porque el redibujo real puede (y probablemente) ocurrirá a veces más tarde. El buen enfoque es usar propiedades personalizadas, como se describe en esta respuesta .

skozin
fuente
Esto no está funcionando para mí. Mira aquí.
aleclarson
Hmmm Nunca he tenido ningún problema con este enfoque. El código en la pregunta vinculada se ve bien y probablemente el problema sea causado por algún otro código.
skozin
Ah, veo que ya has resuelto que estaba mal CALayerevitar que noImplicitAnimationsfuncionara. ¿Quizás debería marcar su propia respuesta como correcta y explicar qué estaba mal con esa capa?
skozin
Simplemente estaba probando con la CALayerinstancia incorrecta (tenía dos en ese momento).
aleclarson
1
Buena solución ... pero NSNullno implementa el CAActionprotocolo y este no es un protocolo que solo tenga métodos opcionales. Este código también falla y ni siquiera puedes traducirlo a rápido. Mejor solución: haga que su objeto se ajuste al CAActionprotocolo (con un runActionForKey:object:arguments:método vacío que no hace nada) y regrese en selflugar de [NSNull null]. El mismo efecto pero seguro (no se bloqueará con seguridad) y también funciona en Swift.
Mecki
9

Aquí hay una solución más eficiente, similar a la respuesta aceptada pero para Swift . En algunos casos, será mejor que crear una transacción cada vez que modifique el valor, lo cual es un problema de rendimiento como otros han mencionado, por ejemplo, el caso de uso común de arrastrar la posición de la capa a 60 fps.

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

Consulte los documentos de Apple para ver cómo se resuelven las acciones de capa . La implementación del delegado omitiría un nivel más en la cascada, pero en mi caso eso fue demasiado complicado debido a la advertencia de que el delegado necesita estar configurado en la UIView asociada .

Editar: actualizado gracias al comentador que señala que se NSNullajusta a CAAction.

Jarrod Smith
fuente
No es necesario crear un NullActionpara Swift, ya se NSNullajusta para CAActionque pueda hacer lo mismo que hace en el objetivo C: layer.actions = ["position": NSNull ()]
user5649358
Combiné tu respuesta con esta para arreglar mi animación CATextLayer stackoverflow.com/a/5144221/816017
Erik Zivkovic
Esta fue una gran solución para mi problema de evitar el retraso de la "animación" al cambiar el color de las líneas CALayer en mi proyecto. ¡¡Gracias!!
PlateReverb
¡Corto y dulce! Gran solución!
David H
7

Según la respuesta de Sam y las dificultades de Simon ... agregue la referencia de delegado después de crear CSShapeLayer:

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

... en otra parte del archivo "m" ...

Esencialmente lo mismo que Sam sin la capacidad de alternar a través de la disposición variable personalizada "disableImplicitAnimations". Más de un enfoque de "cable duro".

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

}
Beto
fuente
7

En realidad, no encontré ninguna de las respuestas correctas. El método que resolvió el problema para mí fue este:

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

Luego puede usar la lógica que contenga, para deshabilitar una animación específica, pero como quería eliminarlos a todos, regresé nulo.

Simón
fuente
5

Para deshabilitar animaciones de capa implícitas en Swift

CATransaction.setDisableActions(true)
pawpoise
fuente
Gracias por esta respuesta Primero intenté usarlo, disableActions()ya que parece que hace lo mismo, pero en realidad es para obtener el valor actual. Creo que también está marcado @discardable, lo que hace que sea más difícil de detectar. Fuente: developer.apple.com/documentation/quartzcore/catransaction/…
Austin
5

Encontró un método más simple para deshabilitar la acción dentro de un CATransactionque internamente solicita setValue:forKey:la kCATransactionDisableActionsclave:

[CATransaction setDisableActions:YES];

Rápido:

CATransaction.setDisableActions(true)
Rounak
fuente
2

Agregue esto a su clase personalizada donde está implementando el método -drawRect (). Realice cambios en el código para satisfacer sus necesidades, para mí 'opacidad' hizo el truco para detener la animación de fundido cruzado.

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

Si alguna vez necesita una solución muy rápida (pero ciertamente hacky), puede valer la pena hacerlo (Swift):

let layer = CALayer()

// set other properties
// ...

layer.speed = 999
Martin CR
fuente
3
Por favor, nunca hagas esto
ffs
@ m1h4 gracias por eso - explique por qué es una mala idea
Martin CR
3
Porque si uno necesita desactivar las animaciones implícitas, existe un mecanismo para hacerlo (ya sea una transacción ca con acciones temporalmente deshabilitadas o establecer explícitamente acciones vacías en una capa). Solo establecer la velocidad de la animación en algo que sea lo suficientemente alto como para que parezca instantáneo provoca una carga de rendimiento innecesaria (que el autor original menciona es relevante para él) y potencial para varias condiciones de carrera (el dibujo aún se realiza en un búfer separado para ser animado en la pantalla en un momento posterior, para ser precisos, para su caso anterior, a 0.25 / 999 segundos más tarde).
m1h4
Realmente es una pena que view.layer?.actions = [:]realmente no funcione. Establecer la velocidad es feo pero funciona.
tcurdt
1

Actualizado para acelerar e inhabilitar solo una animación de propiedad implícita en iOS, no en 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)
}

Otro ejemplo, en este caso eliminando dos animaciones 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
fuente
0

A partir de iOS 7, hay un método de conveniencia que hace exactamente esto:

[UIView performWithoutAnimation:^{
    // apply changes
}];
Deformación
fuente
1
No creo que este método bloquee las animaciones CALayer .
Benjohn
1
@Benjohn Ah, creo que tienes razón. No sabía tanto en agosto. ¿Debo eliminar esta respuesta?
Warpling
:-) Nunca estoy seguro tampoco, lo siento! Los comentarios comunican la incertidumbre de todos modos, por lo que probablemente esté bien.
Benjohn
0

Para deshabilitar la animación molesta (borrosa) al cambiar la propiedad de cadena de un CATextLayer, puede hacer esto:

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

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

y luego úselo así (no olvide configurar su CATextLayer correctamente, por ejemplo, la fuente correcta, etc.):

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

Puedes ver mi configuración completa de CATextLayer aquí:

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"

Ahora puede actualizar caTextLayer.string tanto como desee =)

Inspirado por esto y esta respuesta.

Erik Zivkovic
fuente
0

Prueba esto.

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

Advertencia

Si configura un delegado de la instancia de UITableView, a veces puede ocurrir un bloqueo (probablemente el hittest de scrollview se llama recursivamente).

Tueno
fuente