¿Cómo crear una función de aceleración personalizada con Core Animation?

115

Estoy animando a lo CALayerlargo de CGPath(QuadCurve) bastante bien en iOS. Pero me gustaría usar una función de aceleración más interesante que las pocas proporcionadas por Apple (EaseIn / EaseOut, etc.). Por ejemplo, una función de rebote o elástica.

Estas cosas se pueden hacer con MediaTimingFunction (bezier):

ingrese la descripción de la imagen aquí

Pero me gustaría crear funciones de sincronización que sean más complejas. El problema es que el tiempo de los medios parece requerir un bezier cúbico que no es lo suficientemente poderoso como para crear estos efectos:

ingrese la descripción de la imagen aquí
(fuente: sparrow-framework.org )

El código para crear lo anterior es bastante simple en otros marcos, lo que hace que esto sea muy frustrante. Tenga en cuenta que las curvas están mapeando el tiempo de entrada al tiempo de salida (curva Tt) y no las curvas de tiempo-posición. Por ejemplo, easyOutBounce (T) = t devuelve una nueva t . Entonces esa t se usa para trazar el movimiento (o cualquier propiedad que debamos animar).

Entonces, me gustaría crear una costumbre compleja CAMediaTimingFunctionpero no tengo ni idea de cómo hacerlo, o si es posible. ¿Existen alternativas?

EDITAR:

Aquí hay un ejemplo concreto en pasos. Muy educativo :)

  1. Quiero animar un objeto a lo largo de una línea desde el punto a al b , pero quiero que "rebote" su movimiento a lo largo de la línea usando la curva easyOutBounce anterior. Esto significa que seguirá la línea exacta de a a b , pero acelerará y desacelerará de una manera más compleja de lo que es posible usando la actual función CAMediaTimingFunction basada en bezier.

  2. Hagamos de esa línea cualquier movimiento de curva arbitrario especificado con CGPath. Aún debería moverse a lo largo de esa curva, pero debería acelerar y desacelerar de la misma manera que en el ejemplo de línea.

En teoría, creo que debería funcionar así:

Vamos a describir la curva de movimiento como un movimiento de animación de fotogramas clave (t) = p , donde t es el tiempo [0..1], p es la posición calculada en el tiempo t . Así que move (0) devuelve la posición al inicio de la curva, mueve (0.5) el centro exacto y mueve (1) al final. Usar una función de temporización time (T) = t para proporcionar los valores de t para move debería darme lo que quiero. Para un efecto de rebote, la función de sincronización debe devolver los mismos valores t para el tiempo (0.8) y el tiempo (0.8)(solo un ejemplo). Simplemente reemplace la función de sincronización para obtener un efecto diferente.

(Sí, es posible hacer un rebote de línea creando y uniendo cuatro segmentos de línea que van y vienen, pero eso no debería ser necesario. Después de todo, es solo una función lineal simple que asigna valores de tiempo a posiciones).

Espero tener sentido aquí.

Martin Wickman
fuente
tenga en cuenta que (como suele ser el caso en SO), esta pregunta muy antigua ahora tiene respuestas muy desactualizadas ... asegúrese de desplazarse hacia abajo hasta las asombrosas respuestas actuales. (Muy notable: cubic-bezier.com !)
Fattie

Respuestas:

48

Encontré esto:

Cocoa with Love: curvas de aceleración paramétricas en Core Animation

Pero creo que se puede hacer un poco más simple y más legible usando bloques. Entonces podemos definir una categoría en CAKeyframeAnimation que se parece a esto:

CAKeyframeAnimation + Parametric.h:

// this should be a function that takes a time value between 
//  0.0 and 1.0 (where 0.0 is the beginning of the animation
//  and 1.0 is the end) and returns a scale factor where 0.0
//  would produce the starting value and 1.0 would produce the
//  ending value
typedef double (^KeyframeParametricBlock)(double);

@interface CAKeyframeAnimation (Parametric)

+ (id)animationWithKeyPath:(NSString *)path 
      function:(KeyframeParametricBlock)block
      fromValue:(double)fromValue
      toValue:(double)toValue;

CAKeyframeAnimation + Parametric.m:

