Come creare una funzione di andamento personalizzata con Core Animation?


115

Sto animando abbastanza bene un CALayerlungo a CGPath(QuadCurve) in iOS. Ma mi piacerebbe usare una funzione di facilitazione più interessante delle poche fornite da Apple (EaseIn / EaseOut ecc.). Ad esempio, una funzione di rimbalzo o elastica.

Queste cose sono possibili con MediaTimingFunction (bezier):

inserisci qui la descrizione dell'immagine

Ma vorrei creare funzioni di temporizzazione più complesse. Il problema è che il tempismo dei media sembra richiedere un bezier cubico che non è abbastanza potente per creare questi effetti:

inserisci qui la descrizione dell'immagine
(fonte: sparrow-framework.org )

Il codice per creare quanto sopra è abbastanza semplice in altri framework, il che lo rende molto frustrante. Notare che le curve stanno mappando il tempo di input con il tempo di output (curva Tt) e non le curve di posizione-tempo. Ad esempio, easeOutBounce (T) = t restituisce una nuova t . Quindi quella t è usata per tracciare il movimento (o qualunque proprietà dovremmo animare).

Quindi, mi piacerebbe creare un custom complesso CAMediaTimingFunctionma non ho la più pallida idea di come farlo, o se è anche possibile? Esistono alternative?

MODIFICARE:

Ecco un esempio concreto in to steps. Molto educativo :)

  1. Voglio animare un oggetto lungo una linea dal punto a al punto b , ma voglio che "rimbalzi" il suo movimento lungo la linea usando la curva easeOutBounce sopra. Ciò significa che seguirà la linea esatta da a a b , ma accelererà e decelererà in un modo più complesso di quanto sia possibile utilizzando l'attuale CAMediaTimingFunction basata su Bézier.

  2. Consente di rendere quella linea qualsiasi movimento arbitrario della curva specificato con CGPath. Dovrebbe comunque muoversi lungo quella curva, ma dovrebbe accelerare e decelerare allo stesso modo dell'esempio di linea.

In teoria penso che dovrebbe funzionare in questo modo:

Descriviamo la curva di movimento come un'animazione del fotogramma chiave move (t) = p , dove t è il tempo [0..1], p è la posizione calcolata al tempo t . Quindi sposta (0) restituisce la posizione all'inizio della curva, sposta (0,5) il centro esatto e sposta (1) alla fine. Usare una funzione di temporizzazione time (T) = t per fornire i valori t per lo spostamento dovrebbe darmi quello che voglio. Per un effetto di rimbalzo, la funzione di temporizzazione dovrebbe restituire gli stessi valori t per tempo (0,8) e tempo (0,8)(solo un esempio). Basta sostituire la funzione di temporizzazione per ottenere un effetto diverso.

(Sì, è possibile eseguire il rimbalzo della linea creando e unendo quattro segmenti di linea che vanno avanti e indietro, ma non dovrebbe essere necessario. Dopotutto, è solo una semplice funzione lineare che mappa i valori temporali alle posizioni.)

Spero di avere un senso qui.


essere consapevoli del fatto che (come spesso accade su SO), questa domanda molto vecchia ora ha risposte molto obsolete .. assicurati di scorrere verso il basso fino alle risposte attuali. (Notevole: cubic-bezier.com !)
Fattie

Risposte:


48

Ho trovato questo:

Cocoa with Love - Curve di accelerazione parametriche in Core Animation

Ma penso che possa essere reso un po 'più semplice e più leggibile usando i blocchi. Quindi possiamo definire una categoria su CAKeyframeAnimation simile a questa:

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

Ora l'utilizzo sarà simile a questo:

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

So che potrebbe non essere così semplice come quello che volevi, ma è un inizio.


1
Ho preso la risposta di Jesse Crossen e l'ho ampliata un po '. Puoi usarlo per animare CGPoints e CGSize, ad esempio. A partire da iOS 7, puoi anche utilizzare funzioni temporali arbitrarie con le animazioni UIView. Puoi controllare i risultati su github.com/jjackson26/JMJParametricAnimation
JJ Jackson

@JJJackson Grande raccolta di animazioni! Grazie per averli raccolti
Codey

33

Da iOS 10 è diventato possibile creare una funzione di temporizzazione personalizzata più facilmente utilizzando due nuovi oggetti di temporizzazione.

1) UICubicTimingParameters consente di definire la curva di Bézier cubica come funzione di andamento .

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 semplicemente usando i punti di controllo sull'inizializzazione dell'animatore

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) 

Questo fantastico servizio ti aiuterà a scegliere i punti di controllo per le tue curve.

2) UISpringTimingParameters consente agli sviluppatori di manipolare il rapporto di smorzamento , la massa , la rigidità e la velocità iniziale per creare il comportamento della molla desiderato.

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)

Il parametro di durata è ancora presentato in Animator, ma verrà ignorato per i tempi primaverili.

Se queste due opzioni non sono sufficienti puoi anche implementare la tua curva di temporizzazione confermando a UITimingCurveProvider protocollo .

Maggiori dettagli, come creare animazioni con diversi parametri di temporizzazione, si possono trovare nella documentazione .

Inoltre, per favore, guarda la presentazione Advances in UIKit Animations and Transitions da WWDC 2016.


Puoi trovare esempi di utilizzo delle funzioni di temporizzazione nel repository iOS10-animations-demo .
Olga Konoreva

