objective guide framework developer apple ios caanimation

ios - guide - ¿Cómo hacer una animación de curva/arco con CAAnimation?



objective c documentation (5)

Usando UIBezierPath

(No olvides vincular y luego import QuartzCore , si estás usando iOS 6 o anterior)

Código de ejemplo

Podría usar una animación que siga una ruta, convenientemente, CAKeyframeAnimation admite una CGPath , que se puede obtener de una UIBezierPath . Swift 3

func animate(view : UIView, fromPoint start : CGPoint, toPoint end: CGPoint) { // The animation let animation = CAKeyframeAnimation(keyPath: "position") // Animation''s path let path = UIBezierPath() // Move the "cursor" to the start path.move(to: start) // Calculate the control points let c1 = CGPoint(x: start.x + 64, y: start.y) let c2 = CGPoint(x: end.x, y: end.y - 128) // Draw a curve towards the end, using control points path.addCurve(to: end, controlPoint1: c1, controlPoint2: c2) // Use this path as the animation''s path (casted to CGPath) animation.path = path.cgPath; // The other animations properties animation.fillMode = kCAFillModeForwards animation.isRemovedOnCompletion = false animation.duration = 1.0 animation.timingFunction = CAMediaTimingFunction(name:kCAMediaTimingFunctionEaseIn) // Apply it view.layer.add(animation, forKey:"trash") }

Entendiendo UIBezierPath

Los caminos de Bézier (o las curvas de Bézier , para ser precisos) funcionan exactamente como los que se encuentran en photoshop, fuegos artificiales, bocetos ... Tienen dos "puntos de control", uno para cada vértice. Por ejemplo, la animación que acabo de hacer:

Trabaja el camino más bonito así. Consulte la documentación sobre los detalles , pero son básicamente dos puntos los que "arrastran" el arco hacia una cierta dirección.

Dibujando un camino

Una característica interesante de UIBezierPath es que puedes dibujarlos en la pantalla con CAShapeLayer , lo que te ayuda a visualizar el camino que seguirá.

// Drawing the path let *layer = CAShapeLayer() layer.path = path.cgPath layer.strokeColor = UIColor.black.cgColor layer.lineWidth = 1.0 layer.fillColor = nil self.view.layer.addSublayer(layer)

Mejorando el ejemplo original.

La idea de calcular su propia ruta Bézier es que puede hacer que la animación sea completamente dinámica, por lo tanto, la animación puede cambiar la curva que va a hacer, basada en múltiples factores, en lugar de simplemente codificar como lo hice en el ejemplo, para Por ejemplo, los puntos de control se podrían calcular de la siguiente manera:

// Calculate the control points let factor : CGFloat = 0.5 let deltaX : CGFloat = end.x - start.x let deltaY : CGFloat = end.y - start.y let c1 = CGPoint(x: start.x + deltaX * factor, y: start.y) let c2 = CGPoint(x: end.x , y: end.y - deltaY * factor)

Este último bit de código hace que los puntos sean como la figura anterior, pero en una cantidad variable, con respecto al triángulo que forman los puntos, multiplicado por un factor que sería el equivalente de un valor de "tensión".

Tengo una interfaz de usuario en la que se elimina un elemento, me gustaría imitar el efecto "mover a carpeta" en el correo de iOS. El efecto donde el ícono de la pequeña letra se "lanza" a la carpeta. La mía será botada en un contenedor en su lugar.

Intenté implementarlo usando un CAAnimation en la capa. Por lo que puedo leer en las documentaciones, debería poder establecer un valor de byValue y toValue y CAAnimation debería interpolar los valores. Estoy buscando hacer una pequeña curva, por lo que el elemento atraviesa un punto un poco por encima ya la izquierda de la posición inicial de los elementos.

CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position"]; [animation setDuration:2.0f]; [animation setRemovedOnCompletion:NO]; [animation setFillMode:kCAFillModeForwards]; [animation setTimingFunction:[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]]; [animation setFromValue:[NSValue valueWithCGPoint:fromPoint]]; [animation setByValue:[NSValue valueWithCGPoint:byPoint]]; [animation setToValue:[NSValue valueWithCGPoint:CGPointMake(512.0f, 800.0f)]]; [animation setRepeatCount:1.0];

Jugué con esto durante algún tiempo, pero me parece que Apple significa interpolación lineal. Agregar el valor de byValue no calcula un arco o curva agradable y anima el elemento a través de él.

¿Cómo voy a hacer tal animación?

Gracias por cualquier ayuda prestada.


Descubrí cómo hacerlo. Es realmente posible animar X e Y por separado. Si los anima durante el mismo tiempo (2,0 segundos más abajo) y configura una función de tiempo diferente, hará que se vea como si se moviera en un arco en lugar de una línea recta desde el inicio hasta el final. Para ajustar el arco, tendría que jugar con la configuración de una función de temporización diferente. Sin embargo, no estoy seguro de si CAAnimation admite funciones de sincronización "atractivas"

const CFTimeInterval DURATION = 2.0f; CABasicAnimation* animation = [CABasicAnimation animationWithKeyPath:@"position.y"]; [animation setDuration:DURATION]; [animation setRemovedOnCompletion:NO]; [animation setFillMode:kCAFillModeForwards]; [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionLinear]]; [animation setFromValue:[NSNumber numberWithDouble:400.0]]; [animation setToValue:[NSNumber numberWithDouble:0.0]]; [animation setRepeatCount:1.0]; [animation setDelegate:self]; [myview.layer addAnimation:animation forKey:@"animatePositionY"]; animation = [CABasicAnimation animationWithKeyPath:@"position.x"]; [animation setDuration:DURATION]; [animation setRemovedOnCompletion:NO]; [animation setFillMode:kCAFillModeForwards]; [animation setTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; [animation setFromValue:[NSNumber numberWithDouble:300.0]]; [animation setToValue:[NSNumber numberWithDouble:0.0]]; [animation setRepeatCount:1.0]; [animation setDelegate:self]; [myview.layer addAnimation:animation forKey:@"animatePositionX"];

Editar:

Debería ser posible cambiar la función de tiempo utilizando https://developer.apple.com/library/ios/#documentation/Cocoa/Reference/CAMediaTimingFunction_class/Introduction/Introduction.html (CAMediaTimingFunction inited by functionWithControlPoints ::::) Es un " curva de Bézier cúbico ". Estoy seguro de que Google tiene respuestas allí. http://en.wikipedia.org/wiki/B%C3%A9zier_curve#Cubic_B.C3.A9zier_curves :-)


Es absolutamente correcto que animar la posición con un CABasicAnimation hace que vaya en línea recta. Hay otra clase llamada CAKeyframeAnimation para hacer animaciones más avanzadas.

Una matriz de values

En lugar de toValue , fromValue y byValue para animaciones básicas, puede utilizar una matriz de values o una path completa para determinar los valores en el camino. Si desea animar la posición primero al lado y luego hacia abajo, puede pasar una matriz de 3 posiciones (inicio, intermedio, final).

CGPoint startPoint = myView.layer.position; CGPoint endPoint = CGPointMake(512.0f, 800.0f); // or any point CGPoint midPoint = CGPointMake(endPoint.x, startPoint.y); CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:@"position"]; move.values = @[[NSValue valueWithCGPoint:startPoint], [NSValue valueWithCGPoint:midPoint], [NSValue valueWithCGPoint:endPoint]]; move.duration = 2.0f; myView.layer.position = endPoint; // instead of removeOnCompletion [myView.layer addAnimation:move forKey:@"move the view"];

Si hace esto, notará que la vista se mueve desde el punto de inicio en una línea recta hasta el punto medio y en otra línea recta hasta el punto final. La parte que falta para que sea de principio a fin a través del punto medio es cambiar el modo de calculationMode de la animación.

move.calculationMode = kCAAnimationCubic;

Puede controlarlo cambiando las tensionValues , continuityValues y biasValues . Si desea un control más preciso, puede definir su propia ruta en lugar de la matriz de values .

Un path a seguir

Puede crear cualquier ruta y especificar que la propiedad debe seguir eso. Aquí estoy usando un arco simple

CGMutablePathRef path = CGPathCreateMutable(); CGPathMoveToPoint(path, NULL, startPoint.x, startPoint.y); CGPathAddCurveToPoint(path, NULL, controlPoint1.x, controlPoint1.y, controlPoint2.x, controlPoint2.y, endPoint.x, endPoint.y); CAKeyframeAnimation *move = [CAKeyframeAnimation animationWithKeyPath:@"position"]; move.path = path; move.duration = 2.0f; myView.layer.position = endPoint; // instead of removeOnCompletion [myView.layer addAnimation:move forKey:@"move the view"];


Hace unos días tuve una pregunta similar y la implementé con el temporizador, como dice Brad , pero no con NSTimer . CADisplayLink : es el temporizador que se debe usar para este propósito, ya que se sincroniza con el frameRate de la aplicación y proporciona una animación más suave y natural. Puedes ver mi implementación en mi respuesta here . Esta técnica realmente da mucho más control sobre la animación que CAAnimation , y no es mucho más complicada. CAAnimation no puede dibujar nada, ya que ni siquiera vuelve a dibujar la vista. Solo mueve, deforma y desvanece lo que ya está dibujado.


Intenta esto, resolverá tu problema definitivamente, lo he usado en mi proyecto:

UILabel *label = [[[UILabel alloc] initWithFrame:CGRectMake(10, 126, 320, 24)] autorelease]; label.text = @"Animate image into trash button"; label.textAlignment = UITextAlignmentCenter; [label sizeToFit]; [scrollView addSubview:label]; UIImageView *icon = [[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"carmodel.png"]] autorelease]; icon.center = CGPointMake(290, 150); icon.tag = ButtonActionsBehaviorTypeAnimateTrash; [scrollView addSubview:icon]; UIButton *button = [UIButton buttonWithType:UIButtonTypeRoundedRect]; button.center = CGPointMake(40, 200); button.tag = ButtonActionsBehaviorTypeAnimateTrash; [button setTitle:@"Delete Icon" forState:UIControlStateNormal]; [button sizeToFit]; [button addTarget:self action:@selector(buttonClicked:) forControlEvents:UIControlEventTouchUpInside]; [scrollView addSubview:button]; [scrollView bringSubviewToFront:icon]; - (void)buttonClicked:(id)sender { UIView *senderView = (UIView*)sender; if (![senderView isKindOfClass:[UIView class]]) return; switch (senderView.tag) { case ButtonActionsBehaviorTypeExpand: { CABasicAnimation *anim = [CABasicAnimation animationWithKeyPath:@"transform"]; anim.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseInEaseOut]; anim.duration = 0.125; anim.repeatCount = 1; anim.autoreverses = YES; anim.removedOnCompletion = YES; anim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(1.2, 1.2, 1.0)]; [senderView.layer addAnimation:anim forKey:nil]; break; } case ButtonActionsBehaviorTypeAnimateTrash: { UIView *icon = nil; for (UIView *theview in senderView.superview.subviews) { if (theview.tag != ButtonActionsBehaviorTypeAnimateTrash) continue; if ([theview isKindOfClass:[UIImageView class]]) { icon = theview; break; } } if (!icon) return; UIBezierPath *movePath = [UIBezierPath bezierPath]; [movePath moveToPoint:icon.center]; [movePath addQuadCurveToPoint:senderView.center controlPoint:CGPointMake(senderView.center.x, icon.center.y)]; CAKeyframeAnimation *moveAnim = [CAKeyframeAnimation animationWithKeyPath:@"position"]; moveAnim.path = movePath.CGPath; moveAnim.removedOnCompletion = YES; CABasicAnimation *scaleAnim = [CABasicAnimation animationWithKeyPath:@"transform"]; scaleAnim.fromValue = [NSValue valueWithCATransform3D:CATransform3DIdentity]; scaleAnim.toValue = [NSValue valueWithCATransform3D:CATransform3DMakeScale(0.1, 0.1, 1.0)]; scaleAnim.removedOnCompletion = YES; CABasicAnimation *opacityAnim = [CABasicAnimation animationWithKeyPath:@"alpha"]; opacityAnim.fromValue = [NSNumber numberWithFloat:1.0]; opacityAnim.toValue = [NSNumber numberWithFloat:0.1]; opacityAnim.removedOnCompletion = YES; CAAnimationGroup *animGroup = [CAAnimationGroup animation]; animGroup.animations = [NSArray arrayWithObjects:moveAnim, scaleAnim, opacityAnim, nil]; animGroup.duration = 0.5; [icon.layer addAnimation:animGroup forKey:nil]; break; } } }