Как создать пользовательскую функцию плавности с помощью Core Animation?


115

Я оживляющий CALayerвдоль CGPath(QuadCurve) довольно хорошо в IOS. Но я бы хотел использовать более интересную функцию плавности, чем те, которые предоставляет Apple (EaseIn / EaseOut и т. Д.). Например, отскок или упругая функция.

С помощью MediaTimingFunction (безье) можно делать следующее:

введите описание изображения здесь

Но я бы хотел создать более сложные временные функции . Проблема в том, что для синхронизации мультимедиа, похоже, требуется кубическая Безье, которая недостаточно мощна для создания этих эффектов:

введите описание изображения здесь
(источник: sparrow-framework.org )

Код , чтобы создать рассмотренном выше , является достаточно простым в других рамках, что делает это очень расстраивает. Обратите внимание, что кривые отображают время входа и время выхода (кривая Tt), а не кривые время-положение. Например, easyOutBounce (T) = t возвращает новый t . Затем этот t используется для построения движения (или любого другого свойства, которое мы должны анимировать).

Итак, я хотел бы создать сложный заказ, CAMediaTimingFunctionно понятия не имею, как это сделать, и возможно ли это вообще? Есть ли альтернативы?

РЕДАКТИРОВАТЬ:

Вот конкретный пример по шагам. Очень познавательно :)

  1. Я хочу анимировать объект вдоль линии от точки a до b , но я хочу, чтобы он «подпрыгивал» своим движением вдоль линии, используя приведенную выше кривую easyOutBounce. Это означает, что он будет следовать точной линии от a до b , но будет ускоряться и замедляться более сложным образом, чем это возможно при использовании текущей функции CAMediaTimingFunction на основе Безье.

  2. Давайте сделаем эту линию любым произвольным движением кривой, указанным с помощью CGPath. Он все еще должен двигаться по этой кривой, но он должен ускоряться и замедляться так же, как в примере с прямой.

Теоретически я думаю, что это должно работать так:

Давайте опишем кривую движения как движение анимации по ключевым кадрам (t) = p , где t - время [0..1], p - положение, вычисленное в момент времени t . Таким образом, move (0) возвращает позицию в начале кривой, перемещает (0,5) в точную середину и перемещает (1) в конце. Использование временной функции time (T) = t для предоставления значений t для move должно дать мне то, что я хочу. Для эффекта подпрыгивания функция синхронизации должна возвращать те же значения t для времени (0,8) и времени (0,8).(просто пример). Просто замените функцию синхронизации, чтобы получить другой эффект.

(Да, можно выполнять подпрыгивание линии, создавая и объединяя четыре линейных сегмента, которые движутся вперед и назад, но в этом нет необходимости. В конце концов, это просто простая линейная функция, которая сопоставляет значения времени с позициями.)

Надеюсь, я здесь понимаю.


имейте в виду, что (как это часто бывает с SO), этот очень старый вопрос теперь имеет очень устаревшие ответы ... не забудьте прокрутить вниз до текущих ответов. (Очень примечательно: cubic-bezier.com !)
Fattie

Ответы:


48

Я нашел это:

Какао с любовью - Параметрические кривые ускорения в Core Animation

Но я думаю, что это можно сделать немного проще и читабельнее, используя блоки. Итак, мы можем определить категорию в CAKeyframeAnimation, которая будет выглядеть примерно так:

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

Теперь использование будет выглядеть примерно так:

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

Я знаю, что это может быть не так просто, как вы хотели, но это только начало.


1
Я воспринял ответ Джесси Кроссена и немного расширил его. Вы можете использовать его, например, для анимации CGPoints и CGSize. Начиная с iOS 7, вы также можете использовать произвольные функции времени с анимацией UIView. Вы можете проверить результаты на github.com/jjackson26/JMJParametricAnimation
JJ Jackson,

@JJJackson Отличная коллекция анимаций! Спасибо, что
собрали

33

В iOS 10 стало проще создавать пользовательские функции синхронизации с помощью двух новых объектов синхронизации.

1) UICubicTimingParameters позволяет определить кубическую кривую Безье как функцию ослабления.

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)

или просто используя контрольные точки при инициализации аниматора

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) 

Этот замечательный сервис поможет выбрать контрольные точки для ваших кривых.

2) UISpringTimingParameters позволяет разработчикам управлять коэффициентом демпфирования , массой , жесткостью и начальной скоростью для создания желаемого поведения пружины.

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)

Параметр Duration по-прежнему отображается в Animator, но будет проигнорирован для весеннего времени.

Если этих двух вариантов недостаточно, вы также можете реализовать свою собственную временную кривую, подтвердив протокол UITimingCurveProvider .

Более подробно, как создавать анимацию с разными временными параметрами, вы можете найти в документации .

Также, пожалуйста, посмотрите презентацию Advances in UIKit Animations and Transitions с WWDC 2016.


Вы можете найти примеры использования функций синхронизации в репозитории iOS10-animations-demo .
Ольга Конорева 05

