Canoe

iOS核心动画实践九(缓冲)

2017.06.22

缓冲实际上是为了让CoreAnimation看起来更加自然和流畅,而不是机械和僵硬。

动画速度

动画实际上就是一段时间内的变化,这意味着变化是随着某个特定的速率进行的,速率由以下公式计算而来:
velocity = change / time

CAMediaTimingFunction

我们通过改变CAAnimation的timingFunction属性来控制动画的变换方式。一般CAMediaTimingFunction有以下几个常量:

kCAMediaTimingFunctionLinear    //线性
kCAMediaTimingFunctionEaseIn    //慢慢加速突然停止
kCAMediaTimingFunctionEaseOut   //全速开始慢慢减速
kCAMediaTimingFunctionEaseInEaseOut //慢慢加速慢慢减速,当使用UIView的时候,它是默认的选项,当创建CAAnimation的时候,就需要手动设置
kCAMediaTimingFunctionDefault   //它和kCAMediaTimingFunctionEaseInEaseOut很类似,但是加速和减速的过程都稍微有些慢。

UiView的动画缓冲

UIView动画的缓冲选项,给options参数添加如下常量之一:

UIViewAnimationOptionCurveEaseInOut 
UIViewAnimationOptionCurveEaseIn 
UIViewAnimationOptionCurveEaseOut 
UIViewAnimationOptionCurveLinear

缓冲和关键帧动画

CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的元素个数减一,因为它是描述每一帧之间动画速度的函数。

在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了。

- (IBAction)changeColor
{
    //create a keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"backgroundColor";
    animation.duration = 2.0;
    animation.values = @[
                         (__bridge id)[UIColor blueColor].CGColor,
                         (__bridge id)[UIColor redColor].CGColor,
                         (__bridge id)[UIColor greenColor].CGColor,
                         (__bridge id)[UIColor blueColor].CGColor ];
    //add timing function
    CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn];
    animation.timingFunctions = @[fn, fn, fn];
    //apply animation to layer
    [self.colorLayer addAnimation:animation forKey:nil];
}

自定义缓冲函数

在我们实际的项目中,动画设计师一般不会按照苹果给定的几种缓冲方式来设计动画效果,这必然要我们自己能够自定义缓冲函数。
除了+functionWithName:之外,CAMediaTimingFunction同样有另一个构造函数,一个有四个浮点参数的+functionWithControlPoints::::

三次贝塞尔曲线

CAMediaTimingFunction函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线。
我们通过四个点可以控制一个三次贝塞尔曲线,第一个点和最后一个点代表起点和终点,中间两个点叫控制点,控制曲线的形状。

具体的四个点我们如何获得呢?可以通过这个网站来将UI效果转换成我们需要的四个参数。cubic-bezier.com

更加复杂的动画曲线


这种效果没法用一个简单的三次贝塞尔曲线表示,于是不能用CAMediaTimingFunction来完成。但如果想要实现这样的效果,可以用如下几种方法:

  • 用CAKeyframeAnimation创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数(具体下节介绍)。
  • 使用定时器逐帧更新实现动画(见第11章,“基于定时器的动画”)。

基于关键帧的缓冲

为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。
这种方式还算不错,但是实现起来略显笨重(因为要不停地尝试计算各种关键帧和时间偏移)并且和动画强绑定了(因为如果要改变动画的一个属性,那就意味着要重新计算所有的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面我们来实现它。

流程自动化

上面我们说到将一个复杂的动画分割成多个小的关键帧,对每一个关键帧进行不同的缓冲设置,这种方式缺陷很大,很麻烦而且效果并不好。
我们怎么样优化呢?其实可以将动画分割成更小的部分,每秒60帧关键帧,然后可以直接使用直线来拼接这些曲线,那每一帧我们的动画如何展示呢,我们需要变化的属性值如何确定呢?这就需要CoreAnimation的插值机制。

float interpolate(float from, float to, float time)
{
    return (to - from) * time + from;
}

- (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time
{
    if ([fromValue isKindOfClass:[NSValue class]]) {
       //get type
        const char *type = [fromValue objCType];
        if (strcmp(type, @encode(CGPoint)) == 0) {
            CGPoint from = [fromValue CGPointValue];
            CGPoint to = [toValue CGPointValue];
            CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time));
            return [NSValue valueWithCGPoint:result];
        }
    }
    //provide safe default implementation
    return (time < 0.5)? fromValue: toValue;
}

- (void)animate
{
    //reset ball to top of screen
    self.ballView.center = CGPointMake(150, 32);
    //set up animation parameters
    NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)];
    NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)];
    CFTimeInterval duration = 1.0;
    //generate keyframes
    NSInteger numFrames = duration * 60;
   NSMutableArray *frames = [NSMutableArray array];
    for (int i = 0; i < numFrames; i++) {
        float time = 1 / (float)numFrames * i;
        [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]];
    }
    //create keyframe animation
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = frames;
    //apply animation
    [self.ballView.layer addAnimation:animation forKey:nil];
}

至于如何计算每一帧的函数值,这就需要数学公式等,并不简单,好在我们有一个关于缓冲函数的网页,http://www.robertpenner.com/easing,这个网站包含了大多数普遍的缓冲函数实现。

Comments
Write a Comment