Animación entre dos forms de path de bezier

Me pareció difícil animar un UIImageView entre dos estados: su marco rectángulo original y una nueva forma creada con un UIBezierPath . Se mencionaron muchas técnicas diferentes, la mayoría de las cuales no funcionaron para mí.

Primero fue la comprensión de que usar la animation de bloque UIView no funcionaría; evidentemente no se pueden realizar animaciones de subcapa en un animateWithDuration: block. (ver aquí y aquí )

Eso dejó CAAnimation , con las subclasss concretas como CABasicAnimation . Pronto me di count de que no se puede animar desde una vista que no tiene un CAShapeLayer a una que sí lo hace (ver aquí , por ejemplo).

Y no pueden ser simplemente dos routes de capa de forma, sino "Animar la ruta de una capa de forma solo se garantiza que funciona cuando estás animando de un modo similar" (ver aquí )

Con eso en su lugar, vienen los problemas más mundanos, como qué usar para fromValue y toValue (¿deberían ser un CAShapeLayer o un CGPath ?), A qué agregar la animation (la layer o la mask ), etc.

Parecía que había tantas variables; ¿Qué combinación me daría la animation que estaba buscando?

El primer punto importante es build los dos paths bezier de manera similar, por lo que el rectángulo es análogo (trivial) a la forma más compleja.

 // the complex bezier path let initialPoint = CGPoint(x: 0, y: 0) let curveStart = CGPoint(x: 0, y: (rect.size.height) * (0.2)) let curveControl = CGPoint(x: (rect.size.width) * (0.6), y: (rect.size.height) * (0.5)) let curveEnd = CGPoint(x: 0, y: (rect.size.height) * (0.8)) let firstCorner = CGPoint(x: 0, y: rect.size.height) let secondCorner = CGPoint(x: rect.size.width, y: rect.size.height) let thirdCorner = CGPoint(x: rect.size.width, y: 0) var myBezierArc = UIBezierPath() myBezierArc.moveToPoint(initialPoint) myBezierArc.addLineToPoint(curveStart) myBezierArc.addQuadCurveToPoint(curveEnd, controlPoint: curveControl) myBezierArc.addLineToPoint(firstCorner) myBezierArc.addLineToPoint(secondCorner) myBezierArc.addLineToPoint(thirdCorner) 

La ruta más sencilla 'trivial', que crea un rectángulo, es exactamente la misma, pero el punto de controlPoint se establece de modo que parezca que no está allí:

 let curveControl = CGPoint(x: 0, y: (rect.size.height) * (0.5)) 

(¡Intenta quitar la línea addQuadCurveToPoint para get una animation muy extraña!)

Y, por último, los commands de animation:

 let myAnimation = CABasicAnimation(keyPath: "path") if (isArcVisible == true) { myAnimation.fromValue = myBezierArc.CGPath myAnimation.toValue = myBezierTrivial.CGPath } else { myAnimation.fromValue = myBezierTrivial.CGPath myAnimation.toValue = myBezierArc.CGPath } myAnimation.duration = 0.4 myAnimation.fillMode = kCAFillModeForwards myAnimation.removedOnCompletion = false myImageView.layer.mask.addAnimation(myAnimation, forKey: "animatePath") 

Si alguien está interesado, el proyecto está aquí .

Otro enfoque es usar un enlace de pantalla. Es como un timer, excepto que está coordinado con la actualización de la pantalla. Luego, el manejador del enlace de visualización modifica la vista de acuerdo a cómo debería verse en cualquier punto particular de la animation.

Por ejemplo, si desea animar el networkingondeo de las esquinas de la máscara de 0 a 50 puntos, puede hacer algo como lo siguiente, donde el percent es un valor entre 0.0 y 1.0, que indica qué porcentaje de la animation se realiza:

 let path = UIBezierPath(rect: imageView.bounds) let mask = CAShapeLayer() mask.path = path.CGPath imageView.layer.mask = mask let animation = AnimationDisplayLink(duration: 0.5) { percent in let cornerRadius = percent * 50.0 let path = UIBezierPath(roundedRect: self.imageView.bounds, cornerRadius: cornerRadius) mask.path = path.CGPath } 