@implementation CAKeyframeAnimation (Parametric)

+ (id)animationWithKeyPath:(NSString *)path 
      function:(KeyframeParametricBlock)block
      fromValue:(double)fromValue
      toValue:(double)toValue {
  // get a keyframe animation to set up
  CAKeyframeAnimation *animation = 
    [CAKeyframeAnimation animationWithKeyPath:path];
  // break the time into steps
  //  (the more steps, the smoother the animation)
  NSUInteger steps = 100;
  NSMutableArray *values = [NSMutableArray arrayWithCapacity:steps];
  double time = 0.0;
  double timeStep = 1.0 / (double)(steps - 1);
  for(NSUInteger i = 0; i < steps; i++) {
    double value = fromValue + (block(time) * (toValue - fromValue));
    [values addObject:[NSNumber numberWithDouble:value]];
    time += timeStep;
  }
  // we want linear animation between keyframes, with equal time steps
  animation.calculationMode = kCAAnimationLinear;
  // set keyframes and we're done
  [animation setValues:values];
  return(animation);
}

@end

Ahora el uso se verá así:

// define a parametric function
KeyframeParametricBlock function = ^double(double time) {
  return(1.0 - pow((1.0 - time), 2.0));
};

if (layer) {
  [CATransaction begin];
    [CATransaction 
      setValue:[NSNumber numberWithFloat:2.5]
      forKey:kCATransactionAnimationDuration];

    // make an animation
    CAAnimation *drop = [CAKeyframeAnimation 
      animationWithKeyPath:@"position.y"
      function:function fromValue:30.0 toValue:450.0];
    // use it
    [layer addAnimation:drop forKey:@"position"];

  [CATransaction commit];
}

Sé que puede que no sea tan simple como lo que querías, pero es un comienzo.

Jesse Crossen
fuente
1
Tomé la respuesta de Jesse Crossen y la amplié un poco. Puede usarlo para animar CGPoints y CGSize, por ejemplo. A partir de iOS 7, también puede usar funciones de tiempo arbitrarias con animaciones UIView. Puede consultar los resultados en github.com/jjackson26/JMJParametricAnimation
JJ Jackson
@JJJackson ¡Gran colección de animaciones! Gracias por recopilarlos
Codey
33

Desde iOS 10 fue posible crear una función de sincronización personalizada más fácilmente utilizando dos nuevos objetos de sincronización.

1) UICubicTimingParameters permite definir la curva de Bézier cúbica como función de aceleración.

let cubicTimingParameters = UICubicTimingParameters(controlPoint1: CGPoint(x: 0.25, y: 0.1), controlPoint2: CGPoint(x: 0.25, y: 1))
let animator = UIViewPropertyAnimator(duration: 0.3, timingParameters: cubicTimingParameters)

o simplemente usando puntos de control en la inicialización del animador

let controlPoint1 = CGPoint(x: 0.25, y: 0.1)
let controlPoint2 = CGPoint(x: 0.25, y: 1)
let animator = UIViewPropertyAnimator(duration: 0.3, controlPoint1: controlPoint1, controlPoint2: controlPoint2) 

Este increíble servicio te ayudará a elegir puntos de control para tus curvas.

2) UISpringTimingParameters permite a los desarrolladores manipular la relación de amortiguación , la masa , la rigidez y la velocidad inicial para crear el comportamiento de resorte deseado.

let velocity = CGVector(dx: 1, dy: 0)
let springParameters = UISpringTimingParameters(mass: 1.8, stiffness: 330, damping: 33, initialVelocity: velocity)
let springAnimator = UIViewPropertyAnimator(duration: 0.0, timingParameters: springParameters)

El parámetro de duración todavía se presenta en Animator, pero se ignorará para el tiempo de primavera.

Si estas dos opciones no son suficientes, también puede implementar su propia curva de tiempo confirmando el protocolo UITimingCurveProvider .

Más detalles, cómo crear animaciones con diferentes parámetros de tiempo, puede encontrarlo en la documentación .

Además, consulte la presentación Avances en animaciones y transiciones de UIKit de WWDC 2016.

