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;
}
}
}