iOS核心动画高级技巧--(十)缓冲

在第九章“图层时间”中,我们讨论了动画时间和 CAMediaTiming协议。现在我们 来看一下另一个和时间相关的机制--所谓的缓冲。Core Animation使用缓冲来使动 画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如 何对你的动画控制和自定义缓冲曲线。

动画速度

动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来:

velocity = change / time

这里的变化可以指的是一个物体移动的距离,时间指动画持续的时长,用这样的一 个移动可以更加形象的描述(比如 positionbounds 属性的动画),但实际 上它应用于任意可以做动画的属性(比如 coloropacity)。

上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章“显式动 画”的情况),对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度 而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。

考虑一个场景,一辆车行驶在一定距离内,它并不会一开始就以60mph的速度行 驶,然后到达终点后突然变成0mph。一是因为需要无限大的加速度(即使是最好 的车也不会在0秒内从0跑到60),另外不然的话会干死所有乘客。在现实中,它会 慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下 来。

那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。

现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实 现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然 而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。

CAMediaTimingFunction

那么该如何使用缓冲方程式呢?首先需要设置 CAAnimationtimingFunction 属性,是 CAMediaTimingFunction 类的 一个对象。如果想改变隐式动画的计时函数,同样也可以使
CATransaction+setAnimationTimingFunction:方法。

这里有一些方式来创建CAMediaTimingFunction ,最简单的方式是调 用+ timingFunctionWithName:的构造方法。这里传入如下几个常量之一:

kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault

kCAMediaTimingFunctionLinear选项创建了一个线性的计时函数,同样也是CAAnimationtimingFunction 属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。

kCAMediaTimingFunctionEaseIn 常量创建了一个慢慢加速然后突然停止的方 法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的 发射。

kCAMediaTimingFunctionEaseOut则恰恰相反,它以一个全速开始,然后慢慢 减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地 一声。

kCAMediaTimingFunctionEaseInEaseOut 创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。 如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默 认的选择,实际上当使用 UIView的动画方法时,他的确是默认的,但当创建CAAnimation的时候,就需要手动设置它了。

最后还有一个kCAMediaTimingFunctionDefault ,它和kCAMediaTimingFunctionEaseInEaseOut 很类似,但是加速和减速的过程都 稍微有些慢。它和 kCAMediaTimingFunctionEaseInEaseOut 的区别很难察觉, 可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使 用 kCAMediaTimingFunctionEaseInEaseOut作为默认效果),虽然它的名字说 是默认的,但还是要记住当创建显式的CAAnimation 它并不是默认选项(换句话 说,默认的图层行为动画用kCAMediaTimingFunctionDefault 作为它们的计时方法)。

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong)CALayer *colorLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
    self.colorLayer.position = CGPointMake(self.view.bounds.size.width * 0.5, self.view.bounds.size.height * 0.5);
    self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
    [self.view.layer addSublayer:self.colorLayer];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [CATransaction begin];
    [CATransaction setAnimationDuration:1.0];
    [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:@"easeInEaseOut"]];
    self.colorLayer.position = [[touches anyObject] locationInView:self.view];
    [CATransaction commit];
}
@end
缓冲函数的简单测试效果.gif
UIView 的动画缓冲

UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改
UIView动画的缓冲选项,给 options参数添加如下常量之一:

UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear

它们和 CAMediaTimingFunction 紧密关联,UIViewAnimationOptionCurveEaseInOut 是默认值(这里没 有kCAMediaTimingFunctionDefault相对应的值了)。

使用UIKit动画的缓冲测试工程

#import "ViewController.h"

@interface ViewController ()
@property (nonatomic, strong)UIView *colorView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.colorView = [[UIView alloc]init];
    self.colorView.bounds = CGRectMake(0, 0, 100, 100);
    self.colorView.center = self.view.center;
    self.colorView.backgroundColor = [UIColor redColor];
    [self.view addSubview:self.colorView];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    [UIView animateWithDuration:1.0 delay:0.0 options:UIViewAnimationOptionCurveEaseOut animations:^{
        self.colorView.center = [[touches anyObject] locationInView:self.view];
    } completion:nil];
    
}
@end
缓冲和关键帧动画