Olga Konoreva
fuente
Puede encontrar ejemplos del uso de las funciones de sincronización en el repositorio iOS10-animations-demo .
Olga Konoreva
¿Podría por favor explicar "Si estas dos opciones no son suficientes, también puede implementar su propia curva de tiempo confirmando el protocolo UITimingCurveProvider"? ?
Christian Schnorr
¡Increíble! ¡Y la CoreAnimationversión de UISpringTimingParameters( CASpringAnimation) es compatible con iOS 9!
Rhythmic Fistman
@OlgaKonoreva, el servicio de cubic-bezier.com NO TIENE PRECIO Y ES MARAVILLOSO . Espero que sigas siendo un usuario SO, enviándote una generosa recompensa para agradecer :)
Fattie
@ChristianSchnorr Creo que la respuesta es eludir la capacidad de UITimingCurveProvidera no solo representar una animación cúbica o de resorte, sino también para representar una animación que combina tanto la vía cúbica como la de resorte UITimingCurveType.composed.
David James
14

Una manera de crear una función de temporización de encargo es mediante el uso de la functionWithControlPoints :::: método de fábrica en CAMediaTimingFunction (hay un método init correspondientes initWithControlPoints :::: también). Lo que esto hace es crear una curva de Bézier para su función de sincronización. No es una curva arbitraria, pero las curvas de Bézier son muy potentes y flexibles. Se necesita un poco de práctica para dominar los puntos de control. Un consejo: la mayoría de los programas de dibujo pueden crear curvas de Bézier. Jugar con ellos te dará una retroalimentación visual sobre la curva que estás representando con los puntos de control.

Los este enlace por puntos a la documentación de Apple. Hay una sección breve pero útil sobre cómo se construyen las funciones de preconstrucción a partir de curvas.

Editar: el siguiente código muestra una animación de rebote simple. Para hacerlo, creé una función de temporización compuesta ( valores y propiedades de temporización NSArray) y le di a cada segmento de la animación una duración de tiempo diferente ( propiedad de tiempos clave ). De esta forma, puede componer curvas de Bézier para componer tiempos más sofisticados para animaciones. Este es un buen artículo sobre este tipo de animaciones con un buen código de muestra.

- (void)viewDidLoad {
    UIView *v = [[UIView alloc] initWithFrame:CGRectMake(0.0, 0.0, 50.0, 50.0)];

    v.backgroundColor = [UIColor redColor];
    CGFloat y = self.view.bounds.size.height;
    v.center = CGPointMake(self.view.bounds.size.width/2.0, 50.0/2.0);
    [self.view addSubview:v];

    //[CATransaction begin];

    CAKeyframeAnimation * animation; 
    animation = [CAKeyframeAnimation animationWithKeyPath:@"position.y"]; 
    animation.duration = 3.0; 
    animation.removedOnCompletion = NO;
    animation.fillMode = kCAFillModeForwards;

    NSMutableArray *values = [NSMutableArray array];
    NSMutableArray *timings = [NSMutableArray array];
    NSMutableArray *keytimes = [NSMutableArray array];

    //Start
    [values addObject:[NSNumber numberWithFloat:25.0]];
    [timings addObject:GetTiming(kCAMediaTimingFunctionEaseIn)];
    [keytimes addObject:[NSNumber numberWithFloat:0.0]];


    //Drop down
    [values addObject:[NSNumber numberWithFloat:y]];
    [timings addObject:GetTiming(kCAMediaTimingFunctionEaseOut)];
    [keytimes addObject:[NSNumber numberWithFloat:0.6]];


    // bounce up
    [values addObject:[NSNumber numberWithFloat:0.7 * y]];
    [timings addObject:GetTiming(kCAMediaTimingFunctionEaseIn)];
    [keytimes addObject:[NSNumber numberWithFloat:0.8]];


    // fihish down
    [values addObject:[NSNumber numberWithFloat:y]];
    [keytimes addObject:[NSNumber numberWithFloat:1.0]];
    //[timings addObject:GetTiming(kCAMediaTimingFunctionEaseIn)];



    animation.values = values;
    animation.timingFunctions = timings;
    animation.keyTimes = keytimes;

    [v.layer addAnimation:animation forKey:nil];   

    //[CATransaction commit];

}
fsaint
fuente
Si eso es verdad. Puede combinar varias funciones de sincronización utilizando los valores y las propiedades de función de sincronización de CAKeyframeAnimation para lograr lo que desea. Resolveré un ejemplo y pondré el código en un momento.
fsaint
Gracias por sus esfuerzos y puntos por intentarlo :) Ese enfoque puede funcionar en teoría, pero debe personalizarse para cada uso. Estoy buscando una solución general que funcione para todas las animaciones como función de sincronización. Para ser honesto, no se ve tan bien uniendo líneas y curvas. El ejemplo del "gato en una caja" también sufre de esto.
Martin Wickman
1
Puede especificar un CGPathRef en lugar de una matriz de valores para una animación de fotogramas clave, pero al final Felz es correcto: CAKeyframeAnimation y timingFunctions le darán todo lo que necesita. No estoy seguro de por qué cree que esa no es una "solución general" que tiene que ser "personalizada para cada uso". Usted crea la animación de fotogramas clave una vez en un método de fábrica o algo y luego puede agregar esa animación a cualquier capa una y otra vez tantas veces como desee. ¿Por qué debería personalizarse para cada uso? Me parece que no es diferente de su noción de cómo deberían funcionar las funciones de sincronización.
Matt Long
1
Oh chicos, esto ahora tiene 4 años de retraso, pero si functionWithControlPoints :::: puede hacer animaciones totalmente hinchables / elásticas. Úselo junto con cubic-bezier.com/#.6,1.87,.63,.74
Matt Foley
10

