Como criar uma função de atenuação personalizada com Core Animation?

115

Estou animando um CALayeralong a CGPath(QuadCurve) muito bem no iOS. Mas eu gostaria de usar uma função de atenuação mais interessante do que as poucas fornecidas pela Apple (EaseIn / EaseOut etc). Por exemplo, um salto ou função elástica.

Essas coisas são possíveis de fazer com MediaTimingFunction (bezier):

insira a descrição da imagem aqui

Mas eu gostaria de criar funções de temporização mais complexas. O problema é que o sincronismo da mídia parece exigir um Bezier cúbico que não é poderoso o suficiente para criar estes efeitos:

insira a descrição da imagem aqui
(fonte: sparrow-framework.org )

O código para criar o código acima é simples o suficiente em outras estruturas, o que torna isso muito frustrante. Observe que as curvas estão mapeando o tempo de entrada para o tempo de saída (curva Tt) e não as curvas de posição de tempo. Por exemplo, facilidadeOutBounce (T) = t retorna um novo t . Então esse t é usado para plotar o movimento (ou qualquer propriedade que devemos animar).

Então, eu gostaria de criar um personalizado complexo, CAMediaTimingFunctionmas não tenho ideia de como fazer isso, ou se é possível? Existem alternativas?

EDITAR:

Aqui está um exemplo concreto em etapas. Muito educacional :)

  1. Quero animar um objeto ao longo de uma linha do ponto a ao b , mas quero que ele "salte" seu movimento ao longo da linha usando a curva facilidadeOutBounce acima. Isso significa que ele seguirá a linha exata de a até b , mas irá acelerar e desacelerar de uma forma mais complexa do que seria possível usando o CAMediaTimingFunction atual baseado em bezier.

  2. Vamos fazer dessa linha qualquer movimento de curva arbitrário especificado com CGPath. Ele ainda deve se mover ao longo dessa curva, mas deve acelerar e desacelerar da mesma forma que no exemplo da linha.

Em teoria, acho que deveria funcionar assim:

Vamos descrever a curva de movimento como um movimento de animação de quadro-chave (t) = p , onde t é o tempo [0..1], p é a posição calculada no tempo t . Assim movimento (0) retorna a posição no início da curva, movimento (0,5), o meio exacto e movimento (1) na extremidade. Usar uma função de tempo (T) = t para fornecer os valores de t para mover deve me dar o que eu quero. Para um efeito de salto, a função de tempo deve retornar os mesmos valores t para tempo (0,8) e tempo (0,8)(apenas um exemplo). Basta substituir a função de tempo para obter um efeito diferente.

(Sim, é possível fazer salto de linha criando e juntando quatro segmentos de linha que vão e voltam, mas isso não deve ser necessário. Afinal, é apenas uma função linear simples que mapeia valores de tempo para posições.)

Espero estar fazendo sentido aqui.

Martin Wickman
fonte
esteja ciente de que (como costuma ser o caso no SO), essa pergunta muito antiga agora tem respostas muito desatualizadas ... certifique-se de rolar para baixo até as incríveis respostas atuais. (Altamente notável: cubic-bezier.com !)
Fattie

Respostas:

48

Eu achei isto:

Cocoa with Love - curvas de aceleração paramétrica no Core Animation

Mas acho que pode ser um pouco mais simples e mais legível usando blocos. Portanto, podemos definir uma categoria em CAKeyframeAnimation que se parece com isto:

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

Agora o uso será parecido com isto:

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

Eu sei que pode não ser tão simples quanto o que você queria, mas é um começo.

Jesse Crossen
fonte
1
Eu peguei a resposta de Jesse Crossen e a expandi um pouco. Você pode usá-lo para animar CGPoints e CGSize, por exemplo. A partir do iOS 7, você também pode usar funções de tempo arbitrárias com animações UIView. Você pode verificar os resultados em github.com/jjackson26/JMJParametricAnimation
JJ Jackson
@JJJackson Ótima coleção de animações! Obrigado por coletá-los
Codey
33

A partir do iOS 10, tornou-se possível criar uma função de temporização personalizada mais fácil usando dois novos objetos de temporização.

1) UICubicTimingParameters permite definir a curva de Bézier cúbica como uma função de atenuação.

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 simplesmente usando pontos de controle na inicialização do 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 serviço incrível vai ajudar a escolher pontos de controle para suas curvas.

2) UISpringTimingParameters permite que os desenvolvedores manipulem a taxa de amortecimento , massa , rigidez e velocidade inicial para criar o comportamento de mola desejado.

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)

O parâmetro de duração ainda é apresentado no Animator, mas será ignorado para o tempo da mola.

Se essas duas opções não forem suficientes, você também pode implementar sua própria curva de tempo, confirmando com o protocolo UITimingCurveProvider .

Mais detalhes sobre como criar animações com diferentes parâmetros de tempo, você pode encontrar na documentação .

Além disso, consulte a apresentação Advances in UIKit Animations and Transitions da WWDC 2016.