Puoi approfondire "Se queste due opzioni non sono sufficienti, puoi anche implementare la tua curva di temporizzazione confermando al protocollo UITimingCurveProvider." ?
Christian Schnorr

Eccezionale! E la CoreAnimationversione di UISpringTimingParameters( CASpringAnimation) è supportata di nuovo su iOS 9!
Rhythmic Fistman

@OlgaKonoreva, il servizio cubic-bezier.com è ASSOLUTAMENTE PREGIATO E MERAVIGLIOSO . Spero che tu sia ancora un utente SO - ti
mando

@ChristianSchnorr Penso che la risposta stia sfuggendo alla capacità di UITimingCurveProvidera non solo di rappresentare un'animazione cubica o a molla, ma anche di rappresentare un'animazione che combina sia cubica che Spring via UITimingCurveType.composed.
David James,

14

Un modo per creare una funzione di temporizzazione personalizzata è utilizzare functionWithControlPoints :::: metodo factory in CAMediaTimingFunction (esiste anche un metodo initWithControlPoints :::: init corrispondente). Ciò che fa è creare una curva di Bézier per la tua funzione di temporizzazione. Non è una curva arbitraria, ma le curve di Bézier sono molto potenti e flessibili. Ci vuole un po 'di pratica per prendere confidenza con i punti di controllo. Un consiglio: la maggior parte dei programmi di disegno può creare curve di Bézier. Giocare con quelli ti darà un feedback visivo sulla curva che stai rappresentando con i punti di controllo.

Il collegamento indica la documentazione di Apple. C'è una sezione breve ma utile su come le funzioni di pre-compilazione sono costruite dalle curve.

Modifica: il codice seguente mostra una semplice animazione di rimbalzo. Per fare ciò, ho creato una funzione di temporizzazione composta ( valori e proprietà NSArray di temporizzazione ) e ho assegnato a ciascun segmento dell'animazione una durata di tempo diversa ( proprietà keytimes ). In questo modo puoi comporre curve di Bézier per comporre tempi più sofisticati per le animazioni. Questo è un buon articolo su questo tipo di animazioni con un bel codice di esempio.

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

}

Sì è vero. È possibile combinare più funzioni di temporizzazione utilizzando i valori e le proprietà timingFunctions di CAKeyframeAnimation per ottenere ciò che si desidera. Elaborerò un esempio e inserirò un po 'il codice.
fsaint

Grazie per i vostri sforzi e punti per aver provato :) Questo approccio potrebbe funzionare in teoria, ma deve essere personalizzato per ogni utilizzo. Sto cercando una soluzione generale che funzioni per tutte le animazioni come funzione di temporizzazione. Ad essere onesti, non sembra così bello mettere insieme linee e curve. Anche l'esempio del "jack in a box" ne soffre.
Martin Wickman

1
Puoi specificare un CGPathRef invece di un array di valori per un'animazione di keyframe, ma alla fine Felz è corretto: CAKeyframeAnimation e timingFunctions ti daranno tutto ciò di cui hai bisogno. Non so perché pensi che non sia una "soluzione generale" che debba essere "personalizzata per ogni utilizzo". Crei l'animazione del fotogramma chiave una volta in un metodo di fabbrica o qualcosa del genere e puoi quindi aggiungere quell'animazione a qualsiasi livello più e più volte tutte le volte che vuoi. Perché dovrebbe essere personalizzato per ogni utilizzo? Mi sembra che non sia diverso dalla tua idea di come dovrebbero funzionare le funzioni di temporizzazione.
Matt Long

1
Oh ragazzi, questi sono in ritardo di 4 anni, ma se functionWithControlPoints :::: può fare animazioni rimbalzanti / elastiche. Usalo in congiunzione con cubic-bezier.com/#.6,1.87,.63,.74
Matt Foley,

10

Non sono sicuro se stai ancora cercando, ma PRTween sembra abbastanza impressionante in termini di capacità di andare oltre ciò che Core Animation ti offre fuori dagli schemi, in particolare, funzioni di temporizzazione personalizzate. Inoltre viene fornito con molte, se non tutte, delle popolari curve di andamento fornite dai vari framework web.


2

Un'implementazione rapida della versione è TFAnimation . La demo è un'animazione della curva sin . UtilizzareTFBasicAnimation proprio come CABasicAnimationtranne per l'assegnazione timeFunctioncon un blocco diverso da timingFunction.

Il punto chiave è la sottoclasse CAKeyframeAnimatione calcola la posizione dei fotogrammi in base timeFunctionall'intervallo in 1 / 60fpss. Dopo tutto, aggiungi tutto il valore calcolato a valuesdi CAKeyframeAnimatione anche i tempi per intervallo keyTimes.


2

Ho creato un approccio basato sui blocchi, che genera un gruppo di animazioni, con più animazioni.

Ogni animazione, per proprietà, può utilizzare 1 delle 33 diverse curve parametriche, una funzione di temporizzazione del decadimento con velocità iniziale o una molla personalizzata configurata in base alle proprie esigenze.

Una volta che il gruppo è stato generato, viene memorizzato nella cache nella vista e può essere attivato utilizzando un AnimationKey, con o senza l'animazione. Una volta attivata, l'animazione viene sincronizzata di conseguenza con i valori del livello di presentazione e applicata di conseguenza.

Il framework può essere trovato qui FlightAnimator

Di seguito è riportato un esempio:

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

Per attivare l'animazione

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.