web-dev-qa-db-fra.com

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

J'anime un CALayer le long d'un CGPath (QuadCurve) plutôt bien sous iOS. Mais j'aimerais utiliser une fonction d'accélération plus intéressante que les rares fournies by Apple (EaseIn/EaseOut, etc.). Par exemple, une fonction de rebond ou élastique.

Ces choses sont possibles à faire avec MediaTimingFunction (bezier):

enter image description here

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

enter image description here
(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 sur le temps de sortie (courbe T-t) et non sur les courbes de position dans le temps. Par exemple, aisanceOutBounce (T) = t renvoie un nouveau t . Ensuite, cela t est utilisé pour tracer le mouvement (ou toute propriété que nous devrions animer).

Donc, j'aimerais créer une personnalisation complexe CAMediaTimingFunction mais je ne sais pas comment faire cela, ou si c'est même possible? Y a-t-il des alternatives?

EDIT:

Voici un exemple concret dans les étapes. Très pédagogique :)

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

  2. Faisons de cette ligne tout mouvement de courbe arbitraire spécifié avec CGPath. Il devrait toujours suivre cette courbe, mais il devrait 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 une animation d’image clé move (t) = p , où t est le temps [0..1] , p est la position calculée à l'instant t . Donc move (0) retourne la position au début de la courbe, déplace (0.5) le milieu exact et déplace (1) à la fin. Utilisation d'une fonction de chronométrage time (T) = t pour fournir les valeurs t de move devrait me donner ce que je veux. Pour un effet de rebond, la fonction de chronométrage devrait retourner la même chose t pour time(0.8) et time (0.8) (juste un exemple). Il suffit de remplacer la fonction de minutage pour obtenir un effet différent.

(Oui, il est possible de faire un saut de ligne 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, il ne s'agit que d'une simple fonction linéaire qui mappe le temps valeurs aux positions.)

J'espère que j'ai du sens ici.

110
Martin Wickman

J'ai trouvé ça:

Cacao avec amour - Courbes d’accélération paramétriques dans Core Animation

Mais je pense que cela peut être 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 à quelque chose comme 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.

47
Jesse Crossen

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

1) UICubicTimingParameters permet de définir le cubique courbe de Bézier en tant que 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 vous aidera à choisir des points de contrôle pour vos courbes.

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

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 temps en vérifiant le protocole UITimingCurveProvider.

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

Voir aussi Présentation des avancées dans les animations et transitions UIKit de WWDC 2016.

27
Olga Konoreva

Une méthode pour créer une fonction de minutage personnalisée consiste à utiliser la méthode de fabrique 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.

Le ce lien pointe vers la documentation d'Apple. Il existe une section courte mais utile sur la manière dont les fonctions de pré-génération sont construites à partir de courbes.

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

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

}
13
fsaint

Vous ne savez pas si vous cherchez toujours, mais PRTween semble assez impressionnant pour ce qui est de sa capacité à aller au-delà de ce que Core Animation vous offre, notamment les fonctions de minutage personnalisées. Il contient également de nombreuses courbes, voire la totalité, des courbes d’atténuation populaires proposées par divers frameworks Web.

9
CIFilter

J'ai pris la réponse de Jesse Crossen et l'ai développée un peu. Vous pouvez l'utiliser pour animer des CGPoints, et CGSize par exemple. Depuis iOS 7, vous pouvez également utiliser des fonctions temporelles arbitraires avec les animations UIView.

Vous pouvez consulter les résultats sur https://github.com/jjackson26/JMJParametricAnimation

4
J.J. Jackson

Une implémentation de la version Swift est TFAnimation . La démonstration est une animation sin . Utilisez TFBasicAnimation comme CABasicAnimation sauf assigner timeFunction avec un bloc autre que timingFunction.

Le point clé est la sous-classe CAKeyframeAnimation et calcule la position des images par timeFunction dans 1 / 60fps _ intervalle. Après tout, ajoutez toute la valeur calculée à values de CAKeyframeAnimation et les durées par intervalle à keyTimes.

1
Xingxing

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

Chaque animation, par propriété, peut utiliser l’une des 33 courbes paramétriques différentes, une fonction de temporisation Decay avec vitesse initiale ou un ressort personnalisé configuré selon vos besoins.

Une fois le groupe généré, il est mis en cache dans la vue et peut être déclenché à l'aide d'une clé d'animation, avec ou sans l'animation. Une fois déclenchée, l'animation est synchronisée en conséquence avec les 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)
1
AntonTheDev