本文轉載自:http://www.cocoachina.com/ios/20150105/10829.html? 為了防止cocochina以后刪除該文章,故轉載至此;
緩沖
生活和藝術一樣,最美的永遠是曲線。 -- 愛德華布爾沃 - 利頓
在第九章“圖層時間”中,我們討論了動畫時間和CAMediaTiming協議?,F在我們來看一下另一個和時間相關的機制--所謂的緩沖。Core Animation使用緩沖來使動畫移動更平滑更自然,而不是看起來的那種機械和人工,在這一章我們將要研究如何對你的動畫控制和自定義緩沖曲線。
動畫速度
動畫實際上就是一段時間內的變化,這就暗示了變化一定是隨著某個特定的速率進行。速率由以下公式計算而來:
velocity = change / time
這里的變化可以指的是一個物體移動的距離,時間指動畫持續的時長,用這樣的一個移動可以更加形象的描述(比如position和bounds屬性的動畫),但實際上它應用于任意可以做動畫的屬性(比如color和opacity)。
上面的等式假設了速度在整個動畫過程中都是恒定不變的(就如同第八章“顯式動畫”的情況),對于這種恒定速度的動畫我們稱之為“線性步調”,而且從技術的角度而言這也是實現動畫最簡單的方式,但也是完全不真實的一種效果。
考慮一個場景,一輛車行駛在一定距離內,它并不會一開始就以60mph的速度行駛,然后到達終點后突然變成0mph。一是因為需要無限大的加速度(即使是最好的車也不會在0秒內從0跑到60),另外不然的話會干死所有乘客。在現實中,它會慢慢地加速到全速,然后當它接近終點的時候,它會慢慢地減速,直到最后停下來。
那么對于一個掉落到地上的物體又會怎樣呢?它會首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于積累的動能轉換伴隨著一聲巨響,砰?。?。
現實生活中的任何一個物體都會在運動中加速或者減速。那么我們如何在動畫中實現這種加速度呢?一種方法是使用物理引擎來對運動物體的摩擦和動量來建模,然而這會使得計算過于復雜。我們稱這種類型的方程為緩沖函數,幸運的是,Core Animation內嵌了一系列標準函數提供給我們使用。
CAMediaTimingFunction
那么該如何使用緩沖方程式呢?首先需要設置CAAnimation的timingFunction屬性,是CAMediaTimingFunction類的一個對象。如果想改變隱式動畫的計時函數,同樣也可以使用CATransaction的+setAnimationTimingFunction:方法。
這里有一些方式來創建CAMediaTimingFunction,最簡單的方式是調用+timingFunctionWithName:的構造方法。這里傳入如下幾個常量之一:
kCAMediaTimingFunctionLinear
kCAMediaTimingFunctionEaseIn
kCAMediaTimingFunctionEaseOut
kCAMediaTimingFunctionEaseInEaseOut
kCAMediaTimingFunctionDefault
kCAMediaTimingFunctionLinear選項創建了一個線性的計時函數,同樣也是CAAnimation的timingFunction屬性為空時候的默認函數。線性步調對于那些立即加速并且保持勻速到達終點的場景會有意義(例如射出槍膛的子彈),但是默認來說它看起來很奇怪,因為對大多數的動畫來說確實很少用到。
kCAMediaTimingFunctionEaseIn常量創建了一個慢慢加速然后突然停止的方法。對于之前提到的自由落體的例子來說很適合,或者比如對準一個目標的導彈的發射。
kCAMediaTimingFunctionEaseOut則恰恰相反,它以一個全速開始,然后慢慢減速停止。它有一個削弱的效果,應用的場景比如一扇門慢慢地關上,而不是砰地一聲。
kCAMediaTimingFunctionEaseInEaseOut創建了一個慢慢加速然后再慢慢減速的過程。這是現實世界大多數物體移動的方式,也是大多數動畫來說最好的選擇。如果只可以用一種緩沖函數的話,那就必須是它了。那么你會疑惑為什么這不是默認的選擇,實際上當使用UIView的動畫方法時,他的確是默認的,但當創建CAAnimation的時候,就需要手動設置它了。
最后還有一個kCAMediaTimingFunctionDefault,它和kCAMediaTimingFunctionEaseInEaseOut很類似,但是加速和減速的過程都稍微有些慢。它和kCAMediaTimingFunctionEaseInEaseOut的區別很難察覺,可能是蘋果覺得它對于隱式動畫來說更適合(然后對UIKit就改變了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作為默認效果),雖然它的名字說是默認的,但還是要記住當創建顯式的CAAnimation它并不是默認選項(換句話說,默認的圖層行為動畫用kCAMediaTimingFunctionDefault作為它們的計時方法)。
你可以使用一個簡單的測試工程來實驗一下(清單10.1),在運行之前改變緩沖函數的代碼,然后點擊任何地方來觀察圖層是如何通過指定的緩沖移動的。
清單10.1 緩沖函數的簡單測試
@interface?ViewController?()
@property?(nonatomic,?strong)?CALayer?*colorLayer;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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的動畫緩沖
UIKit的動畫也同樣支持這些緩沖方法的使用,盡管語法和常量有些不同,為了改變UIView動畫的緩沖選項,給options參數添加如下常量之一:
UIViewAnimationOptionCurveEaseInOut
UIViewAnimationOptionCurveEaseIn
UIViewAnimationOptionCurveEaseOut
UIViewAnimationOptionCurveLinear
它們和CAMediaTimingFunction緊密關聯,UIViewAnimationOptionCurveEaseInOut是默認值(這里沒有kCAMediaTimingFunctionDefault相對應的值了)。
具體使用方法見清單10.2(注意到這里不再使用UIView額外添加的圖層,因為UIKit的動畫并不支持這類圖層)。
清單10.2 使用UIKit動畫的緩沖測試工程
@interface?ViewController?()
@property?(nonatomic,?strong)?UIView?*colorView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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
緩沖和關鍵幀動畫
或許你會回想起第八章里面顏色切換的關鍵幀動畫由于線性變換的原因(見清單8.5)看起來有些奇怪,使得顏色變換非常不自然。為了糾正這點,我們來用更加合適的緩沖方法,例如kCAMediaTimingFunctionEaseIn,給圖層的顏色變化添加一點脈沖效果,讓它更像現實中的一個彩色燈泡。
我們不想給整個動畫過程應用這個效果,我們希望對每個動畫的過程重復這樣的緩沖,于是每次顏色的變換都會有脈沖效果。
CAKeyframeAnimation有一個NSArray類型的timingFunctions屬性,我們可以用它來對每次動畫的步驟指定不同的計時函數。但是指定函數的個數一定要等于keyframes數組的元素個數減一,因為它是描述每一幀之間動畫速度的函數。
在這個例子中,我們自始至終想使用同一個緩沖函數,但我們同樣需要一個函數的數組來告訴動畫不停地重復每個步驟,而不是在整個動畫序列只做一次緩沖,我們簡單地使用包含多個相同函數拷貝的數組就可以了(見清單10.3)。
運行更新后的代碼,你會發現動畫看起來更加自然了。
清單10.3 對CAKeyframeAnimation使用CAMediaTimingFunction
@interface?ViewController?()
@property?(nonatomic,?weak)?IBOutlet?UIView?*layerView;
@property?(nonatomic,?weak)?IBOutlet?CALayer?*colorLayer;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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
自定義緩沖函數
在第八章中,我們給時鐘項目添加了動畫。看起來很贊,但是如果有合適的緩沖函數就更好了。在顯示世界中,鐘表指針轉動的時候,通常起步很慢,然后迅速啪地一聲,最后緩沖到終點。但是標準的緩沖函數在這里每一個適合它,那該如何創建一個新的呢?
除了+functionWithName:之外,CAMediaTimingFunction同樣有另一個構造函數,一個有四個浮點參數的+functionWithControlPoints::::(注意這里奇怪的語法,并沒有包含具體每個參數的名稱,這在objective-C中是合法的,但是卻違反了蘋果對方法命名的指導方針,而且看起來是一個奇怪的設計)。
使用這個方法,我們可以創建一個自定義的緩沖函數,來匹配我們的時鐘動畫,為了理解如何使用這個方法,我們要了解一些CAMediaTimingFunction是如何工作的。
三次貝塞爾曲線
CAMediaTimingFunction函數的主要原則在于它把輸入的時間轉換成起點和終點之間成比例的改變。我們可以用一個簡單的圖標來解釋,橫軸代表時間,縱軸代表改變的量,于是線性的緩沖就是一條從起點開始的簡單的斜線(圖10.1)。
圖10.1 線性緩沖函數的圖像
這條曲線的斜率代表了速度,斜率的改變代表了加速度,原則上來說,任何加速的曲線都可以用這種圖像來表示,但是CAMediaTimingFunction使用了一個叫做三次貝塞爾曲線的函數,它只可以產出指定緩沖函數的子集(我們之前在第八章中創建CAKeyframeAnimation路徑的時候提到過三次貝塞爾曲線)。
你或許會回想起,一個三次貝塞爾曲線通過四個點來定義,第一個和最后一個點代表了曲線的起點和終點,剩下中間兩個點叫做控制點,因為它們控制了曲線的形狀,貝塞爾曲線的控制點其實是位于曲線之外的點,也就是說曲線并不一定要穿過它們。你可以把它們想象成吸引經過它們曲線的磁鐵。
圖10.2展示了一個三次貝塞爾緩沖函數的例子
圖10.2 三次貝塞爾緩沖函數
實際上它是一個很奇怪的函數,先加速,然后減速,最后快到達終點的時候又加速,那么標準的緩沖函數又該如何用圖像來表示呢?
CAMediaTimingFunction有一個叫做-getControlPointAtIndex:values:的方法,可以用來檢索曲線的點,這個方法的設計的確有點奇怪(或許也就只有蘋果能回答為什么不簡單返回一個CGPoint),但是使用它我們可以找到標準緩沖函數的點,然后用UIBezierPath和CAShapeLayer來把它畫出來。
曲線的起始和終點始終是{0, 0}和{1, 1},于是我們只需要檢索曲線的第二個和第三個點(控制點)。具體代碼見清單10.4。所有的標準緩沖函數的圖像見圖10.3。
清單10.4 使用UIBezierPath繪制CAMediaTimingFunction
@interface?ViewController?()
@property?(nonatomic,?weak)?IBOutlet?UIView?*layerView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//create?timing?function
CAMediaTimingFunction?*function=?CAMediaTimingFunction?functionWithName:?kCAMediaTimingFunctionEaseOut];
//get?control?points
CGPoint?controlPoint1,?controlPoint2;
[functiongetControlPointAtIndex:1?values:(float?*)&controlPoint1];
[functiongetControlPointAtIndex: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 標準CAMediaTimingFunction緩沖曲線
那么對于我們自定義時鐘指針的緩沖函數來說,我們需要初始微弱,然后迅速上升,最后緩沖到終點的曲線,通過一些實驗之后,最終結果如下:
[CAMediaTimingFunction?functionWithControlPoints:1?:0?:0.75?:1];
如果把它轉換成緩沖函數的圖像,最后如圖10.4所示,如果把它添加到時鐘的程序,就形成了之前一直期待的非常贊的效果(見代清單10.5)。
圖10.4 自定義適合時鐘的緩沖函數
清單10.5 添加了自定義緩沖函數的時鐘程序
-?(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;
}
}
更加復雜的動畫曲線
考慮一個橡膠球掉落到堅硬的地面的場景,當開始下落的時候,它會持續加速知道落到地面,然后經過幾次反彈,最后停下來。如果用一張圖來說明,它會如圖10.5所示。
圖10.5 一個沒法用三次貝塞爾曲線描述的反彈的動畫
這種效果沒法用一個簡單的三次貝塞爾曲線表示,于是不能用CAMediaTimingFunction來完成。但如果想要實現這樣的效果,可以用如下幾種方法:
用CAKeyframeAnimation創建一個動畫,然后分割成幾個步驟,每個小步驟使用自己的計時函數(具體下節介紹)。
使用定時器逐幀更新實現動畫(見第11章,“基于定時器的動畫”)。
基于關鍵幀的緩沖
為了使用關鍵幀實現反彈動畫,我們需要在緩沖曲線中對每一個顯著的點創建一個關鍵幀(在這個情況下,關鍵點也就是每次反彈的峰值),然后應用緩沖函數把每段曲線連接起來。同時,我們也需要通過keyTimes來指定每個關鍵幀的時間偏移,由于每次反彈的時間都會減少,于是關鍵幀并不會均勻分布。
清單10.6展示了實現反彈球動畫的代碼(見圖10.6)
清單10.6 使用關鍵幀實現反彈球的動畫
@interface?ViewController?()
@property?(nonatomic,?weak)?IBOutlet?UIView?*containerView;
@property?(nonatomic,?strong)?UIImageView?*ballView;
@end
@implementation?ViewController
-?(void)viewDidLoad
{
[superviewDidLoad];
//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 使用關鍵幀實現的反彈球動畫
這種方式還算不錯,但是實現起來略顯笨重(因為要不停地嘗試計算各種關鍵幀和時間偏移)并且和動畫強綁定了(因為如果要改變動畫的一個屬性,那就意味著要重新計算所有的關鍵幀)。那該如何寫一個方法,用緩沖函數來把任何簡單的屬性動畫轉換成關鍵幀動畫呢,下面我們來實現它。
流程自動化
在清單10.6中,我們把動畫分割成相當大的幾塊,然后用Core Animation的緩沖進入和緩沖退出函數來大約形成我們想要的曲線。但如果我們把動畫分割成更小的幾部分,那么我們就可以用直線來拼接這些曲線(也就是線性緩沖)。為了實現自動化,我們需要知道如何做如下兩件事情:
自動把任意屬性動畫分割成多個關鍵幀
用一個數學函數表示彈性動畫,使得可以對幀做便宜
為了解決第一個問題,我們需要復制Core Animation的插值機制。這是一個傳入起點和終點,然后在這兩個點之間指定時間點產出一個新點的機制。對于簡單的浮點起始值,公式如下(假設時間從0到1):
value = (endValue – startValue) × time + startValue;
那么如果要插入一個類似于CGPoint,CGColorRef或者CATransform3D這種更加復雜類型的值,我們可以簡單地對每個獨立的元素應用這個方法(也就CGPoint中的x和y值,CGColorRef中的紅,藍,綠,透明值,或者是CATransform3D中獨立矩陣的坐標)。我們同樣需要一些邏輯在插值之前對對象拆解值,然后在插值之后在重新封裝成對象,也就是說需要實時地檢查類型。
一旦我們可以用代碼獲取屬性動畫的起始值之間的任意插值,我們就可以把動畫分割成許多獨立的關鍵幀,然后產出一個線性的關鍵幀動畫。清單10.7展示了相關代碼。
注意到我們用了60 x 動畫時間(秒做單位)作為關鍵幀的個數,這時因為Core Animation按照每秒60幀去渲染屏幕更新,所以如果我們每秒生成60個關鍵幀,就可以保證動畫足夠的平滑(盡管實際上很可能用更少的幀率就可以達到很好的效果)。
我們在示例中僅僅引入了對CGPoint類型的插值代碼。但是,從代碼中很清楚能看出如何擴展成支持別的類型。作為不能識別類型的備選方案,我們僅僅在前一半返回了fromValue,在后一半返回了toValue。
清單10.7 使用插入的值創建一個關鍵幀動畫
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];
}
這可以起到作用,但效果并不是很好,到目前為止我們所完成的只是一個非常復雜的方式來使用線性緩沖復制CABasicAnimation的行為。這種方式的好處在于我們可以更加精確地控制緩沖,這也意味著我們可以應用一個完全定制的緩沖函數。那么該如何做呢?
緩沖背后的數學并不很簡單,但是幸運的是我們不需要一一實現它。羅伯特·彭納有一個網頁關于緩沖函數(http://www.robertpenner.com/easing),包含了大多數普遍的緩沖函數的多種編程語言的實現的鏈接,包括C。這里是一個緩沖進入緩沖退出函數的示例(實際上有很多不同的方式去實現它)。
float?quadraticEaseInOut(float?t)
{
return(t?<?0.5)??(2?*?t?*?t):?(-2?*?t?*?t)?+?(4?*?t)?-?1;
}
對我們的彈性球來說,我們可以使用bounceEaseOut函數:
float?bounceEaseOut(float?t)
{
if(t?<?4/11.0)?{
return(121?*?t?*?t)/16.0;
}elseif(t?<?8/11.0)?{
return(363/40.0?*?t?*?t)?-?(99/10.0?*?t)?+?17/5.0;
}elseif(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的代碼來引入bounceEaseOut方法,我們的任務就是僅僅交換緩沖函數,現在就可以選擇任意的緩沖類型創建動畫了(見清單10.8)。
清單10.8 用關鍵幀實現自定義的緩沖函數
-?(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];
}
總結
在這一章中,我們了解了有關緩沖和CAMediaTimingFunction類,它可以允許我們創建自定義的緩沖函數來完善我們的動畫,同樣了解了如何用CAKeyframeAnimation來避開CAMediaTimingFunction的限制,創建完全自定義的緩沖函數。