或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合 适的缓冲方法,例如 kCAMediaTimingFunctionEaseIn,给图层的颜色变化添加 一点脉冲效果,让它更像现实中的一个彩色灯泡。

我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓
冲,于是每次颜色的变换都会有脉冲效果。

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

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *colorView;
@property (nonatomic, strong)CALayer *colorLayer;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    self.colorLayer = [CALayer layer];
    self.colorLayer.frame = CGRectMake(0, 0, self.colorView.bounds.size.width, self.colorView.bounds.size.height);
    self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
    [self.colorView.layer addSublayer:self.colorLayer];
}


- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    CAKeyframeAnimation *keyAnimation = [CAKeyframeAnimation animation];
    keyAnimation.keyPath = @"backgroundColor";
    keyAnimation.duration = 2.0;
    keyAnimation.values = @[
                            (__bridge id) [UIColor blueColor].CGColor,
                            (__bridge id)[UIColor redColor].CGColor,
                            (__bridge id)[UIColor greenColor].CGColor,
                            (__bridge id)[UIColor blueColor].CGColor
                            ];
    
    CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName:@"easeIn"];
    keyAnimation.timingFunctions = @[fn,fn,fn];
    [self.colorLayer addAnimation:keyAnimation forKey:nil];
    
}
@end
CAKeyframeAnimation 使用 CAMediaTimingFunction效果.gif
自定义缓冲函数

在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢?
除了+functionWithName:之外,CAMediaTimingFunction 同样有另一个构造 函数,一个有四个浮点参数的+ functionWithControlPoints:::: (注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。
使用这个方法,我们可以创建一个自定义的缓冲函数,来匹配我们的时钟动画,为了理解如何使用这个方法,我们要了解一些 CAMediaTimingFunction 是如何工作 的。

三次贝塞尔曲线

CAMediaTimingFunction 函数的主要原则在于它把输入的时间转换成起点和终点 之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表 改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线。

线性缓冲函数的图像.png

这条曲线的斜率代表了速度,斜率的改变代表了加速度,原则上来说,任何加速的 曲线都可以用这种图像来表示,但是CAMediaTimingFunction使用了一个叫做三次贝塞尔曲线的函数,它只可以产出指定缓冲函数的子集(我们之前在第八章中创 建CAKeyframeAnimation 路径的时候提到过三次贝塞尔曲线)。

你或许会回想起,一个三次贝塞尔曲线通过四个点来定义,第一个和最后一个点代
表了曲线的起点和终点,剩下中间两个点叫做控制点,因为它们控制了曲线的形
状,贝塞尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过
它们。你可以把它们想象成吸引经过它们曲线的磁铁。

三次贝塞尔缓冲函数.png

实际上它是一个很奇怪的函数,先加速,然后减速,最后快到达终点的时候又加
速,那么标准的缓冲函数又该如何用图像来表示呢?

CAMediaTimingFunction 有一个叫做 -getControlPointAtIndex:values: 的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果 能回答为什么不简单返回一个CGPoint ),但是使用它我们可以找到标准缓冲函数的点,然后用UIBezierPathCAShapeLayer 来把它画出来。

曲线的起始和终点始终是{0, 0}和{1, 1},于是我们只需要检索曲线的第二个和第三 个点(控制点)。

更加复杂的动画曲线

考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,然后经过几次反弹,最后停下来。如果用一张图来说明,它会如图10.5 所示。

描述的反弹的动画.png

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

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

为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个 关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过 keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。

#import "ViewController.h"

@interface ViewController ()
@property (weak, nonatomic) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    UIImage *image = [UIImage imageNamed:@"ball.png"];
    self.ballView= [[UIImageView alloc]initWithImage:image];
    self.ballView.contentMode = UIViewContentModeScaleToFill;
    [self.containerView addSubview:self.ballView];
    
    [self animate];
}

- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event{
    
    [self animate];
}

- (void)animate{
    self.ballView.center = CGPointMake(150, 32);
    
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
    animation.keyPath = @"position";
    animation.duration = 1.0;
    animation.delegate = self;
    animation.values = @[
                         [NSValue valueWithCGPoint:CGPointMake(150, 140)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 130)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 120)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 110)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 100)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 90)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 80)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 70)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 60)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 50)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 40)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 30)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 20)],
                         [NSValue valueWithCGPoint:CGPointMake(150, 10)],
                         ];
    
    
    animation.timingFunctions =  @[
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"],
                                   [CAMediaTimingFunction functionWithName:@"easeOut"]
                                   ];
    animation.keyTimes = @[@0.0,@0.2,@0.3,@0.4,@0.45,@0.5,@0.6,@0.65,@0.7,@0.8,@0.9];
    self.ballView.layer.position = CGPointMake(150, 268);
    [self.ballView.layer addAnimation:animation forKey:nil];
}
@end
使用关键帧实现反弹球的动画效果.gif

这种方式还算不错,但是实现起来略显笨重(因为要不停地尝试计算各种关键帧和时间偏移)并且和动画强制绑定了(因为如果要改变动画的一个属性,那就意味着要
重新计算所有的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性
动画转换成关键帧动画呢,下面我们来实现它。

流程自动化

我们把动画分割成相当大的几块,然后用Core Animation的缓冲进 入和缓冲退出函数来大约形成我们想要的曲线。但如果我们把动画分割成更小的几 部分,那么我们就可以用直线来拼接这些曲线(也就是线性缓冲)。为了实现自动 化,我们需要知道如何做如下两件事情:

  • 自动把任意属性动画分割成多个关键帧
  • 用一个数学函数表示弹性动画,使得可以对帧做便宜

为了解决第一个问题,我们需要复制Core Animation的插值机制。这是一个传入起 点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点 起始值,公式如下(假设时间从0到1):

 value = (endValue – startValue) × time + startValue;

那么如果要插入一个类似于 CGPointCGColorRef 或者CATransform3D这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就CGPoint中的xy值, CGColorRef 中的绿透明值,或者CATransfrom3D中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对 象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。

一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分 割成许多独立的关键帧,然后产出一个线性的关键帧动画。

注意到我们用了60 x 动画时间(秒做单位)作为关键帧的个数这是因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果我们每秒生成60个关键帧,就 可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效 果)。

我们在示例中仅仅引入了对 CGPoint 类型的插值代码。但是,从代码中很清楚能 看出如何扩展成支持别的类型。作为不能识别类型的备选方案,我们仅仅在前一半 返回了 fromValue,在后一半返回了toValue

总结

在这一章中,我们了解了有关缓冲和 CAMediaTimingFunction类,它可以允许我 们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用 CAKeyframeAnimation来避开 CAMediaTimingFunction 的限制,创建完全 自定义的缓冲函数。

iOS核心动画高级技巧--目录

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容

  • 本文转载自:http://www.cocoachina.com/ios/20150105/10829.html 为...
    idiot_lin阅读 436评论 0 0
  • 生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿 在第九章“图层时间”中,我们讨论了动画时间和CA...
    雪_晟阅读 447评论 0 0
  • 缓冲 生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿 在第九章“图层时间”中,我们讨论了动画时间...
    方圆几度阅读 452评论 0 0
  • 以下三个是最经常被问到的,基本上属于介绍性的题目,无所谓正确答案,在我看来,这些不算真正的问题。 Discuss ...
    蜀湘情缘阅读 5,945评论 0 8
  • 每当夜深人静,我总无法正常入眠,心绪起伏不定,幼年时的噩梦,虽已远离,每个夜晚,仍旧布满了恐惧,我不知道下一个瞬间...
    韩易辰阅读 129评论 0 0