Olga Konoreva
fonte
Você pode encontrar exemplos de como usar as funções de temporização no repositório iOS10-animations-demo .
Olga Konoreva
Você pode explicar "Se essas duas opções não forem suficientes, você também pode implementar sua própria curva de tempo, confirmando com o protocolo UITimingCurveProvider." ?
Christian Schnorr
Impressionante! E a CoreAnimationversão de UISpringTimingParameters( CASpringAnimation) é compatível com iOS 9!
Rítmico Fistman
@OlgaKonoreva, o serviço cubic-bezier.com é TOTALMENTE IMPRESSIONANTE E MARAVILHOSO . Espero que você ainda seja um usuário do SO - enviando uma recompensa para agradecer :)
Fattie
@ChristianSchnorr Acho que a resposta está iludida é a capacidade de UITimingCurveProvidera não apenas representar uma animação cúbica ou de mola, mas também de representar uma animação que combina ambas as vias cúbica e elástica UITimingCurveType.composed.
David James
14

Uma maneira de criar uma função de temporização personalizada é usando o método de fábrica functionWithControlPoints :::: em CAMediaTimingFunction (há um método initWithControlPoints :::: init correspondente também). O que isso faz é criar uma curva de Bézier para sua função de tempo. Não é uma curva arbitrária, mas as curvas de Bézier são muito poderosas e flexíveis. É preciso um pouco de prática para pegar o jeito dos pontos de controle. Uma dica: a maioria dos programas de desenho pode criar curvas de Bézier. Jogar com eles lhe dará um feedback visual sobre a curva que você está representando com os pontos de controle.

Os esta ligação aponta para a documentação da Apple. Há uma seção curta, mas útil, sobre como as funções de pré-construção são construídas a partir de curvas.

Editar: O código a seguir mostra uma animação de salto simples. Para fazer isso, criei uma função de tempo composta ( propriedades NSArray de valores e tempo ) e dei a cada segmento da animação uma duração de tempo diferente ( propriedade de tempos-chave ). Desta forma, você pode compor curvas de Bézier para compor um tempo mais sofisticado para animações. Este é um bom artigo sobre este tipo de animações com um bom código de exemplo.

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

}
fraco
fonte
Sim, isso é verdade. Você pode combinar várias funções de temporização usando as propriedades values ​​e timingFunctions de CAKeyframeAnimation para alcançar o que deseja. Vou trabalhar em um exemplo e colocar o código um pouco.
fraco de
Obrigado por seus esforços e pontos por tentar :) Essa abordagem pode funcionar em teoria, mas precisa ser personalizada para cada uso. Estou procurando uma solução geral que funcione para todas as animações como uma função de tempo. Para ser honesto, não parece muito bom juntar linhas e curvas. O exemplo "jack in a box" também sofre com isso.
Martin Wickman,
1
Você pode especificar um CGPathRef em vez de uma matriz de valores para uma animação de quadro-chave, mas no final Felz está correto - CAKeyframeAnimation e timingFunctions fornecerão tudo o que você precisa. Não sei por que você acha que essa não é uma "solução geral" que precisa ser "personalizada para cada uso". Você constrói a animação do quadro-chave uma vez em um método de fábrica ou algo assim e pode então adicionar essa animação a qualquer camada quantas vezes quiser. Por que ele precisa ser personalizado para cada uso? Parece-me que não é diferente da sua noção de como as funções de temporização devem funcionar.
Matt Long,
1
Ah, pessoal, agora está 4 anos atrasado, mas se functionWithControlPoints :::: pode fazer animações saltitantes / elásticas. Use-o em conjunto com cubic-bezier.com/#.6,1.87,.63,.74
Matt Foley
10

Não tenho certeza se você ainda está procurando, mas o PRTween parece bastante impressionante em termos de sua capacidade de ir além do que o Core Animation oferece fora da caixa, mais notavelmente, funções de temporização personalizadas. Ele também vem embalado com muitas - senão todas - as curvas de atenuação populares que várias estruturas da web fornecem.

CIFilter
fonte
2

Uma implementação de versão rápida é TFAnimation . A demonstração é uma animação de curva de pecado . UseTFBasicAnimation apenas como, CABasicAnimationexceto atribuir timeFunctiona um bloco diferente de timingFunction.

O ponto-chave é a subclasse CAKeyframeAnimatione calcula a posição dos quadros por timeFunctionno 1 / 60fpsintervalo s. Depois disso, adicione todos os valores calculados a valuesde CAKeyframeAnimatione os tempos por intervalo keyTimestambém.

Xingxing
fonte
2

Criei uma abordagem baseada em blocos, que gera um grupo de animação, com várias animações.

Cada animação, por propriedade, pode usar 1 de 33 curvas paramétricas diferentes, uma função de temporização de diminuição com velocidade inicial ou uma mola personalizada configurada de acordo com suas necessidades.

Uma vez que o grupo é gerado, ele é armazenado em cache na Visualização e pode ser disparado usando uma AnimationKey, com ou sem a animação. Uma vez acionada, a animação é sincronizada de acordo com os valores da camada de apresentação e aplicada de acordo.

O framework pode ser encontrado aqui FlightAnimator

Aqui está um exemplo abaixo:

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 acionar a animação

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