10、緩沖

緩沖

生活和藝術(shù)一樣辟犀,最美的永遠(yuǎn)是曲線。 -- 愛德華布爾沃 - 利頓

在第九章“圖層時(shí)間”中绸硕,我們討論了動(dòng)畫時(shí)間和CAMediaTiming協(xié)議√镁梗現(xiàn)在我們來(lái)看一下另一個(gè)和時(shí)間相關(guān)的機(jī)制--所謂的緩沖魂毁。Core Animation使用緩沖來(lái)使動(dòng)畫移動(dòng)更平滑更自然,而不是看起來(lái)的那種機(jī)械和人工出嘹,在這一章我們將要研究如何對(duì)你的動(dòng)畫控制和自定義緩沖曲線席楚。

動(dòng)畫速度

動(dòng)畫實(shí)際上就是一段時(shí)間內(nèi)的變化,這就暗示了變化一定是隨著某個(gè)特定的速率進(jìn)行税稼。速率由以下公式計(jì)算而來(lái):

velocity = change / time

這里的變化可以指的是一個(gè)物體移動(dòng)的距離酣胀,時(shí)間指動(dòng)畫持續(xù)的時(shí)長(zhǎng),用這樣的一個(gè)移動(dòng)可以更加形象的描述(比如positionbounds屬性的動(dòng)畫)娶聘,但實(shí)際上它應(yīng)用于任意可以做動(dòng)畫的屬性(比如coloropacity)。

上面的等式假設(shè)了速度在整個(gè)動(dòng)畫過(guò)程中都是恒定不變的(就如同第八章“顯式動(dòng)畫”的情況)甚脉,對(duì)于這種恒定速度的動(dòng)畫我們稱之為“線性步調(diào)”丸升,而且從技術(shù)的角度而言這也是實(shí)現(xiàn)動(dòng)畫最簡(jiǎn)單的方式,但也是完全不真實(shí)的一種效果牺氨。

考慮一個(gè)場(chǎng)景狡耻,一輛車行駛在一定距離內(nèi),它并不會(huì)一開始就以60mph的速度行駛猴凹,然后到達(dá)終點(diǎn)后突然變成0mph夷狰。一是因?yàn)樾枰獰o(wú)限大的加速度(即使是最好的車也不會(huì)在0秒內(nèi)從0跑到60),另外不然的話會(huì)干死所有乘客郊霎。在現(xiàn)實(shí)中沼头,它會(huì)慢慢地加速到全速,然后當(dāng)它接近終點(diǎn)的時(shí)候书劝,它會(huì)慢慢地減速进倍,直到最后停下來(lái)。

那么對(duì)于一個(gè)掉落到地上的物體又會(huì)怎樣呢购对?它會(huì)首先停在空中猾昆,然后一直加速到落到地面,然后突然停止(然后由于積累的動(dòng)能轉(zhuǎn)換伴隨著一聲巨響骡苞,砰4刮稀)。

現(xiàn)實(shí)生活中的任何一個(gè)物體都會(huì)在運(yùn)動(dòng)中加速或者減速解幽。那么我們?nèi)绾卧趧?dòng)畫中實(shí)現(xiàn)這種加速度呢贴见?一種方法是使用物理引擎來(lái)對(duì)運(yùn)動(dòng)物體的摩擦和動(dòng)量來(lái)建模,然而這會(huì)使得計(jì)算過(guò)于復(fù)雜躲株。我們稱這種類型的方程為緩沖函數(shù)蝇刀,幸運(yùn)的是,Core Animation內(nèi)嵌了一系列標(biāo)準(zhǔn)函數(shù)提供給我們使用徘溢。

CAMediaTimingFunction

那么該如何使用緩沖方程式呢吞琐?首先需要設(shè)置CAAnimationtimingFunction屬性捆探,是CAMediaTimingFunction類的一個(gè)對(duì)象。如果想改變隱式動(dòng)畫的計(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è)線性的計(jì)時(shí)函數(shù)助被,同樣也是CAAnimationtimingFunction屬性為空時(shí)候的默認(rèn)函數(shù)。線性步調(diào)對(duì)于那些立即加速并且保持勻速到達(dá)終點(diǎn)的場(chǎng)景會(huì)有意義(例如射出槍膛的子彈)切诀,但是默認(rèn)來(lái)說(shuō)它看起來(lái)很奇怪揩环,因?yàn)閷?duì)大多數(shù)的動(dòng)畫來(lái)說(shuō)確實(shí)很少用到。