Dónde:

 class AnimationDisplayLink : NSObject { var animationDuration: CGFloat var animationHandler: (percent: CGFloat) -> () var completionHandler: (() -> ())? private var startTime: CFAbsoluteTime! private var displayLink: CADisplayLink! init(duration: CGFloat, animationHandler: (percent: CGFloat)->(), completionHandler: (()->())? = nil) { animationDuration = duration self.animationHandler = animationHandler self.completionHandler = completionHandler super.init() startDisplayLink() } private func startDisplayLink () { startTime = CFAbsoluteTimeGetCurrent() displayLink = CADisplayLink(target: self, selector: "handleDisplayLink:") displayLink.addToRunLoop(NSRunLoop.currentRunLoop(), forMode: NSRunLoopCommonModes) } private func stopDisplayLink() { displayLink.invalidate() displayLink = nil } func handleDisplayLink(displayLink: CADisplayLink) { let elapsed = CFAbsoluteTimeGetCurrent() - startTime var percent = CGFloat(elapsed) / animationDuration if percent >= 1.0 { stopDisplayLink() animationHandler(percent: 1.0) completionHandler?() } else { animationHandler(percent: percent) } } } 

La virtud del enfoque del enlace de visualización es que se puede usar para animar algunas properties que de otro modo son inanimables. También le permite dictar con precisión el estado provisional durante la animation.

Si puede usar CAAnimation o la animation basada en bloques UIKit , ese es probablemente el path a seguir. Pero el enlace de la pantalla a veces puede ser un buen enfoque alternativo.

