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:
Ocasionalmente, recibo un bloqueo (Editar: no importa. Olvidé convertirEXC_BAD_ACCESS
) enCGPathApply()
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.UIBezierPath
enCGPathRef
. Gracias @antonioviero !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.
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:
laactionForKey:
la capaactionForKey:
oactionForKey:
la vistaactionForLayer:forKey:
para devolver un objeto de animación personalizado para la propiedadpath
. 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:
actionForLayer:forKey:
elactionForLayer:forKey:
o el métodoactionForKey:
y compruebe si la clave que se pasa a este método es la que desea animar (@"path"
).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 nonil
oNSNull
), lo somos.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