kCAMediaTimingFunctionEaseIn常量創(chuàng)建了一個(gè)慢慢加速然后突然停止的方法幅虑。對(duì)于之前提到的自由落體的例子來(lái)說(shuō)很適合丰滑,或者比如對(duì)準(zhǔn)一個(gè)目標(biāo)的導(dǎo)彈的發(fā)射。

kCAMediaTimingFunctionEaseOut則恰恰相反倒庵,它以一個(gè)全速開始褒墨,然后慢慢減速停止。它有一個(gè)削弱的效果擎宝,應(yīng)用的場(chǎng)景比如一扇門慢慢地關(guān)上郁妈,而不是砰地一聲。

kCAMediaTimingFunctionEaseInEaseOut創(chuàng)建了一個(gè)慢慢加速然后再慢慢減速的過(guò)程绍申。這是現(xiàn)實(shí)世界大多數(shù)物體移動(dòng)的方式噩咪,也是大多數(shù)動(dòng)畫來(lái)說(shuō)最好的選擇。如果只可以用一種緩沖函數(shù)的話极阅,那就必須是它了剧腻。那么你會(huì)疑惑為什么這不是默認(rèn)的選擇,實(shí)際上當(dāng)使用UIView的動(dòng)畫方法時(shí)涂屁,他的確是默認(rèn)的书在,但當(dāng)創(chuàng)建CAAnimation的時(shí)候,就需要手動(dòng)設(shè)置它了拆又。

最后還有一個(gè)kCAMediaTimingFunctionDefault儒旬,它和kCAMediaTimingFunctionEaseInEaseOut很類似,但是加速和減速的過(guò)程都稍微有些慢帖族。它和kCAMediaTimingFunctionEaseInEaseOut的區(qū)別很難察覺栈源,可能是蘋果覺得它對(duì)于隱式動(dòng)畫來(lái)說(shuō)更適合(然后對(duì)UIKit就改變了想法,而是使用kCAMediaTimingFunctionEaseInEaseOut作為默認(rèn)效果)竖般,雖然它的名字說(shuō)是默認(rèn)的甚垦,但還是要記住當(dāng)創(chuàng)建顯式CAAnimation它并不是默認(rèn)選項(xiàng)(換句話說(shuō),默認(rèn)的圖層行為動(dòng)畫用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è)試

@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)畫緩沖

UIKit的動(dòng)畫也同樣支持這些緩沖方法的使用迄埃,盡管語(yǔ)法和常量有些不同疗韵,為了改變UIView動(dòng)畫的緩沖選項(xiàng),給options參數(shù)添加如下常量之一:

UIViewAnimationOptionCurveEaseInOut 
UIViewAnimationOptionCurveEaseIn 
UIViewAnimationOptionCurveEaseOut 
UIViewAnimationOptionCurveLinear

它們和CAMediaTimingFunction緊密關(guān)聯(lián)侄非,UIViewAnimationOptionCurveEaseInOut是默認(rèn)值(這里沒有kCAMediaTimingFunctionDefault相對(duì)應(yīng)的值了)蕉汪。

具體使用方法見清單10.2(注意到這里不再使用UIView額外添加的圖層,因?yàn)閁IKit的動(dòng)畫并不支持這類圖層)逞怨。

清單10.2 使用UIKit動(dòng)畫的緩沖測(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ì)回想起第八章里面顏色切換的關(guān)鍵幀動(dòng)畫由于線性變換的原因(見清單8.5)看起來(lái)有些奇怪者疤,使得顏色變換非常不自然。為了糾正這點(diǎn)叠赦,我們來(lái)用更加合適的緩沖方法驹马,例如kCAMediaTimingFunctionEaseIn,給圖層的顏色變化添加一點(diǎn)脈沖效果眯搭,讓它更像現(xiàn)實(shí)中的一個(gè)彩色燈泡。

我們不想給整個(gè)動(dòng)畫過(guò)程應(yīng)用這個(gè)效果业岁,我們希望對(duì)每個(gè)動(dòng)畫的過(guò)程重復(fù)這樣的緩沖鳞仙,于是每次顏色的變換都會(huì)有脈沖效果。

CAKeyframeAnimation有一個(gè)NSArray類型的timingFunctions屬性笔时,我們可以用它來(lái)對(duì)每次動(dòng)畫的步驟指定不同的計(jì)時(shí)函數(shù)棍好。但是指定函數(shù)的個(gè)數(shù)一定要等于keyframes數(shù)組的元素個(gè)數(shù)減一,因?yàn)樗敲枋雒恳粠g動(dòng)畫速度的函數(shù)允耿。

