如何使用核心动画创建自定义缓动函数?

119
我正在iOS上沿着QuadCurve动画一个CALayer,效果很好。但我想使用比苹果提供的几个函数(如EaseIn/EaseOut等)更有趣的缓动函数,例如弹跳或弹性函数。
这些可以使用MediaTimingFunction(贝塞尔曲线)来实现:

enter image description here

但我希望创建更复杂的时间函数。问题是媒体时间似乎需要一个三次贝塞尔曲线,这不足以创建这些效果:

在此输入图片描述
(来源:sparrow-framework.org

在其他框架中创建上述内容的代码非常简单,这使得这个问题非常令人沮丧。请注意,曲线将输入时间映射到输出时间(T-t 曲线),而不是时间位置曲线。例如,easeOutBounce(T) = t 返回一个新的 t。然后,该 t 用于绘制移动(或我们应该动画化的任何属性)。

所以,我想创建一个复杂的自定义CAMediaTimingFunction,但我不知道如何做到这一点,也不确定是否可能?有什么替代方案吗?

编辑:

这里是两个步骤的具体示例,非常有启发性:)

  1. 我想要将一个对象沿着从点ab的直线移动,但我希望它在使用上面的easeOutBounce曲线时“弹跳”其运动。这意味着它将沿着从ab的精确线路移动,但将以比当前基于贝塞尔曲线的CAMediaTimingFunction更为复杂的方式加速和减速。

  2. 让我们将该线路变为使用CGPath指定的任意曲线运动。它仍应沿着该曲线移动,但应与线路示例中的加速度和减速度相同。

理论上,我认为它应该像这样工作:

让我们将运动曲线描述为一个关键帧动画 move(t) = p,其中 t 是时间 [0..1],p 是在时间 t 计算的位置。因此,move(0) 返回曲线起始位置,move(0.5) 返回精确的中间位置,move(1) 返回曲线结束位置。使用一个时间函数 time(T) = t 来为 move 提供 t 值应该可以得到我想要的效果。对于弹跳效果,时间函数应该返回相同的 t 值,例如 time(0.8)time(0.8)。只需替换时间函数即可获得不同的效果。

(是的,通过创建和连接来回移动的四条线段可以实现线性弹跳,但这并不是必要的。毕竟,它只是一个简单的线性函数,将 time 值映射到位置上。)

我希望我表达清楚了。


请注意(正如在SO上经常发生的那样),这个非常古老的问题现在有非常过时的答案...一定要向下滚动查看最新的惊人答案。(特别值得注意的是:http://cubic-bezier.com!) - Fattie
6个回答

48

我找到了这个链接:

Cocoa with Love - Core Animation 中的参数加速曲线

但我认为可以通过使用 blocks 使其更简单易懂。因此,我们可以定义一个在 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
我参考了Jesse Crossen的回答,并进行了一些扩展。例如,您可以使用它来为CGPoints和CGSize添加动画效果。从iOS 7开始,您还可以在UIView动画中使用任意时间函数。您可以在https://github.com/jjackson26/JMJParametricAnimation上查看结果。 - J.J. Jackson
@J.J.Jackson 很棒的动画收藏!感谢你的收集。 - Codey

36

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

动画器中仍然存在持续时间参数,但在弹簧计时中将被忽略。

如果这两个选项不足够,您还可以通过符合UITimingCurveProvider协议来实现自己的时间曲线。

有关如何使用不同时间参数创建动画的详细信息,请参见文档

此外,请参阅WWDC 2016的UIKit动画和过渡的进展演示


您可以在iOS10-animations-demo存储库中找到使用计时函数的示例。 - Olga Konoreva
1
请问您能详细说明一下"If these two options are not enough you also can implement your own timing curve by confirming to the UITimingCurveProvider protocol."的意思吗? - Christian Schnorr
太棒了!UISpringTimingParametersCoreAnimation版本(CASpringAnimation)支持iOS 9及以上版本! - Rhythmic Fistman
@OlgaKonoreva,cubic-bezier.com服务是无价和美妙的。希望你仍然是SO用户 - 给你发送一个丰厚的赏金来表达感谢 :) - Fattie
@ChristianSchnorr 我认为答案所指的是 UITimingCurveProvider 的能力,它不仅可以表示立方体或弹簧动画,还可以通过 UITimingCurveType.composed 表示同时结合立方体和弹簧的动画。 - David James
@Fattie 看起来我忘了感谢你...非常感谢你! - Olga Konoreva

14
使用CAMediaTimingFunction中的functionWithControlPoints::::工厂方法(也有相应的initWithControlPoints::::init方法),可以创建自定义时间函数的一种方法。这将为您的时间函数创建一个贝塞尔曲线。它不是任意曲线,但贝塞尔曲线非常强大和灵活。需要一些练习来掌握控制点。提示:大多数绘图程序都可以创建贝塞尔曲线。通过玩弄这些,您可以获得有关用控制点表示的曲线的视觉反馈。 此链接指向苹果的文档。其中有一个简短但有用的部分,介绍了如何从曲线构建预构建函数。
编辑: 以下代码显示了一个简单的弹跳动画。为此,我创建了一个组合时间函数(values和timing 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];

}

是的,这是正确的。您可以使用CAKeyframeAnimation的values和timingFunctions属性组合多个计时函数,以实现您想要的效果。我会编写一个示例并稍后放置代码。 - fsaint
感谢你的努力和尝试,给你点赞 :) 那种方法在理论上可能有效,但需要针对每个具体情况进行定制。我正在寻找一种适用于所有动画的通用解决方案,作为一个时间函数。老实说,将线条和曲线拼凑在一起看起来并不好。"盒中之娃"的例子也有这个问题。 - Martin Wickman
1
您可以将CGPathRef指定为关键帧动画的值数组,但最终Felz是正确的 - CAKeyframeAnimation和timingFunctions将为您提供所需的一切。我不确定您为什么认为这不是必须“针对每个用途进行定制”的“通用解决方案”。您只需在工厂方法或其他地方构建关键帧动画一次,然后可以将该动画添加到任何图层中,重复多次。为什么它需要针对每个用途进行定制呢?在我看来,它与您对时间函数应该如何工作的概念没有什么区别。 - Matt Long
1
哦,伙计们,这已经晚了4年了,但是如果functionWithControlPoints::::完全可以做弹性/弹簧动画。与http://cubic-bezier.com/#.6,1.87,.63,.74一起使用。 - Matt Foley

10

不确定您是否还在寻找,但PRTween在超越Core Animation提供的基本功能方面似乎相当出色,尤其是自定义时间函数。它还附带了许多(如果不是全部)各种Web框架提供的流行缓动曲线。


2
我创建了一种基于块的方法,可以生成一个动画组,其中包含多个动画。
每个属性的动画都可以使用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)

2
一个快速版本的实现是TFAnimation。该演示为正弦曲线动画。使用TFBasicAnimation就像使用CABasicAnimation一样,只需将timeFunction分配给块而不是timingFunction
关键点是子类化CAKeyframeAnimation并通过timeFunction1 / 60fps秒间隔内计算帧位置。最后将所有计算值添加到CAKeyframeAnimationvalues中,并且将时间间隔乘以次数添加到keyTimes中。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接