Tu ejemplo me inspiró para probar una animation de círculo a cuadrado usando las técnicas que se mencionan en tu respuesta y algunos de los enlaces. Pretendo extender esto para ser un círculo más general para la animation de polígonos, pero actualmente solo funciona para cuadrados. Tengo una class llamada RDPolyCircle (una subclass de CAShapeLayer) que hace el trabajo pesado. Aquí está su código,

 @interface RDPolyCircle () @property (strong,nonatomic) UIBezierPath *polyPath; @property (strong,nonatomic) UIBezierPath *circlePath; @end @implementation RDPolyCircle { double cpDelta; double cosR; } -(instancetype)initWithFrame:(CGRect) frame numberOfSides:(NSInteger)sides isPointUp:(BOOL) isUp isInitiallyCircle:(BOOL) isCircle { if (self = [super init]) { self.frame = frame; _isPointUp = isUp; _isExpandedPolygon = !isCircle; double radius = (frame.size.width/2.0); cosR = sin(45 * M_PI/180.0) * radius; double fractionAlongTangent = 4.0*(sqrt(2)-1)/3.0; cpDelta = fractionAlongTangent * radius * sin(45 * M_PI/180.0); _circlePath = [self createCirclePathForFrame:frame]; _polyPath = [self createPolygonPathForFrame:frame numberOfSides:sides]; self.path = (isCircle)? self.circlePath.CGPath : self.polyPath.CGPath; self.fillColor = [UIColor clearColor].CGColor; self.strokeColor = [UIColor blueColor].CGColor; self.lineWidth = 6.0; } return self; } -(UIBezierPath *)createCirclePathForFrame:(CGRect) frame { CGPoint ctr = CGPointMake(frame.origin.x + frame.size.width/2.0, frame.origin.y + frame.size.height/2.0); // create a circle using 4 arcs, with the first one symmetrically spanning the y-axis CGPoint leftUpper = CGPointMake(ctr.x - cosR, ctr.y - cosR); CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y - cpDelta); CGPoint rightUpper = CGPointMake(ctr.x + cosR, ctr.y - cosR); CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y - cpDelta); CGPoint cp3 = CGPointMake(rightUpper.x + cpDelta, rightUpper.y + cpDelta); CGPoint rightLower = CGPointMake(ctr.x + cosR, ctr.y + cosR); CGPoint cp4 = CGPointMake(rightLower.x + cpDelta, rightLower.y - cpDelta); CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y + cpDelta); CGPoint leftLower = CGPointMake(ctr.x - cosR, ctr.y + cosR); CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y + cpDelta); CGPoint cp7 = CGPointMake(leftLower.x - cpDelta, leftLower.y - cpDelta); CGPoint cp8 = CGPointMake(leftUpper.x - cpDelta, leftUpper.y + cpDelta); UIBezierPath *circle = [UIBezierPath bezierPath]; [circle moveToPoint:leftUpper]; [circle addCurveToPoint:rightUpper controlPoint1:cp1 controlPoint2:cp2]; [circle addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4]; [circle addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6]; [circle addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8]; [circle closePath]; circle.lineCapStyle = kCGLineCapRound; return circle; } -(UIBezierPath *)createPolygonPathForFrame:(CGRect) frame numberOfSides:(NSInteger) sides { CGPoint leftUpper = CGPointMake(self.frame.origin.x, self.frame.origin.y); CGPoint cp1 = CGPointMake(leftUpper.x + cpDelta, leftUpper.y); CGPoint rightUpper = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y); CGPoint cp2 = CGPointMake(rightUpper.x - cpDelta, rightUpper.y); CGPoint cp3 = CGPointMake(rightUpper.x, rightUpper.y + cpDelta); CGPoint rightLower = CGPointMake(self.frame.origin.x + self.frame.size.width, self.frame.origin.y + self.frame.size.height); CGPoint cp4 = CGPointMake(rightLower.x , rightLower.y - cpDelta); CGPoint cp5 = CGPointMake(rightLower.x - cpDelta, rightLower.y); CGPoint leftLower = CGPointMake(self.frame.origin.x, self.frame.origin.y + self.frame.size.height); CGPoint cp6 = CGPointMake(leftLower.x + cpDelta, leftLower.y); CGPoint cp7 = CGPointMake(leftLower.x, leftLower.y - cpDelta); CGPoint cp8 = CGPointMake(leftUpper.x, leftUpper.y + cpDelta); UIBezierPath *square = [UIBezierPath bezierPath]; [square moveToPoint:leftUpper]; [square addCurveToPoint:rightUpper controlPoint1:cp1 controlPoint2:cp2]; [square addCurveToPoint:rightLower controlPoint1:cp3 controlPoint2:cp4]; [square addCurveToPoint:leftLower controlPoint1:cp5 controlPoint2:cp6]; [square addCurveToPoint:leftUpper controlPoint1:cp7 controlPoint2:cp8]; [square closePath]; square.lineCapStyle = kCGLineCapRound; return square; } -(void)toggleShape { if (self.isExpandedPolygon) { [self restre]; }else{ CABasicAnimation *expansionAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; expansionAnimation.fromValue = (__bridge id)(self.circlePath.CGPath); expansionAnimation.toValue = (__bridge id)(self.polyPath.CGPath); expansionAnimation.duration = 0.5; expansionAnimation.fillMode = kCAFillModeForwards; expansionAnimation.removedOnCompletion = NO; [self addAnimation:expansionAnimation forKey:@"Expansion"]; self.isExpandedPolygon = YES; } } -(void)restre { CABasicAnimation *contractionAnimation = [CABasicAnimation animationWithKeyPath:@"path"]; contractionAnimation.fromValue = (__bridge id)(self.polyPath.CGPath); contractionAnimation.toValue = (__bridge id)(self.circlePath.CGPath); contractionAnimation.duration = 0.5; contractionAnimation.fillMode = kCAFillModeForwards; contractionAnimation.removedOnCompletion = NO; [self addAnimation:contractionAnimation forKey:@"Contraction"]; self.isExpandedPolygon = NO; } 

Desde el controller de vista, creo una instancia de esta capa y la agrego a la capa de una vista simple, luego hago las animaciones con un button,

 #import "ViewController.h" #import "RDPolyCircle.h" @interface ViewController () @property (weak, nonatomic) IBOutlet UIView *circleView; // a plain UIView 150 x 150 centenetworking in the superview @property (strong,nonatomic) RDPolyCircle *shapeLayer; @end @implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; // currently isPointUp and numberOfSides are not implemented (the shape created has numberOfSides=4 and isPointUp=NO) // isInitiallyCircle is implemented self.shapeLayer = [[RDPolyCircle alloc] initWithFrame:self.circleView.bounds numberOfSides: 4 isPointUp:NO isInitiallyCircle:YES]; [self.circleView.layer addSublayer:self.shapeLayer]; } - (IBAction)toggleShape:(UIButton *)sender { [self.shapeLayer toggleShape]; } 

El proyecto se puede encontrar aquí, http://jmp.sh/iK3kuVs .