在這個(gè)例子中借笙,我們自始至終想使用同一個(gè)緩沖函數(shù),但我們同樣需要一個(gè)函數(shù)的數(shù)組來(lái)告訴動(dòng)畫不停地重復(fù)每個(gè)步驟较锡,而不是在整個(gè)動(dòng)畫序列只做一次緩沖业稼,我們簡(jiǎn)單地使用包含多個(gè)相同函數(shù)拷貝的數(shù)組就可以了(見清單10.3)。

運(yùn)行更新后的代碼蚂蕴,你會(huì)發(fā)現(xiàn)動(dòng)畫看起來(lái)更加自然了低散。

清單10.3 對(duì)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

自定義緩沖函數(shù)

在第八章中,我們給時(shí)鐘項(xiàng)目添加了動(dòng)畫骡楼∪酆牛看起來(lái)很贊鸟整,但是如果有合適的緩沖函數(shù)就更好了。在現(xiàn)實(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ǔ)法,并沒有包含具體每個(gè)參數(shù)的名稱落追,這在objective-C中是合法的雹熬,但是卻違反了蘋果對(duì)方法命名的指導(dǎo)方針竿报,而且看起來(lái)是一個(gè)奇怪的設(shè)計(jì))烈菌。

使用這個(gè)方法挚赊,我們可以創(chuàng)建一個(gè)自定義的緩沖函數(shù)荠割,來(lái)匹配我們的時(shí)鐘動(dòng)畫,為了理解如何使用這個(gè)方法箕宙,我們要了解一些CAMediaTimingFunction是如何工作的哟忍。

三次貝塞爾曲線

CAMediaTimingFunction函數(shù)的主要原則在于它把輸入的時(shí)間轉(zhuǎn)換成起點(diǎn)和終點(diǎn)之間成比例的改變锅很。我們可以用一個(gè)簡(jiǎn)單的圖標(biāo)來(lái)解釋爆安,橫軸代表時(shí)間致扯,縱軸代表改變的量,于是線性的緩沖就是一條從起點(diǎn)開始的簡(jiǎn)單的斜線(圖10.1)鲤看。

圖10.1 線性緩沖函數(shù)的圖像

這條曲線的斜率代表了速度,斜率的改變代表了加速度慷吊,原則上來(lái)說(shuō)溉瓶,任何加速的曲線都可以用這種圖像來(lái)表示疾宏,但是CAMediaTimingFunction使用了一個(gè)叫做三次貝塞爾曲線的函數(shù),它只可以產(chǎn)出指定緩沖函數(shù)的子集(我們之前在第八章中創(chuàng)建CAKeyframeAnimation路徑的時(shí)候提到過(guò)三次貝塞爾曲線)岩馍。

你或許會(huì)回想起兼雄,一個(gè)三次貝塞爾曲線通過(guò)四個(gè)點(diǎn)來(lái)定義赦肋,第一個(gè)和最后一個(gè)點(diǎn)代表了曲線的起點(diǎn)和終點(diǎn)佃乘,剩下中間兩個(gè)點(diǎn)叫做控制點(diǎn),因?yàn)樗鼈兛刂屏饲€的形狀程帕,貝塞爾曲線的控制點(diǎn)其實(shí)是位于曲線之外的點(diǎn)愁拭,也就是說(shuō)曲線并不一定要穿過(guò)它們。你可以把它們想象成吸引經(jīng)過(guò)它們曲線的磁鐵蔚鸥。

圖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)檢索曲線的點(diǎn)吕嘀,這個(gè)方法的設(shè)計(jì)的確有點(diǎn)奇怪(或許也就只有蘋果能回答為什么不簡(jiǎn)單返回一個(gè)CGPoint)趁曼,但是使用它我們可以找到標(biāo)準(zhǔn)緩沖函數(shù)的點(diǎn)挡闰,然后用UIBezierPathCAShapeLayer來(lái)把它畫出來(lái)。

曲線的起始和終點(diǎn)始終是{0, 0}和{1, 1}摄悯,于是我們只需要檢索曲線的第二個(gè)和第三個(gè)點(diǎn)(控制點(diǎn))。具體代碼見清單10.4典蜕。所有的標(biāo)準(zhǔn)緩沖函數(shù)的圖像見圖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 標(biāo)準(zhǔn)`CAMediaTimingFunction`緩沖曲線

那么對(duì)于我們自定義時(shí)鐘指針的緩沖函數(shù)來(lái)說(shuō)淑倾,我們需要初始微弱,然后迅速上升碍讨,最后緩沖到終點(diǎn)的曲線蒙秒,通過(guò)一些實(shí)驗(yàn)之后勃黍,最終結(jié)果如下:

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

