# 自定義緩沖函數
在第八章中,我們給時鐘項目添加了動畫。看起來很贊,但是如果有合適的緩沖函數就更好了。在顯示世界中,鐘表指針轉動的時候,通常起步很慢,然后迅速啪地一聲,最后緩沖到終點。但是標準的緩沖函數在這里每一個適合它,那該如何創建一個新的呢?
除了`+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
{
[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](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];
}
~~~
- Introduction
- 1. 圖層樹
- 1.1 圖層與視圖
- 1.2 圖層的能力
- 1.3 使用圖層
- 1.4 總結
- 2. 寄宿圖
- 2.1 contents屬性
- 2.2 Custom Drawing
- 2.3 總結
- 3. 圖層幾何學
- 3.1 布局
- 3.2 錨點
- 3.3 坐標系
- 3.4 Hit Testing
- 3.5 自動布局
- 3.6 總結
- 4. 視覺效果
- 4.1 圓角
- 4.2 圖層邊框
- 4.3 陰影
- 4.4 圖層蒙板
- 4.5 拉伸過濾
- 4.6 組透明
- 4.7 總結
- 5. 變換
- 5.1 仿射變換
- 5.2 3D變換
- 5.3 固體對象
- 5.4 總結
- 6. 專用圖層
- 6.1 CAShapeLayer
- 6.2 CATextLayer
- 6.3 CATransformLayer
- 6.4 CAGradientLayer
- 6.5 CAReplicatorLayer
- 6.6 CAScrollLayer
- 6.7 CATiledLayer
- 6.8 CAEmitterLayer
- 6.9 CAEAGLLayer
- 6.10 AVPlayerLayer
- 6.11 總結
- 7. 隱式動畫
- 7.1 事務
- 7.2 完成塊
- 7.3 圖層行為
- 7.4 呈現與模型
- 7.5 總結
- 8. 顯式動畫
- 8.1 屬性動畫
- 8.2 動畫組
- 8.3 過渡
- 8.4 在動畫過程中取消動畫
- 8.5 總結
- 9. 圖層時間
- 9.1 CAMediaTiming協議
- 9.2 層級關系時間
- 9.3 手動動畫
- 9.4 總結
- 10. 緩沖
- 10.1 動畫速度
- 10.2 自定義緩沖函數
- 10.3 總結
- 11. 基于定時器的動畫
- 11.1 定時幀
- 11.2 物理模擬
- 12. 性能調優
- 12.1. CPU VS GPU
- 12.2 測量,而不是猜測
- 12.3 Instruments
- 12.4 總結
- 13. 高效繪圖
- 13.1 軟件繪圖
- 13.2 矢量圖形
- 13.3 臟矩形
- 13.4 異步繪制
- 13.5 總結
- 14. 圖像IO
- 14.1 加載和潛伏
- 14.2 緩存
- 14.3 文件格式
- 14.4 總結
- 15. 圖層性能
- 15.1 隱式繪制
- 15.2 離屏渲染
- 15.3 混合和過度繪制
- 15.4 減少圖層數量
- 15.5 總結