Не могли бы вы уточнить: «Если этих двух вариантов недостаточно, вы также можете реализовать свою собственную временную кривую, подтвердив протокол UITimingCurveProvider». ?
Christian Schnorr

Потрясающие! И CoreAnimationверсия UISpringTimingParameters( CASpringAnimation) поддерживается до iOS 9!
Rhythmic Fistman

@OlgaKonoreva, сервис cubic-bezier.com БЕСПЛАТНО И ЧУДЕСНЫЙ . Надеюсь, вы все еще ТАК пользователь - посылаете вам жирную награду в
знак

@ChristianSchnorr Я думаю, что ответ ускользает от способности a UITimingCurveProviderне только представлять кубическую или пружинную анимацию, но также представлять анимацию, которая сочетает в себе кубический и пружинный переходы UITimingCurveType.composed.
Дэвид Джеймс

14

Способ создания настраиваемой функции синхронизации - использование фабричного метода functionWithControlPoints :::: в CAMediaTimingFunction (также имеется соответствующий метод initWithControlPoints :::: init). Это создает кривую Безье для вашей временной функции. Это не произвольная кривая, но кривые Безье очень мощные и гибкие. Чтобы разобраться в контрольных точках, потребуется немного практики. Совет: большинство программ для рисования могут создавать кривые Безье. Играя с ними, вы получите визуальную обратную связь с кривой, которую вы представляете с контрольными точками.

Эта ссылка указывает на документацию Apple. Есть небольшой, но полезный раздел о том, как функции предварительной сборки строятся из кривых.

Изменить: следующий код показывает простую анимацию отскока. Для этого я создал составную функцию времени ( значения и свойства NSArray времени ) и дал каждому сегменту анимации различную продолжительность времени ( свойство keytimes ). Таким образом, вы можете составлять кривые Безье, чтобы создать более сложную синхронизацию для анимации. Это хорошая статья об этом типе анимации с хорошим примером кода.

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

}

Да это правда. Вы можете комбинировать несколько функций времени, используя значения и свойства timerFunctions CAKeyframeAnimation, чтобы добиться желаемого. Я разработаю пример и немного вставлю код.
fsaint 02

Спасибо за ваши усилия и очки за попытку :) Этот подход может работать теоретически, но его нужно настраивать для каждого использования. Я ищу общее решение, которое работает для всех анимаций как функция времени. Честно говоря, это не выглядит так хорошо, как соединять вместе линии и кривые. Пример «домкрат в коробке» тоже страдает от этого.
Мартин Викман,

1
Вы можете указать CGPathRef вместо массива значений для анимации по ключевым кадрам, но, в конце концов, Фелц прав - CAKeyframeAnimation и TimingFunctions предоставят вам все необходимое. Я не уверен, почему вы думаете, что это не «общее решение», которое нужно «настраивать для каждого использования». Вы создаете анимацию по ключевым кадрам один раз с помощью фабричного метода или чего-то еще, а затем можете добавлять эту анимацию к любому слою снова и снова столько раз, сколько захотите. Зачем нужно настраивать его для каждого использования? Мне кажется, это не отличается от вашего представления о том, как должны работать функции синхронизации.
Мэтт Лонг

1
Ой, ребята, это опоздание на 4 года, но если functionWithControlPoints :::: может полностью выполнять упругую / упругую анимацию. Используйте его вместе с cubic-bezier.com/#.6,1.87,.63,.74
Мэтт Фоули

10

Не уверен, что вы все еще ищете, но PRTween выглядит довольно впечатляюще с точки зрения его способности выходить за рамки того, что Core Animation дает вам из коробки, в первую очередь, настраиваемых функций времени. Он также поставляется со многими - если не всеми - популярными кривыми плавности, которые предоставляют различные веб-фреймворки.


2

Быстрая реализация версии - TFAnimation . Демонстрация представляет собой анимацию кривой греха . ИспользуйтеTFBasicAnimation точно так же, CABasicAnimationза исключением назначения timeFunctionс блоком, отличным от timingFunction.

Ключевым моментом является создание подкласса CAKeyframeAnimationи вычисление положения кадров по интервалу timeFunctionв 1 / 60fpss. В конце концов, добавьте все вычисленное значение в valuesof CAKeyframeAnimationи времена по интервалу keyTimesтакже.


2

Я создал подход, основанный на блоках, который генерирует группу анимаций с несколькими анимациями.

Каждая анимация для каждого свойства может использовать 1 из 33 различных параметрических кривых, функцию времени затухания с начальной скоростью или настраиваемую пружину, настроенную в соответствии с вашими потребностями.

После того, как группа сгенерирована, она кэшируется в представлении и может быть запущена с помощью AnimationKey, с анимацией или без нее. После запуска анимация синхронизируется в соответствии со значениями уровня представления и применяется соответствующим образом.

Фреймворк можно найти здесь FlightAnimator

Вот пример ниже:

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

Для запуска анимации

view.applyAnimation(forKey: AnimationKeys.StageOneAnimationKey)
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.