如果把它轉(zhuǎn)換成緩沖函數(shù)的圖像,最后如圖10.4所示晕讲,如果把它添加到時(shí)鐘的程序覆获,就形成了之前一直期待的非常贊的效果(見代清單10.5)。

圖10.4 自定義適合時(shí)鐘的緩沖函數(shù)

清單10.5 添加了自定義緩沖函數(shù)的時(shí)鐘程序

- (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)畫曲線

考慮一個(gè)橡膠球掉落到堅(jiān)硬的地面的場(chǎng)景瓢省,當(dāng)開始下落的時(shí)候锻梳,它會(huì)持續(xù)加速知道落到地面净捅,然后經(jīng)過(guò)幾次反彈具钥,最后停下來(lái)宁玫。如果用一張圖來(lái)說(shuō)明妖碉,它會(huì)如圖10.5所示鱼鸠。


圖10.5 一個(gè)沒法用三次貝塞爾曲線描述的反彈的動(dòng)畫

這種效果沒法用一個(gè)簡(jiǎn)單的三次貝塞爾曲線表示扮授,于是不能用CAMediaTimingFunction來(lái)完成。但如果想要實(shí)現(xiàn)這樣的效果,可以用如下幾種方法:

  • CAKeyframeAnimation創(chuàng)建一個(gè)動(dòng)畫揖曾,然后分割成幾個(gè)步驟兑宇,每個(gè)小步驟使用自己的計(jì)時(shí)函數(shù)(具體下節(jié)介紹)。
  • 使用定時(shí)器逐幀更新實(shí)現(xiàn)動(dòng)畫(見第11章,“基于定時(shí)器的動(dòng)畫”)沽损。

基于關(guān)鍵幀的緩沖

為了使用關(guān)鍵幀實(shí)現(xiàn)反彈動(dòng)畫,我們需要在緩沖曲線中對(duì)每一個(gè)顯著的點(diǎn)創(chuàng)建一個(gè)關(guān)鍵幀(在這個(gè)情況下盒使,關(guān)鍵點(diǎn)也就是每次反彈的峰值)绍赛,然后應(yīng)用緩沖函數(shù)把每段曲線連接起來(lái)腿倚。同時(shí)饭豹,我們也需要通過(guò)keyTimes來(lái)指定每個(gè)關(guān)鍵幀的時(shí)間偏移,由于每次反彈的時(shí)間都會(huì)減少诗越,于是關(guān)鍵幀并不會(huì)均勻分布薇搁。

清單10.6展示了實(shí)現(xiàn)反彈球動(dòng)畫的代碼(見圖10.6)

清單10.6 使用關(guān)鍵幀實(shí)現(xiàn)反彈球的動(dòng)畫

@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)畫

這種方式還算不錯(cuò)孵坚,但是實(shí)現(xiàn)起來(lái)略顯笨重(因?yàn)橐煌5貒L試計(jì)算各種關(guān)鍵幀和時(shí)間偏移)并且和動(dòng)畫強(qiáng)綁定了(因?yàn)槿绻淖儎?dòng)畫的一個(gè)屬性作媚,那就意味著要重新計(jì)算所有的關(guān)鍵幀)境蔼。那該如何寫一個(gè)方法,用緩沖函數(shù)來(lái)把任何簡(jiǎn)單的屬性動(dòng)畫轉(zhuǎn)換成關(guān)鍵幀動(dòng)畫呢伺通,下面我們來(lái)實(shí)現(xiàn)它箍土。

流程自動(dòng)化

在清單10.6中,我們把動(dòng)畫分割成相當(dāng)大的幾塊罐监,然后用Core Animation的緩沖進(jìn)入和緩沖退出函數(shù)來(lái)大約形成我們想要的曲線吴藻。但如果我們把動(dòng)畫分割成更小的幾部分,那么我們就可以用直線來(lái)拼接這些曲線(也就是線性緩沖)弓柱。為了實(shí)現(xiàn)自動(dòng)化调缨,我們需要知道如何做如下兩件事情:

  • 自動(dòng)把任意屬性動(dòng)畫分割成多個(gè)關(guān)鍵幀
  • 用一個(gè)數(shù)學(xué)函數(shù)表示彈性動(dòng)畫,使得可以對(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è)類似于CGPointCGColorRef或者CATransform3D這種更加復(fù)雜類型的值,我們可以簡(jiǎn)單地對(duì)每個(gè)獨(dú)立的元素應(yīng)用這個(gè)方法(也就CGPoint中的x和y值立莉,CGColorRef中的紅绢彤,藍(lán),綠蜓耻,透明值茫舶,或者是CATransform3D中獨(dú)立矩陣的坐標(biāo))。我們同樣需要一些邏輯在插值之前對(duì)對(duì)象拆解值刹淌,然后在插值之后在重新封裝成對(duì)象饶氏,也就是說(shuō)需要實(shí)時(shí)地檢查類型。

