>生活和藝術(shù)一樣躯概,最美的永遠(yuǎn)是曲線(xiàn)。--愛(ài)德華布爾沃-利頓
在第九章“圖層時(shí)間”中畔师,我們討論了動(dòng)畫(huà)時(shí)間和`CAMediaTiming`協(xié)議∪⒚遥現(xiàn)在我們來(lái)看一下另一個(gè)和時(shí)間相關(guān)的機(jī)制--所謂的*緩沖*。Core Animation使用緩沖來(lái)使動(dòng)畫(huà)移動(dòng)更平滑更自然看锉,而不是看起來(lái)的那種機(jī)械和人工姿锭,在這一章我們將要研究如何對(duì)你的動(dòng)畫(huà)控制和自定義緩沖曲線(xiàn)。
##動(dòng)畫(huà)速度
動(dòng)畫(huà)實(shí)際上就是一段時(shí)間內(nèi)的變化伯铣,這就暗示了變化一定是隨著某個(gè)特定的速率進(jìn)行呻此。速率由以下公式計(jì)算而來(lái):
velocity = change / time
這里的*變化*可以指的是一個(gè)物體移動(dòng)的距離,*時(shí)間*指動(dòng)畫(huà)持續(xù)的時(shí)長(zhǎng)腔寡,用這樣的一個(gè)移動(dòng)可以更加形象的描述(比如`position`和`bounds`屬性的動(dòng)畫(huà))焚鲜,但實(shí)際上它應(yīng)用于任意可以做動(dòng)畫(huà)的屬性(比如`color`和`opacity`)。
上面的等式假設(shè)了速度在整個(gè)動(dòng)畫(huà)過(guò)程中都是恒定不變的(就如同第八章“顯式動(dòng)畫(huà)”的情況)放前,對(duì)于這種恒定速度的動(dòng)畫(huà)我們稱(chēng)之為“線(xiàn)性步調(diào)”忿磅,而且從技術(shù)的角度而言這也是實(shí)現(xiàn)動(dòng)畫(huà)最簡(jiǎn)單的方式,但也是*完全不真實(shí)*的一種效果凭语。
考慮一個(gè)場(chǎng)景葱她,一輛車(chē)行駛在一定距離內(nèi),它并不會(huì)一開(kāi)始就以60mph的速度行駛似扔,然后到達(dá)終點(diǎn)后突然變成0mph吨些。一是因?yàn)樾枰獰o(wú)限大的加速度(即使是最好的車(chē)也不會(huì)在0秒內(nèi)從0跑到60),另外不然的話(huà)會(huì)干死所有乘客炒辉。在現(xiàn)實(shí)中豪墅,它會(huì)慢慢地加速到全速,然后當(dāng)它接近終點(diǎn)的時(shí)候辆脸,它會(huì)慢慢地減速但校,直到最后停下來(lái)。
那么對(duì)于一個(gè)掉落到地上的物體又會(huì)怎樣呢啡氢?它會(huì)首先停在空中状囱,然后一直加速到落到地面,然后突然停止(然后由于積累的動(dòng)能轉(zhuǎn)換伴隨著一聲巨響倘是,砰Mぜ稀)。
現(xiàn)實(shí)生活中的任何一個(gè)物體都會(huì)在運(yùn)動(dòng)中加速或者減速搀崭。那么我們?nèi)绾卧趧?dòng)畫(huà)中實(shí)現(xiàn)這種加速度呢叨粘?一種方法是使用*物理引擎*來(lái)對(duì)運(yùn)動(dòng)物體的摩擦和動(dòng)量來(lái)建模猾编,然而這會(huì)使得計(jì)算過(guò)于復(fù)雜。我們稱(chēng)這種類(lèi)型的方程為*緩沖函數(shù)*升敲,幸運(yùn)的是答倡,Core Animation內(nèi)嵌了一系列標(biāo)準(zhǔn)函數(shù)提供給我們使用。
###`CAMediaTimingFunction`
那么該如何使用緩沖方程式呢驴党?首先需要設(shè)置`CAAnimation`的`timingFunction`屬性瘪撇,是`CAMediaTimingFunction`類(lèi)的一個(gè)對(duì)象。如果想改變隱式動(dòng)畫(huà)的計(jì)時(shí)函數(shù)港庄,同樣也可以使用`CATransaction`的`+setAnimationTimingFunction:`方法倔既。
這里有一些方式來(lái)創(chuàng)建`CAMediaTimingFunction`,最簡(jiǎn)單的方式是調(diào)用`+timingFunctionWithName:`的構(gòu)造方法鹏氧。這里傳入如下幾個(gè)常量之一:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
`kCAMediaTimingFunctionLinear`選項(xiàng)創(chuàng)建了一個(gè)線(xiàn)性的計(jì)時(shí)函數(shù)渤涌,同樣也是`CAAnimation`的`timingFunction`屬性為空時(shí)候的默認(rèn)函數(shù)。線(xiàn)性步調(diào)對(duì)于那些立即加速并且保持勻速到達(dá)終點(diǎn)的場(chǎng)景會(huì)有意義(例如射出槍膛的子彈)把还,但是默認(rèn)來(lái)說(shuō)它看起來(lái)很奇怪实蓬,因?yàn)閷?duì)大多數(shù)的動(dòng)畫(huà)來(lái)說(shuō)確實(shí)很少用到。
`kCAMediaTimingFunctionEaseIn`常量創(chuàng)建了一個(gè)慢慢加速然后突然停止的方法笨篷。對(duì)于之前提到的自由落體的例子來(lái)說(shuō)很適合姻成,或者比如對(duì)準(zhǔn)一個(gè)目標(biāo)的導(dǎo)彈的發(fā)射肄程。
`kCAMediaTimingFunctionEaseOut`則恰恰相反,它以一個(gè)全速開(kāi)始,然后慢慢減速停止放棒。它有一個(gè)削弱的效果鸽心,應(yīng)用的場(chǎng)景比如一扇門(mén)慢慢地關(guān)上墓猎,而不是砰地一聲卷胯。
`kCAMediaTimingFunctionEaseInEaseOut`創(chuàng)建了一個(gè)慢慢加速然后再慢慢減速的過(guò)程。這是現(xiàn)實(shí)世界大多數(shù)物體移動(dòng)的方式辜贵,也是大多數(shù)動(dòng)畫(huà)來(lái)說(shuō)最好的選擇悯蝉。如果只可以用一種緩沖函數(shù)的話(huà),那就必須是它了托慨。那么你會(huì)疑惑為什么這不是默認(rèn)的選擇鼻由,實(shí)際上當(dāng)使用`UIView`的動(dòng)畫(huà)方法時(shí),他的確是默認(rèn)的厚棵,但當(dāng)創(chuàng)建`CAAnimation`的時(shí)候蕉世,就需要手動(dòng)設(shè)置它了。
最后還有一個(gè)`kCAMediaTimingFunctionDefault`婆硬,它和`kCAMediaTimingFunctionEaseInEaseOut`很類(lèi)似狠轻,但是加速和減速的過(guò)程都稍微有些慢。它和`kCAMediaTimingFunctionEaseInEaseOut`的區(qū)別很難察覺(jué)彬犯,可能是蘋(píng)果覺(jué)得它對(duì)于隱式動(dòng)畫(huà)來(lái)說(shuō)更適合(然后對(duì)UIKit就改變了想法向楼,而是使用`kCAMediaTimingFunctionEaseInEaseOut`作為默認(rèn)效果)查吊,雖然它的名字說(shuō)是默認(rèn)的,但還是要記住當(dāng)創(chuàng)建*顯式*的`CAAnimation`它并不是默認(rèn)選項(xiàng)(換句話(huà)說(shuō)湖蜕,默認(rèn)的圖層行為動(dòng)畫(huà)用`kCAMediaTimingFunctionDefault`作為它們的計(jì)時(shí)方法)逻卖。
你可以使用一個(gè)簡(jiǎn)單的測(cè)試工程來(lái)實(shí)驗(yàn)一下(清單10.1),在運(yùn)行之前改變緩沖函數(shù)的代碼昭抒,然后點(diǎn)擊任何地方來(lái)觀察圖層是如何通過(guò)指定的緩沖移動(dòng)的箭阶。
清單10.1緩沖函數(shù)的簡(jiǎn)單測(cè)試
```objective-c
@interface ViewController ()
@property (nonatomic, strong) CALayer *colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a red layer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(0, 0, 100, 100);
self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//configure the transaction
[CATransaction begin];
[CATransaction setAnimationDuration:1.0];
[CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]];
//set the position
self.colorLayer.position = [[touches anyObject] locationInView:self.view];
//commit transaction
[CATransaction commit];
}
@end
```
###`UIView`的動(dòng)畫(huà)緩沖
UIKit的動(dòng)畫(huà)也同樣支持這些緩沖方法的使用,盡管語(yǔ)法和常量有些不同戈鲁,為了改變`UIView`動(dòng)畫(huà)的緩沖選項(xiàng),給`options`參數(shù)添加如下常量之一:
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
它們和`CAMediaTimingFunction`緊密關(guān)聯(lián)嘹叫,`UIViewAnimationOptionCurveEaseInOut`是默認(rèn)值(這里沒(méi)有`kCAMediaTimingFunctionDefault`相對(duì)應(yīng)的值了)婆殿。
具體使用方法見(jiàn)清單10.2(注意到這里不再使用`UIView`額外添加的圖層,因?yàn)閁IKit的動(dòng)畫(huà)并不支持這類(lèi)圖層)罩扇。
清單10.2使用UIKit動(dòng)畫(huà)的緩沖測(cè)試工程
```objective-c
@interface ViewController ()
@property (nonatomic, strong) UIView *colorView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a red layer
self.colorView = [[UIView alloc] init];
self.colorView.bounds = CGRectMake(0, 0, 100, 100);
self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2);
self.colorView.backgroundColor = [UIColor redColor];
[self.view addSubview:self.colorView];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//perform the animation
[UIView animateWithDuration:1.0 delay:0.0
options:UIViewAnimationOptionCurveEaseOut
animations:^{
//set the position
self.colorView.center = [[touches anyObject] locationInView:self.view];
}
completion:NULL];
}
@end
```
###緩沖和關(guān)鍵幀動(dòng)畫(huà)
或許你會(huì)回想起第八章里面顏色切換的關(guān)鍵幀動(dòng)畫(huà)由于線(xiàn)性變換的原因(見(jiàn)清單8.5)看起來(lái)有些奇怪婆芦,使得顏色變換非常不自然。為了糾正這點(diǎn)喂饥,我們來(lái)用更加合適的緩沖方法消约,例如`kCAMediaTimingFunctionEaseIn`,給圖層的顏色變化添加一點(diǎn)*脈沖*效果员帮,讓它更像現(xiàn)實(shí)中的一個(gè)彩色燈泡或粮。
我們不想給整個(gè)動(dòng)畫(huà)過(guò)程應(yīng)用這個(gè)效果,我們希望對(duì)每個(gè)動(dòng)畫(huà)的過(guò)程重復(fù)這樣的緩沖捞高,于是每次顏色的變換都會(huì)有脈沖效果氯材。
`CAKeyframeAnimation`有一個(gè)`NSArray`類(lèi)型的`timingFunctions`屬性,我們可以用它來(lái)對(duì)每次動(dòng)畫(huà)的步驟指定不同的計(jì)時(shí)函數(shù)硝岗。但是指定函數(shù)的個(gè)數(shù)一定要等于`keyframes`數(shù)組的元素個(gè)數(shù)*減一*氢哮,因?yàn)樗敲枋雒恳粠g動(dòng)畫(huà)速度的函數(shù)。
在這個(gè)例子中型檀,我們自始至終想使用同一個(gè)緩沖函數(shù)冗尤,但我們同樣需要一個(gè)函數(shù)的數(shù)組來(lái)告訴動(dòng)畫(huà)不停地重復(fù)每個(gè)步驟,而不是在整個(gè)動(dòng)畫(huà)序列只做一次緩沖胀溺,我們簡(jiǎn)單地使用包含多個(gè)相同函數(shù)拷貝的數(shù)組就可以了(見(jiàn)清單10.3)裂七。
運(yùn)行更新后的代碼,你會(huì)發(fā)現(xiàn)動(dòng)畫(huà)看起來(lái)更加自然了月幌。
清單10.3對(duì)`CAKeyframeAnimation`使用`CAMediaTimingFunction`
```objective-c
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) IBOutlet CALayer *colorLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create sublayer
self.colorLayer = [CALayer layer];
self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
self.colorLayer.backgroundColor = [UIColor blueColor].CGColor;
//add it to our view
[self.layerView.layer addSublayer:self.colorLayer];
}
- (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];
}
@end
```
##自定義緩沖函數(shù)
在第八章中碍讯,我們給時(shí)鐘項(xiàng)目添加了動(dòng)畫(huà)〕短桑看起來(lái)很贊捉兴,但是如果有合適的緩沖函數(shù)就更好了蝎困。在顯示世界中,鐘表指針轉(zhuǎn)動(dòng)的時(shí)候倍啥,通常起步很慢禾乘,然后迅速啪地一聲,最后緩沖到終點(diǎn)虽缕。但是標(biāo)準(zhǔn)的緩沖函數(shù)在這里每一個(gè)適合它始藕,那該如何創(chuàng)建一個(gè)新的呢?
除了`+functionWithName:`之外氮趋,`CAMediaTimingFunction`同樣有另一個(gè)構(gòu)造函數(shù)伍派,一個(gè)有四個(gè)浮點(diǎn)參數(shù)的`+functionWithControlPoints::::`(注意這里奇怪的語(yǔ)法,并沒(méi)有包含具體每個(gè)參數(shù)的名稱(chēng)剩胁,這在objective-C中是合法的诉植,但是卻違反了蘋(píng)果對(duì)方法命名的指導(dǎo)方針,而且看起來(lái)是一個(gè)奇怪的設(shè)計(jì))昵观。
使用這個(gè)方法晾腔,我們可以創(chuàng)建一個(gè)自定義的緩沖函數(shù),來(lái)匹配我們的時(shí)鐘動(dòng)畫(huà)啊犬,為了理解如何使用這個(gè)方法灼擂,我們要了解一些`CAMediaTimingFunction`是如何工作的。
###三次貝塞爾曲線(xiàn)
`CAMediaTimingFunction`函數(shù)的主要原則在于它把輸入的時(shí)間轉(zhuǎn)換成起點(diǎn)和終點(diǎn)之間成比例的改變觉至。我們可以用一個(gè)簡(jiǎn)單的圖標(biāo)來(lái)解釋?zhuān)瑱M軸代表時(shí)間剔应,縱軸代表改變的量,于是線(xiàn)性的緩沖就是一條從起點(diǎn)開(kāi)始的簡(jiǎn)單的斜線(xiàn)(圖10.1)语御。
圖10.1線(xiàn)性緩沖函數(shù)的圖像
這條曲線(xiàn)的斜率代表了速度领斥,斜率的改變代表了加速度,原則上來(lái)說(shuō)沃暗,任何加速的曲線(xiàn)都可以用這種圖像來(lái)表示月洛,但是`CAMediaTimingFunction`使用了一個(gè)叫做*三次貝塞爾曲線(xiàn)*的函數(shù),它只可以產(chǎn)出指定緩沖函數(shù)的子集(我們之前在第八章中創(chuàng)建`CAKeyframeAnimation`路徑的時(shí)候提到過(guò)三次貝塞爾曲線(xiàn))孽锥。
你或許會(huì)回想起嚼黔,一個(gè)三次貝塞爾曲線(xiàn)通過(guò)四個(gè)點(diǎn)來(lái)定義,第一個(gè)和最后一個(gè)點(diǎn)代表了曲線(xiàn)的起點(diǎn)和終點(diǎn)惜辑,剩下中間兩個(gè)點(diǎn)叫做*控制點(diǎn)*唬涧,因?yàn)樗鼈兛刂屏饲€(xiàn)的形狀,貝塞爾曲線(xiàn)的控制點(diǎn)其實(shí)是位于曲線(xiàn)之外的點(diǎn)盛撑,也就是說(shuō)曲線(xiàn)并不一定要穿過(guò)它們碎节。你可以把它們想象成吸引經(jīng)過(guò)它們曲線(xiàn)的磁鐵。
圖10.2展示了一個(gè)三次貝塞爾緩沖函數(shù)的例子
圖10.2三次貝塞爾緩沖函數(shù)
實(shí)際上它是一個(gè)很奇怪的函數(shù)抵卫,先加速狮荔,然后減速胎撇,最后快到達(dá)終點(diǎn)的時(shí)候又加速,那么標(biāo)準(zhǔn)的緩沖函數(shù)又該如何用圖像來(lái)表示呢殖氏?
`CAMediaTimingFunction`有一個(gè)叫做`-getControlPointAtIndex:values:`的方法晚树,可以用來(lái)檢索曲線(xiàn)的點(diǎn),這個(gè)方法的設(shè)計(jì)的確有點(diǎn)奇怪(或許也就只有蘋(píng)果能回答為什么不簡(jiǎn)單返回一個(gè)`CGPoint`)雅采,但是使用它我們可以找到標(biāo)準(zhǔn)緩沖函數(shù)的點(diǎn)爵憎,然后用`UIBezierPath`和`CAShapeLayer`來(lái)把它畫(huà)出來(lái)。
曲線(xiàn)的起始和終點(diǎn)始終是{0, 0}和{1, 1}婚瓜,于是我們只需要檢索曲線(xiàn)的第二個(gè)和第三個(gè)點(diǎn)(控制點(diǎn))宝鼓。具體代碼見(jiàn)清單10.4。所有的標(biāo)準(zhǔn)緩沖函數(shù)的圖像見(jiàn)圖10.3巴刻。
清單10.4使用`UIBezierPath`繪制`CAMediaTimingFunction`
```objective-c
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create timing function
CAMediaTimingFunction *function = CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut];
//get control points
CGPoint controlPoint1, controlPoint2;
[function getControlPointAtIndex:1 values:(float *)&controlPoint1];
[function getControlPointAtIndex:2 values:(float *)&controlPoint2];
//create curve
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointZero];
[path addCurveToPoint:CGPointMake(1, 1)
controlPoint1:controlPoint1 controlPoint2:controlPoint2];
//scale the path up to a reasonable size for display
[path applyTransform:CGAffineTransformMakeScale(200, 200)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 4.0f;
shapeLayer.path = path.CGPath;
[self.layerView.layer addSublayer:shapeLayer];
//flip geometry so that 0,0 is in the bottom-left
self.layerView.layer.geometryFlipped = YES;
}
@end
```
圖10.3標(biāo)準(zhǔn)`CAMediaTimingFunction`緩沖曲線(xiàn)
那么對(duì)于我們自定義時(shí)鐘指針的緩沖函數(shù)來(lái)說(shuō)席函,我們需要初始微弱,然后迅速上升冈涧,最后緩沖到終點(diǎn)的曲線(xiàn),通過(guò)一些實(shí)驗(yàn)之后正蛙,最終結(jié)果如下:
[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
如果把它轉(zhuǎn)換成緩沖函數(shù)的圖像督弓,最后如圖10.4所示,如果把它添加到時(shí)鐘的程序乒验,就形成了之前一直期待的非常贊的效果(見(jiàn)代清單10.5)愚隧。
圖10.4自定義適合時(shí)鐘的緩沖函數(shù)
清單10.5添加了自定義緩沖函數(shù)的時(shí)鐘程序
```objective-c
- (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated
{
//generate transform
CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1);
if (animated) {
//create transform animation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform";
animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"];
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.duration = 0.5;
animation.delegate = self;
animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];
//apply animation
handView.layer.transform = transform;
[handView.layer addAnimation:animation forKey:nil];
} else {
//set transform directly
handView.layer.transform = transform;
}
}
```
###更加復(fù)雜的動(dòng)畫(huà)曲線(xiàn)
考慮一個(gè)橡膠球掉落到堅(jiān)硬的地面的場(chǎng)景,當(dāng)開(kāi)始下落的時(shí)候锻全,它會(huì)持續(xù)加速知道落到地面狂塘,然后經(jīng)過(guò)幾次反彈,最后停下來(lái)鳄厌。如果用一張圖來(lái)說(shuō)明荞胡,它會(huì)如圖10.5所示。
圖10.5一個(gè)沒(méi)法用三次貝塞爾曲線(xiàn)描述的反彈的動(dòng)畫(huà)
這種效果沒(méi)法用一個(gè)簡(jiǎn)單的三次貝塞爾曲線(xiàn)表示了嚎,于是不能用`CAMediaTimingFunction`來(lái)完成泪漂。但如果想要實(shí)現(xiàn)這樣的效果,可以用如下幾種方法:
*用`CAKeyframeAnimation`創(chuàng)建一個(gè)動(dòng)畫(huà)歪泳,然后分割成幾個(gè)步驟萝勤,每個(gè)小步驟使用自己的計(jì)時(shí)函數(shù)(具體下節(jié)介紹)。
*使用定時(shí)器逐幀更新實(shí)現(xiàn)動(dòng)畫(huà)(見(jiàn)第11章呐伞,“基于定時(shí)器的動(dòng)畫(huà)”)敌卓。
###基于關(guān)鍵幀的緩沖
為了使用關(guān)鍵幀實(shí)現(xiàn)反彈動(dòng)畫(huà),我們需要在緩沖曲線(xiàn)中對(duì)每一個(gè)顯著的點(diǎn)創(chuàng)建一個(gè)關(guān)鍵幀(在這個(gè)情況下伶氢,關(guān)鍵點(diǎn)也就是每次反彈的峰值)趟径,然后應(yīng)用緩沖函數(shù)把每段曲線(xiàn)連接起來(lái)瘪吏。同時(shí),我們也需要通過(guò)`keyTimes`來(lái)指定每個(gè)關(guān)鍵幀的時(shí)間偏移舵抹,由于每次反彈的時(shí)間都會(huì)減少肪虎,于是關(guān)鍵幀并不會(huì)均勻分布。
清單10.6展示了實(shí)現(xiàn)反彈球動(dòng)畫(huà)的代碼(見(jiàn)圖10.6)
清單10.6使用關(guān)鍵幀實(shí)現(xiàn)反彈球的動(dòng)畫(huà)
```objective-c
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@property (nonatomic, strong) UIImageView *ballView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//add ball image view
UIImage *ballImage = [UIImage imageNamed:@"Ball.png"];
self.ballView = [[UIImageView alloc] initWithImage:ballImage];
[self.containerView addSubview:self.ballView];
//animate
[self animate];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
//replay animation on tap
[self animate];
}
- (void)animate
{
//reset ball to top of screen
self.ballView.center = CGPointMake(150, 32);
//create keyframe animation
CAKeyframeAnimation *animation = [CAKeyframeAnimation animation];
animation.keyPath = @"position";
animation.duration = 1.0;
animation.delegate = self;
animation.values = @[
[NSValue valueWithCGPoint:CGPointMake(150, 32)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)],
[NSValue valueWithCGPoint:CGPointMake(150, 140)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)],
[NSValue valueWithCGPoint:CGPointMake(150, 220)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)],
[NSValue valueWithCGPoint:CGPointMake(150, 250)],
[NSValue valueWithCGPoint:CGPointMake(150, 268)]
];
animation.timingFunctions = @[
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut],
[CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]
];
animation.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0];
//apply animation
self.ballView.layer.position = CGPointMake(150, 268);
[self.ballView.layer addAnimation:animation forKey:nil];
}
@end
```
圖10.6使用關(guān)鍵幀實(shí)現(xiàn)的反彈球動(dòng)畫(huà)
這種方式還算不錯(cuò)惧蛹,但是實(shí)現(xiàn)起來(lái)略顯笨重(因?yàn)橐煌5貒L試計(jì)算各種關(guān)鍵幀和時(shí)間偏移)并且和動(dòng)畫(huà)強(qiáng)綁定了(因?yàn)槿绻淖儎?dòng)畫(huà)的一個(gè)屬性扇救,那就意味著要重新計(jì)算所有的關(guān)鍵幀)。那該如何寫(xiě)一個(gè)方法香嗓,用緩沖函數(shù)來(lái)把任何簡(jiǎn)單的屬性動(dòng)畫(huà)轉(zhuǎn)換成關(guān)鍵幀動(dòng)畫(huà)呢迅腔,下面我們來(lái)實(shí)現(xiàn)它。
###流程自動(dòng)化
在清單10.6中靠娱,我們把動(dòng)畫(huà)分割成相當(dāng)大的幾塊沧烈,然后用Core Animation的緩沖進(jìn)入和緩沖退出函數(shù)來(lái)大約形成我們想要的曲線(xiàn)。但如果我們把動(dòng)畫(huà)分割成更小的幾部分像云,那么我們就可以用直線(xiàn)來(lái)拼接這些曲線(xiàn)(也就是線(xiàn)性緩沖)锌雀。為了實(shí)現(xiàn)自動(dòng)化,我們需要知道如何做如下兩件事情:
*自動(dòng)把任意屬性動(dòng)畫(huà)分割成多個(gè)關(guān)鍵幀
*用一個(gè)數(shù)學(xué)函數(shù)表示彈性動(dòng)畫(huà)迅诬,使得可以對(duì)幀做便宜
為了解決第一個(gè)問(wèn)題腋逆,我們需要復(fù)制Core Animation的插值機(jī)制。這是一個(gè)傳入起點(diǎn)和終點(diǎn)侈贷,然后在這兩個(gè)點(diǎn)之間指定時(shí)間點(diǎn)產(chǎn)出一個(gè)新點(diǎn)的機(jī)制惩歉。對(duì)于簡(jiǎn)單的浮點(diǎn)起始值,公式如下(假設(shè)時(shí)間從0到1):
value = (endValue – startValue) × time + startValue;
那么如果要插入一個(gè)類(lèi)似于`CGPoint`俏蛮,`CGColorRef`或者`CATransform3D`這種更加復(fù)雜類(lèi)型的值撑蚌,我們可以簡(jiǎn)單地對(duì)每個(gè)獨(dú)立的元素應(yīng)用這個(gè)方法(也就`CGPoint`中的x和y值,`CGColorRef`中的紅搏屑,藍(lán)争涌,綠,透明值辣恋,或者是`CATransform3D`中獨(dú)立矩陣的坐標(biāo))第煮。我們同樣需要一些邏輯在插值之前對(duì)對(duì)象拆解值,然后在插值之后在重新封裝成對(duì)象抑党,也就是說(shuō)需要實(shí)時(shí)地檢查類(lèi)型包警。
一旦我們可以用代碼獲取屬性動(dòng)畫(huà)的起始值之間的任意插值,我們就可以把動(dòng)畫(huà)分割成許多獨(dú)立的關(guān)鍵幀底靠,然后產(chǎn)出一個(gè)線(xiàn)性的關(guān)鍵幀動(dòng)畫(huà)害晦。清單10.7展示了相關(guān)代碼。
注意到我們用了60 x動(dòng)畫(huà)時(shí)間(秒做單位)作為關(guān)鍵幀的個(gè)數(shù),這時(shí)因?yàn)镃ore Animation按照每秒60幀去渲染屏幕更新壹瘟,所以如果我們每秒生成60個(gè)關(guān)鍵幀鲫剿,就可以保證動(dòng)畫(huà)足夠的平滑(盡管實(shí)際上很可能用更少的幀率就可以達(dá)到很好的效果)。
我們?cè)谑纠袃H僅引入了對(duì)`CGPoint`類(lèi)型的插值代碼稻轨。但是灵莲,從代碼中很清楚能看出如何擴(kuò)展成支持別的類(lèi)型。作為不能識(shí)別類(lèi)型的備選方案殴俱,我們僅僅在前一半返回了`fromValue`政冻,在后一半返回了`toValue`。
清單10.7使用插入的值創(chuàng)建一個(gè)關(guān)鍵幀動(dòng)畫(huà)
```objective-c
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];
}
```
這可以起到作用线欲,但效果并不是很好明场,到目前為止我們所完成的只是一個(gè)非常復(fù)雜的方式來(lái)使用線(xiàn)性緩沖復(fù)制`CABasicAnimation`的行為。這種方式的好處在于我們可以更加精確地控制緩沖李丰,這也意味著我們可以應(yīng)用一個(gè)完全定制的緩沖函數(shù)苦锨。那么該如何做呢?
緩沖背后的數(shù)學(xué)并不很簡(jiǎn)單趴泌,但是幸運(yùn)的是我們不需要一一實(shí)現(xiàn)它舟舒。羅伯特·彭納有一個(gè)網(wǎng)頁(yè)關(guān)于緩沖函數(shù)([http://www.robertpenner.com/easing](http://www.robertpenner.com/easing)),包含了大多數(shù)普遍的緩沖函數(shù)的多種編程語(yǔ)言的實(shí)現(xiàn)的鏈接嗜憔,包括C秃励。這里是一個(gè)緩沖進(jìn)入緩沖退出函數(shù)的示例(實(shí)際上有很多不同的方式去實(shí)現(xiàn)它)。
```c
float quadraticEaseInOut(float t)
{
return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1;
}
```
對(duì)我們的彈性球來(lái)說(shuō)痹筛,我們可以使用`bounceEaseOut`函數(shù):
```c
float bounceEaseOut(float t)
{
if (t < 4/11.0) {
return (121 * t * t)/16.0;
} else if (t < 8/11.0) {
return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0;
} else if (t < 9/10.0) {
return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0;
}
return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0;
}
```
如果修改清單10.7的代碼來(lái)引入`bounceEaseOut`方法,我們的任務(wù)就是僅僅交換緩沖函數(shù)廓鞠,現(xiàn)在就可以選擇任意的緩沖類(lèi)型創(chuàng)建動(dòng)畫(huà)了(見(jiàn)清單10.8)帚稠。
清單10.8用關(guān)鍵幀實(shí)現(xiàn)自定義的緩沖函數(shù)
```objective-c
- (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;
//apply easing
time = bounceEaseOut(time);
//add keyframe
[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];
}
```
##總結(jié)
在這一章中,我們了解了有關(guān)緩沖和`CAMediaTimingFunction`類(lèi)床佳,它可以允許我們創(chuàng)建自定義的緩沖函數(shù)來(lái)完善我們的動(dòng)畫(huà)滋早,同樣了解了如何用`CAKeyframeAnimation`來(lái)避開(kāi)`CAMediaTimingFunction`的限制,創(chuàng)建完全自定義的緩沖函數(shù)砌们。
在下一章中杆麸,我們將要研究基于定時(shí)器的動(dòng)畫(huà)--另一個(gè)給我們對(duì)動(dòng)畫(huà)更多控制的選擇,并且實(shí)現(xiàn)對(duì)動(dòng)畫(huà)的實(shí)時(shí)操縱浪感。