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

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

Эти вещи можно сделать с помощью MediaTimingFunction (Безье):

enter image description here

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

enter image description here
(источник: sparrow-framework.org)

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

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

EDIT:

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

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

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

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

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

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

Я надеюсь, что здесь есть смысл.

Ответ 1

Я нашел это:

Cocoa с любовью - кривые параметрического ускорения в 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];
}

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

Ответ 2

Из 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 позволяет разработчикам манипулировать отношением демпфирования, массой, жесткостью и начальной скоростью для создания желаемого поведения spring.

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)

Параметр продолжительности все еще присутствует в Animator, но будет игнорироваться для spring синхронизации.

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

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

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

Ответ 3

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

эта ссылка указывает на документацию на яблоко. Существует короткий, но полезный раздел о том, как функции предварительной сборки строятся из кривых.

Изменить: Следующий код показывает простую анимацию отскока. Для этого я создал созданную функцию синхронизации ( значения и время свойства 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];

}

Ответ 4

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

Ответ 5

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

Вы можете проверить результаты на https://github.com/jjackson26/JMJParametricAnimation

Ответ 6

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

Ключевым моментом является подкласс CAKeyframeAnimation и вычисляет положение кадра на timeFunction в интервале 1 / 60fps. После того, как все добавьте все вычисленное значение в values of CAKeyframeAnimation и время от интервала до keyTimes тоже.

Ответ 7

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

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

Как только группа сгенерирована, она кэшируется в представлении и может запускаться с помощью 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)