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作爲它們的計時方法)。

你可以使用一個簡單的測試工程來實驗一下(清單10.1),在運行之前改變緩衝函數的代碼,然後點擊任何地方來觀察圖層是如何通過指定的緩衝移動的。

清單10.1 緩衝函數的簡單測試

@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的動畫緩衝

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
{
    [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

緩衝和關鍵幀動畫

或許你會回想起第八章裏面顏色切換的關鍵幀動畫由於線性變換的原因(見清單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
{
    [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

自定義緩衝函數

在第八章中,我們給時鐘項目添加了動畫。看起來很贊,但是如果有合適的緩衝函數就更好了。在顯示世界中,鐘錶指針轉動的時候,通常起步很慢,然後迅速啪地一聲,最後緩衝到終點。但是標準的緩衝函數在這裏每一個適合它,那該如何創建一個新的呢?

除了+functionWithName:之外,CAMediaTimingFunction同樣有另一個構造函數,一個有四個浮點參數的+functionWithControlPoints::::(注意這裏奇怪的語法,並沒有包含具體每個參數的名稱,這在objective-C中是合法的,但是卻違反了蘋果對方法命名的指導方針,而且看起來是一個奇怪的設計)。

使用這個方法,我們可以創建一個自定義的緩衝函數,來匹配我們的時鐘動畫,爲了理解如何使用這個方法,我們要了解一些CAMediaTimingFunction是如何工作的。

三次貝塞爾曲線

CAMediaTimingFunction函數的主要原則在於它把輸入的時間轉換成起點和終點之間成比例的改變。我們可以用一個簡單的圖標來解釋,橫軸代表時間,縱軸代表改變的量,於是線性的緩衝就是一條從起點開始的簡單的斜線(圖10.1)。

圖10.1

圖10.1 線性緩衝函數的圖像

這條曲線的斜率代表了速度,斜率的改變代表了加速度,原則上來說,任何加速的曲線都可以用這種圖像來表示,但是CAMediaTimingFunction使用了一個叫做三次貝塞爾曲線的函數,它只可以產出指定緩衝函數的子集(我們之前在第八章中創建CAKeyframeAnimation路徑的時候提到過三次貝塞爾曲線)。

你或許會回想起,一個三次貝塞爾曲線通過四個點來定義,第一個和最後一個點代表了曲線的起點和終點,剩下中間兩個點叫做控制點,因爲它們控制了曲線的形狀,貝塞爾曲線的控制點其實是位於曲線之外的點,也就是說曲線並不一定要穿過它們。你可以把它們想象成吸引經過它們曲線的磁鐵。

圖10.2展示了一個三次貝塞爾緩衝函數的例子

圖10.2

圖10.2 三次貝塞爾緩衝函數

實際上它是一個很奇怪的函數,先加速,然後減速,最後快到達終點的時候又加速,那麼標準的緩衝函數又該如何用圖像來表示呢?

CAMediaTimingFunction有一個叫做-getControlPointAtIndex:values:的方法,可以用來檢索曲線的點,這個方法的設計的確有點奇怪(或許也就只有蘋果能回答爲什麼不簡單返回一個CGPoint),但是使用它我們可以找到標準緩衝函數的點,然後用UIBezierPathCAShapeLayer來把它畫出來。

曲線的起始和終點始終是{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

圖10.3 標準CAMediaTimingFunction緩衝曲線

那麼對於我們自定義時鐘指針的緩衝函數來說,我們需要初始微弱,然後迅速上升,最後緩衝到終點的曲線,通過一些實驗之後,最終結果如下:

[CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1];

如果把它轉換成緩衝函數的圖像,最後如圖10.4所示,如果把它添加到時鐘的程序,就形成了之前一直期待的非常讚的效果(見代清單10.5)。

圖10.4

圖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

圖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 使用關鍵幀實現的反彈球動畫

這種方式還算不錯,但是實現起來略顯笨重(因爲要不停地嘗試計算各種關鍵幀和時間偏移)並且和動畫強綁定了(因爲如果要改變動畫的一個屬性,那就意味着要重新計算所有的關鍵幀)。那該如何寫一個方法,用緩衝函數來把任何簡單的屬性動畫轉換成關鍵幀動畫呢,下面我們來實現它。

流程自動化

在清單10.6中,我們把動畫分割成相當大的幾塊,然後用Core Animation的緩衝進入和緩衝退出函數來大約形成我們想要的曲線。但如果我們把動畫分割成更小的幾部分,那麼我們就可以用直線來拼接這些曲線(也就是線性緩衝)。爲了實現自動化,我們需要知道如何做如下兩件事情:

  • 自動把任意屬性動畫分割成多個關鍵幀
  • 用一個數學函數表示彈性動畫,使得可以對幀做便宜

爲了解決第一個問題,我們需要複製Core Animation的插值機制。這是一個傳入起點和終點,然後在這兩個點之間指定時間點產出一個新點的機制。對於簡單的浮點起始值,公式如下(假設時間從0到1):

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

那麼如果要插入一個類似於CGPointCGColorRef或者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的限制,創建完全自定義的緩衝函數。

在下一章中,我們將要研究基於定時器的動畫--另一個給我們對動畫更多控制的選擇,並且實現對動畫的實時操縱。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章