No estoy seguro de si todavía está buscando, pero PRTween se ve bastante impresionante en términos de su capacidad para ir más allá de lo que Core Animation le ofrece, en particular, las funciones de sincronización personalizadas. También viene empaquetado con muchas, si no todas, las populares curvas de flexibilización que ofrecen varios marcos web.

CIFilter
fuente
2

Una implementación rápida de la versión es TFAnimation . La demostración es una animación de curva de pecado . ÚseloTFBasicAnimation como CABasicAnimationexcepto asignar timeFunctioncon un bloque que no sea timingFunction.

El punto clave es la subclase CAKeyframeAnimationy calcular la posición de los fotogramas timeFunctionen 1 / 60fpsel intervalo de s. Después de todo, agregue todo el valor calculado a valuesde CAKeyframeAnimationy los tiempos por intervalo a keyTimestambién.

Xingxing
fuente
2

Creé un enfoque basado en bloques, que genera un grupo de animación, con múltiples animaciones.

Cada animación, por propiedad, puede usar 1 de 33 curvas paramétricas diferentes, una función de tiempo de decaimiento con velocidad inicial o un resorte personalizado configurado según sus necesidades.

Una vez que se genera el grupo, se almacena en caché en la Vista y se puede activar usando una AnimationKey, con o sin la animación. Una vez activada, la animación se sincroniza en consecuencia con los valores de la capa de presentación y se aplica en consecuencia.

El marco se puede encontrar aquí FlightAnimator

A continuación se muestra un ejemplo:

struct AnimationKeys {
    static let StageOneAnimationKey  = "StageOneAnimationKey"
    static let StageTwoAnimationKey  = "StageTwoAnimationKey"
}

...

view.registerAnimation(forKey: AnimationKeys.StageOneAnimationKey, maker:  { (maker) in

    maker.animateBounds(toValue: newBounds,
                     duration: 0.5,
                     easingFunction: .EaseOutCubic)

    maker.animatePosition(toValue: newPosition,
                     duration: 0.5,
                     easingFunction: .EaseOutCubic)

    maker.triggerTimedAnimation(forKey: AnimationKeys.StageTwoAnimationKey,
                           onView: self.secondaryView,
                           atProgress: 0.5, 
                           maker: { (makerStageTwo) in

        makerStageTwo.animateBounds(withDuration: 0.5,
                             easingFunction: .EaseOutCubic,
                             toValue: newSecondaryBounds)

        makerStageTwo.animatePosition(withDuration: 0.5,
                             easingFunction: .EaseOutCubic,
                             toValue: newSecondaryCenter)
     })                    
})

Para activar la animación

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
AntonTheDev
fuente