Comment créer une fonction d'accélération personnalisée avec Core Animation?

115

J'anime un CALayerlong a CGPath(QuadCurve) assez bien sous iOS. Mais j'aimerais utiliser une fonction d'assouplissement plus intéressante que les quelques-unes fournies par Apple (EaseIn / EaseOut, etc.). Par exemple, un rebond ou une fonction élastique.

Ces choses sont possibles avec MediaTimingFunction (bezier):

entrez la description de l'image ici

Mais j'aimerais créer des fonctions de chronométrage plus complexes. Le problème est que la synchronisation des médias semble nécessiter un bezier cubique qui n'est pas assez puissant pour créer ces effets:

entrez la description de l'image ici
(source: sparrow-framework.org )

Le code pour créer ce qui précède est assez simple dans d'autres frameworks, ce qui rend cela très frustrant. Notez que les courbes mappent le temps d'entrée au temps de sortie (courbe Tt) et non les courbes temps-position. Par exemple, easOutBounce (T) = t renvoie un nouveau t . Ensuite, ce t est utilisé pour tracer le mouvement (ou toute propriété à animer).

Alors, j'aimerais créer une coutume complexe CAMediaTimingFunctionmais je ne sais pas comment faire ça, ou si c'est même possible? Existe-t-il des alternatives?

ÉDITER:

Voici un exemple concret en étapes. Très éducatif :)

  1. Je veux animer un objet le long d'une ligne allant du point a au point b , mais je veux qu'il "rebondisse" son mouvement le long de la ligne en utilisant la courbe easOutBounce ci-dessus. Cela signifie qu'il suivra la ligne exacte de a à b , mais accélérera et décélérera d'une manière plus complexe que ce qui est possible en utilisant la fonction CAMediaTimingFunction actuelle basée sur Bezier.

  2. Permet de faire de cette ligne tout mouvement de courbe arbitraire spécifié avec CGPath. Il doit toujours se déplacer le long de cette courbe, mais il doit accélérer et décélérer de la même manière que dans l'exemple de ligne.

En théorie, je pense que cela devrait fonctionner comme ceci:

Décrivons la courbe de mouvement comme un mouvement d' animation d'image clé (t) = p , où t est le temps [0..1], p est la position calculée au temps t . Ainsi, move (0) renvoie la position au début de la courbe, déplace (0,5) le milieu exact et déplace (1) à la fin. Utiliser une fonction de synchronisation time (T) = t pour fournir les valeurs t pour le mouvement devrait me donner ce que je veux. Pour un effet de rebond, la fonction de synchronisation doit renvoyer les mêmes valeurs t pour le temps (0,8) et le temps (0,8)(juste un exemple). Remplacez simplement la fonction de synchronisation pour obtenir un effet différent.

(Oui, il est possible de faire rebondir les lignes en créant et en joignant quatre segments de ligne qui vont et viennent, mais cela ne devrait pas être nécessaire. Après tout, c'est juste une simple fonction linéaire qui mappe les valeurs de temps aux positions.)

J'espère avoir un sens ici.

Martin Wickman
la source
sachez que (comme c'est souvent le cas sur SO), cette question très ancienne a maintenant des réponses très obsolètes .. assurez-vous de faire défiler les réponses actuelles. (Très notable: cubic-bezier.com !)
Fattie

Réponses:

48

J'ai trouvé ça:

Cocoa with Love - Courbes d'accélération paramétriques dans Core Animation

Mais je pense que cela peut être rendu un peu plus simple et plus lisible en utilisant des blocs. Nous pouvons donc définir une catégorie sur CAKeyframeAnimation qui ressemble à ceci:

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

Maintenant, l'utilisation ressemblera à ceci:

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

Je sais que ce n'est peut-être pas aussi simple que ce que vous vouliez, mais c'est un début.

Jesse Crossen
la source
1
J'ai pris la réponse de Jesse Crossen et je l'ai un peu développé. Vous pouvez l'utiliser pour animer des CGPoints, et CGSize par exemple. À partir d'iOS 7, vous pouvez également utiliser des fonctions de temps arbitraires avec des animations UIView. Vous pouvez consulter les résultats sur github.com/jjackson26/JMJParametricAnimation
JJ Jackson
@JJJackson Grande collection d'animations! Merci de les avoir
récupérés
33

À partir d'iOS 10, il est devenu possible de créer plus facilement une fonction de synchronisation personnalisée en utilisant deux nouveaux objets de synchronisation.

1) UICubicTimingParameters permet de définir la courbe de Bézier cubique comme fonction d' accélération .

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)

ou simplement en utilisant des points de contrôle lors de l'initialisation de l'animateur

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) 

Ce service génial va vous aider à choisir des points de contrôle pour vos courbes.

2) UISpringTimingParameters permet aux développeurs de manipuler le rapport d'amortissement , la masse , la rigidité et la vitesse initiale pour créer le comportement de ressort souhaité.

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)

Le paramètre de durée est toujours présenté dans Animator, mais sera ignoré pour la synchronisation du printemps.

Si ces deux options ne suffisent pas, vous pouvez également implémenter votre propre courbe de synchronisation en confirmant au protocole UITimingCurveProvider .

Plus de détails, comment créer des animations avec différents paramètres de synchronisation, vous pouvez trouver dans la documentation .

Veuillez également consulter la présentation Advances in UIKit Animations and Transitions de la WWDC 2016.

