objective framework developer apple ios animation core-animation cashapelayer

ios - framework - Animar la ruta CAShapeLayer en el cambio de límites animados



swift ios documentation (4)

Creo que está fuera de sincronización entre los límites y la animación de ruta es porque la función de sincronización diferente entre UIVIew Spring y CABasicAnimation.

Tal vez puedas probar la transformación animada en su lugar, también debe transformar la ruta (no probados), luego de que termine de animar, puedes establecer el límite.

Una forma más de hacerlo es tomar una instantánea de la ruta, configurarla como contenido de la capa, luego animar la encuadernación, luego el contenido debe seguir la animación.

Tengo un CAShapeLayer (que es la capa de una subclase de UIView) cuya path debe actualizarse cada vez que cambie el tamaño de los bounds la vista. Para esto, he reemplazado el método setBounds: la capa para restablecer la ruta:

@interface CustomShapeLayer : CAShapeLayer @end @implementation CustomShapeLayer - (void)setBounds:(CGRect)bounds { [super setBounds:bounds]; self.path = [[self shapeForBounds:bounds] CGPath]; }

Esto funciona bien hasta que anime el cambio de límites. Me gustaría hacer que la ruta anime junto con cualquier cambio de límite animado (en un bloque de animación UIView) para que la capa de forma siempre se adapte a su tamaño.

Como la propiedad path no se anima de forma predeterminada, se me ocurrió esto:

Reemplace el método addAnimation:forKey: la capa. En él, averigüe si se está agregando una animación de límites a la capa. Si es así, crea una segunda animación explícita para animar la ruta junto a los límites copiando todas las propiedades de la animación de límites que se pasa al método. En codigo:

- (void)addAnimation:(CAAnimation *)animation forKey:(NSString *)key { [super addAnimation:animation forKey:key]; if ([animation isKindOfClass:[CABasicAnimation class]]) { CABasicAnimation *basicAnimation = (CABasicAnimation *)animation; if ([basicAnimation.keyPath isEqualToString:@"bounds.size"]) { CABasicAnimation *pathAnimation = [basicAnimation copy]; pathAnimation.keyPath = @"path"; // The path property has not been updated to the new value yet pathAnimation.fromValue = (id)self.path; // Compute the new value for path pathAnimation.toValue = (id)[[self shapeForBounds:self.bounds] CGPath]; [self addAnimation:pathAnimation forKey:@"path"]; } } }

Obtuve esta idea al leer el artículo View-Layer Synergy de David Rönnqvist. (Este código es para iOS 8. En iOS 7, parece que debe verificar keyPath la animación contra @"bounds" y no `@" bounds.size ".)

El código de llamada que activa el cambio de los límites animados de la vista se vería así:

[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{ self.customShapeView.bounds = newBounds; } completion:nil];

Preguntas

Esto funciona principalmente, pero tengo dos problemas con él:

  1. Ocasionalmente, recibo un bloqueo ( EXC_BAD_ACCESS ) en CGPathApply() al activar esta animación mientras una animación anterior todavía está en progreso. No estoy seguro de si esto tiene algo que ver con mi implementación particular. Editar: no importa. Olvidé convertir UIBezierPath en CGPathRef . Gracias @antonioviero !

  2. Cuando se utiliza una animación de primavera UIView estándar, la animación de los límites de la vista y la ruta de la capa están ligeramente desincronizadas. Es decir, la animación de ruta también funciona de forma elástica, pero no sigue exactamente los límites de la vista.

  3. En términos más generales, ¿es este el mejor enfoque? Parece que tener una capa de forma cuya ruta dependa de su tamaño de límites y que debe animarse en sincronización con cualquier cambio de límites es algo que debería funcionar, pero estoy teniendo dificultades. Siento que debe haber una mejor manera.

Otras cosas que he intentado

  • actionForKey: la actionForKey: la capa actionForKey: o actionForKey: la vista actionForLayer:forKey: para devolver un objeto de animación personalizado para la propiedad path . Creo que esta sería la forma preferida, pero no encontré la forma de acceder a las propiedades de transacción que debería establecer el bloque de animación adjunto. Incluso si se llama desde dentro de un bloque de animación, [CATransaction animationDuration ] etc. siempre devuelve los valores predeterminados.

¿Hay alguna manera de (a) determinar que usted está actualmente dentro de un bloque de animación, y (b) obtener las propiedades de animación (duración, curva de animación, etc.) que se han establecido en ese bloque?

Proyecto

Aquí está la animación: el triángulo naranja es la ruta de la capa de forma. El contorno negro es el marco de la vista que aloja la capa de forma.

Echa un vistazo al proyecto de muestra en GitHub . (Este proyecto es para iOS 8 y requiere Xcode 6 para ejecutarse, lo siento).

Actualizar

José Luis Piedrahita me señaló este artículo de Nick Lockwood , que sugiere el siguiente enfoque:

  1. actionForLayer:forKey: el actionForLayer:forKey: o el método actionForKey: y compruebe si la clave que se pasa a este método es la que desea animar ( @"path" ).

  2. Si es así, si llama super a una de las propiedades animables "normales" de la capa (como @"bounds" ) le dirá implícitamente si se encuentra dentro de un bloque de animación. Si la vista (el delegado de la capa) devuelve un objeto válido (y no nil o NSNull ), lo somos.

  3. Establezca los parámetros (duración, función de tiempo, etc.) para la animación de ruta desde la animación devuelta desde [super actionForKey:] y devuelva la animación de ruta.

Esto realmente funciona muy bien en iOS 7.x. Sin embargo, en iOS 8, el objeto devuelto por actionForLayer:forKey: no es una CA...Animation estándar (y documentada) CA...Animation sino una instancia de la clase _UIViewAdditiveAnimationAction privada (una subclase NSObject ). Como las propiedades de esta clase no están documentadas, no puedo usarlas fácilmente para crear la animación de ruta.

_Editar: O podría funcionar después de todo. Como mencionó Nick en su artículo, algunas propiedades como backgroundColor aún devuelven una CA...Animation estándar CA...Animation en iOS 8. Tendré que probarlo.


Creo que tienes problemas porque juegas con el marco de una capa y su camino al mismo tiempo.

Me gustaría ir con CustomView que tiene drawRect personalizado: que dibuja lo que necesita, y luego simplemente hazlo

[UIView animateWithDuration:1.0 delay:0.0 usingSpringWithDamping:0.3 initialSpringVelocity:0.0 options:0 animations:^{ self.customView.bounds = newBounds; } completion:nil];

Sor, no es necesario usar pathes en absoluto

Esto es lo que tengo usando este enfoque https://dl.dropboxusercontent.com/u/73912254/triangle.mov


Sé que esta es una vieja pregunta, pero puedo ofrecerle una solución que parece similar al enfoque del Sr. Lockwoods. Lamentablemente, el código fuente aquí es rápido, por lo que tendrá que convertirlo a ObjC.

Como se mencionó anteriormente, si la capa es una capa de respaldo para una vista, puede interceptar las CAAction en la vista misma. Sin embargo, esto no es conveniente, por ejemplo, si la capa de respaldo se usa en más de una vista.

La buena noticia es actionForLayer: forKey: en realidad llama a actionForKey: en la capa de respaldo.

Está en actionForKey: en la capa de respaldo donde podemos interceptar estas llamadas y proporcionar una animación para cuando se cambie la ruta.

Una capa de ejemplo escrita en swift es la siguiente:

class AnimatedBackingLayer: CAShapeLayer { override var bounds: CGRect { didSet { if !CGRectIsEmpty(bounds) { path = UIBezierPath(roundedRect: CGRectInset(bounds, 10, 10), cornerRadius: 5).CGPath } } } override func actionForKey(event: String) -> CAAction? { if event == "path" { if let action = super.actionForKey("backgroundColor") as? CABasicAnimation { let animation = CABasicAnimation(keyPath: event) animation.fromValue = path // Copy values from existing action animation.autoreverses = action.autoreverses animation.beginTime = action.beginTime animation.delegate = action.delegate animation.duration = action.duration animation.fillMode = action.fillMode animation.repeatCount = action.repeatCount animation.repeatDuration = action.repeatDuration animation.speed = action.speed animation.timingFunction = action.timingFunction animation.timeOffset = action.timeOffset return animation } } return super.actionForKey(event) } }


Solución encontrada para animar la path de CAShapeLayer mientras que los bounds están animados:

typedef UIBezierPath *(^PathGeneratorBlock)(); @interface AnimatedPathShapeLayer : CAShapeLayer @property (copy, nonatomic) PathGeneratorBlock pathGenerator; @end @implementation AnimatedPathShapeLayer - (void)addAnimation:(CAAnimation *)anim forKey:(NSString *)key { if ([key rangeOfString:@"bounds.size"].location == 0) { CAShapeLayer *presentationLayer = self.presentationLayer; CABasicAnimation *pathAnim = [anim copy]; pathAnim.keyPath = @"path"; pathAnim.fromValue = (id)[presentationLayer path]; pathAnim.toValue = (id)self.pathGenerator().CGPath; self.path = [presentationLayer path]; [super addAnimation:pathAnim forKey:@"path"]; } [super addAnimation:anim forKey:key]; } - (void)removeAnimationForKey:(NSString *)key { if ([key rangeOfString:@"bounds.size"].location == 0) { [super removeAnimationForKey:@"path"]; } [super removeAnimationForKey:key]; } @end // @interface ShapeLayeredView : UIView @property (strong, nonatomic) AnimatedPathShapeLayer *layer; @end @implementation ShapeLayeredView @dynamic layer; + (Class)layerClass { return [AnimatedPathShapeLayer class]; } - (instancetype)initWithGenerator:(PathGeneratorBlock)pathGenerator { self = [super init]; if (self) { self.layer.pathGenerator = pathGenerator; } return self; } @end