一旦我們可以用代碼獲取屬性動(dòng)畫的起始值之間的任意插值有勾,我們就可以把動(dòng)畫分割成許多獨(dú)立的關(guān)鍵幀疹启,然后產(chǎn)出一個(gè)線性的關(guān)鍵幀動(dòng)畫。清單10.7展示了相關(guān)代碼蔼卡。

注意到我們用了60 x 動(dòng)畫時(shí)間(秒做單位)作為關(guān)鍵幀的個(gè)數(shù)喊崖,這時(shí)因?yàn)镃ore Animation按照每秒60幀去渲染屏幕更新,所以如果我們每秒生成60個(gè)關(guān)鍵幀雇逞,就可以保證動(dòng)畫足夠的平滑(盡管實(shí)際上很可能用更少的幀率就可以達(dá)到很好的效果)荤懂。

我們?cè)谑纠袃H僅引入了對(duì)CGPoint類型的插值代碼。但是塘砸,從代碼中很清楚能看出如何擴(kuò)展成支持別的類型节仿。作為不能識(shí)別類型的備選方案,我們僅僅在前一半返回了fromValue谣蠢,在后一半返回了toValue粟耻。

清單10.7 使用插入的值創(chuàng)建一個(gè)關(guān)鍵幀動(dòng)畫

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)使用線性緩沖復(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)赏僧,包含了大多數(shù)普遍的緩沖函數(shù)的多種編程語(yǔ)言的實(shí)現(xiàn)的鏈接,包括C扭倾。這里是一個(gè)緩沖進(jìn)入緩沖退出函數(shù)的示例(實(shí)際上有很多不同的方式去實(shí)現(xiàn)它)淀零。

float quadraticEaseInOut(float t) 
{
    return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1; 
}

對(duì)我們的彈性球來(lái)說(shuō),我們可以使用bounceEaseOut函數(shù):

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)在就可以選擇任意的緩沖類型創(chuàng)建動(dòng)畫了(見清單10.8)唉堪。

清單10.8 用關(guān)鍵幀實(shí)現(xiàn)自定義的緩沖函數(shù)

- (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類肩民,它可以允許我們創(chuàng)建自定義的緩沖函數(shù)來(lái)完善我們的動(dòng)畫唠亚,同樣了解了如何用CAKeyframeAnimation來(lái)避開CAMediaTimingFunction的限制,創(chuàng)建完全自定義的緩沖函數(shù)持痰。

文章摘錄自:https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末灶搜,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子工窍,更是在濱河造成了極大的恐慌割卖,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件移剪,死亡現(xiàn)場(chǎng)離奇詭異究珊,居然都是意外死亡薪者,警方通過(guò)查閱死者的電腦和手機(jī)纵苛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)言津,“玉大人攻人,你說(shuō)我怎么就攤上這事⌒郏” “怎么了怀吻?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)初婆。 經(jīng)常有香客問(wèn)我蓬坡,道長(zhǎng),這世上最難降的妖魔是什么磅叛? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任屑咳,我火速辦了婚禮,結(jié)果婚禮上弊琴,老公的妹妹穿的比我還像新娘兆龙。我一直安慰自己,他們只是感情好敲董,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布紫皇。 她就那樣靜靜地躺著,像睡著了一般腋寨。 火紅的嫁衣襯著肌膚如雪聪铺。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天萄窜,我揣著相機(jī)與錄音铃剔,去河邊找鬼锣杂。 笑死,一個(gè)胖子當(dāng)著我的面吹牛番宁,可吹牛的內(nèi)容都是我干的元莫。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蝶押,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼踱蠢!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起棋电,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤茎截,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后赶盔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體企锌,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年于未,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了撕攒。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡烘浦,死狀恐怖抖坪,靈堂內(nèi)的尸體忽然破棺而出须喂,到底是詐尸還是另有隱情家夺,我是刑警寧澤,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布槽驶,位于F島的核電站握侧,受9級(jí)特大地震影響蚯瞧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜品擎,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一埋合、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧孽查,春花似錦饥悴、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至答朋,卻和暖如春贷揽,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背梦碗。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工禽绪, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蓖救,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓印屁,卻偏偏與公主長(zhǎng)得像循捺,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子雄人,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容