在第八章中,咱們給時鐘項目添加了動畫。看起來很贊,可是若是有合適的緩衝函數就更好了。在顯示世界中,鐘錶指針轉動的時候,一般起步很慢,而後迅速啪地一聲,最後緩衝到終點。可是標準的緩衝函數在這裏每個適合它,那該如何建立一個新的呢?git
除了+functionWithName:
以外,CAMediaTimingFunction
一樣有另外一個構造函數,一個有四個浮點參數的+functionWithControlPoints::::
(注意這裏奇怪的語法,並無包含具體每一個參數的名稱,這在objective-C中是合法的,可是卻違反了蘋果對方法命名的指導方針,並且看起來是一個奇怪的設計)。github
使用這個方法,咱們能夠建立一個自定義的緩衝函數,來匹配咱們的時鐘動畫,爲了理解如何使用這個方法,咱們要了解一些CAMediaTimingFunction
是如何工做的。編程
CAMediaTimingFunction
函數的主要原則在於它把輸入的時間轉換成起點和終點之間成比例的改變。咱們能夠用一個簡單的圖標來解釋,橫軸表明時間,縱軸表明改變的量,因而線性的緩衝就是一條從起點開始的簡單的斜線(圖10.1)。app
圖10.1 線性緩衝函數的圖像編程語言
這條曲線的斜率表明了速度,斜率的改變表明了加速度,原則上來講,任何加速的曲線均可以用這種圖像來表示,可是CAMediaTimingFunction
使用了一個叫作三次貝塞爾曲線的函數,它只能夠產出指定緩衝函數的子集(咱們以前在第八章中建立CAKeyframeAnimation
路徑的時候提到過三次貝塞爾曲線)。ide
你或許會回想起,一個三次貝塞爾曲線經過四個點來定義,第一個和最後一個點表明了曲線的起點和終點,剩下中間兩個點叫作控制點,由於它們控制了曲線的形狀,貝塞爾曲線的控制點實際上是位於曲線以外的點,也就是說曲線並不必定要穿過它們。你能夠把它們想象成吸引通過它們曲線的磁鐵。函數
圖10.2展現了一個三次貝塞爾緩衝函數的例子動畫
圖10.2 三次貝塞爾緩衝函數atom
實際上它是一個很奇怪的函數,先加速,而後減速,最後快到達終點的時候又加速,那麼標準的緩衝函數又該如何用圖像來表示呢?spa
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 { [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 標準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 { [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 使用關鍵幀實現的反彈球動畫
這種方式還算不錯,可是實現起來略顯笨重(由於要不停地嘗試計算各類關鍵幀和時間偏移)而且和動畫強綁定了(由於若是要改變更畫的一個屬性,那就意味着要從新計算全部的關鍵幀)。那該如何寫一個方法,用緩衝函數來把任何簡單的屬性動畫轉換成關鍵幀動畫呢,下面咱們來實現它。
在清單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; } 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的代碼來引入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
的限制,建立徹底自定義的緩衝函數。