Olga Konoreva
la source
Vous pouvez trouver des exemples d'utilisation des fonctions de minutage dans le référentiel iOS10-animations-demo .
Olga Konoreva
Pouvez-vous s'il vous plaît élaborer sur "Si ces deux options ne suffisent pas, vous pouvez également implémenter votre propre courbe de temps en confirmant au protocole UITimingCurveProvider." ?
Christian Schnorr
Impressionnant! Et la CoreAnimationversion de UISpringTimingParameters( CASpringAnimation) est prise en charge vers iOS 9!
Rhythmic Fistman
@OlgaKonoreva, le service cubic-bezier.com est absolument sans prix et merveilleux . J'espère que vous êtes toujours un utilisateur SO - vous envoyer une prime de gros cul pour dire merci :)
Fattie
@ChristianSchnorr Je pense que ce à quoi la réponse échappe est la capacité de a UITimingCurveProvidernon seulement de représenter une animation cubique ou de ressort, mais aussi de représenter une animation qui combine à la fois cubique et ressort via UITimingCurveType.composed.
David James
14

Un moyen de créer une fonction de minutage personnalisée consiste à utiliser la méthode d'usine functionWithControlPoints :::: dans CAMediaTimingFunction (il existe également une méthode initWithControlPoints :::: init correspondante). Cela crée une courbe de Bézier pour votre fonction de chronométrage. Ce n'est pas une courbe arbitraire, mais les courbes de Bézier sont très puissantes et flexibles. Il faut un peu de pratique pour maîtriser les points de contrôle. Un conseil: la plupart des programmes de dessin peuvent créer des courbes de Bézier. Jouer avec ceux-ci vous donnera un retour visuel sur la courbe que vous représentez avec les points de contrôle.

Ce lien pointe vers la documentation d'Apple. Il existe une section courte mais utile sur la façon dont les fonctions de pré-construction sont construites à partir de courbes.

Edit: Le code suivant montre une simple animation de rebond. Pour ce faire, j'ai créé une fonction de synchronisation composée ( valeurs et propriétés de synchronisation NSArray) et donné à chaque segment de l'animation une durée différente ( propriété keytimes ). De cette façon, vous pouvez composer des courbes de Bézier pour composer un timing plus sophistiqué pour les animations. C'est un bon article sur ce type d'animations avec un bon exemple de code.

- (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];

}
faible
la source
Oui c'est vrai. Vous pouvez combiner plusieurs fonctions de minutage à l'aide des valeurs et des propriétés timingFunctions de CAKeyframeAnimation pour obtenir ce que vous voulez. Je vais élaborer un exemple et mettre un peu le code.
fsaint
Merci pour vos efforts et vos efforts :) Cette approche peut fonctionner en théorie, mais elle doit être personnalisée pour chaque utilisation. Je recherche une solution générale qui fonctionne pour toutes les animations en tant que fonction de minutage. Pour être honnête, cela n'a pas l'air si bien d'assembler les lignes et les courbes. L'exemple «jack in a box» en souffre également.
Martin Wickman
1
Vous pouvez spécifier un CGPathRef au lieu d'un tableau de valeurs pour une animation d'image clé, mais à la fin Felz est correct - CAKeyframeAnimation et timingFunctions vous donneront tout ce dont vous avez besoin. Je ne sais pas pourquoi vous pensez que ce n'est pas une «solution générale» qui doit être «personnalisée pour chaque utilisation». Vous créez l'animation d'image clé une fois dans une méthode d'usine ou quelque chose du genre et vous pouvez ensuite ajouter cette animation à n'importe quel calque encore et encore autant de fois que vous le souhaitez. Pourquoi aurait-il besoin d'être personnalisé pour chaque utilisation? Il me semble que ce n'est pas différent de votre idée de la façon dont les fonctions de synchronisation devraient fonctionner.
Matt Long
1
Oh les gars, ils ont maintenant 4 ans de retard, mais si functionWithControlPoints :::: peut totalement faire des animations rebondissantes / élastiques. Utilisez-le en conjonction avec cubic-bezier.com/#.6,1.87,.63,.74
Matt Foley
10

Je ne sais pas si vous cherchez toujours, mais PRTween semble assez impressionnant en termes de capacité à aller au-delà de ce que Core Animation vous offre prêt à l'emploi , notamment des fonctions de synchronisation personnalisées. Il est également livré avec un grand nombre, sinon la totalité, des courbes d'assouplissement populaires fournies par divers frameworks Web.

CIFiltre
la source
2

Une implémentation de version rapide est TFAnimation . La démo est une animation de courbe de péché . UtilisezTFBasicAnimation juste comme CABasicAnimationexcept assign timeFunctionavec un bloc autre que timingFunction.

Le point clé est la sous - classe CAKeyframeAnimationet de calculer la position des cadres par timeFunctiondans l' 1 / 60fpsintervalle s .Après tout ajouter toute la valeur calculée à valuesdes CAKeyframeAnimationet les temps par intervalle keyTimestrop.

Xingxing
la source
2

J'ai créé une approche basée sur des blocs, qui génère un groupe d'animation, avec plusieurs animations.

Chaque animation, par propriété, peut utiliser 1 des 33 courbes paramétriques différentes, une fonction de minutage de déclin avec une vitesse initiale ou un ressort personnalisé configuré selon vos besoins.

Une fois le groupe généré, il est mis en cache sur la vue et peut être déclenché à l'aide d'une animationKey, avec ou sans l'animation. Une fois déclenchée, l'animation est synchronisée en fonction des valeurs de la couche de présentation et appliquée en conséquence.

Le cadre peut être trouvé ici FlightAnimator

Voici un exemple ci-dessous:

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

Pour déclencher l'animation

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
AntonTheDev
la source