iOS Core Animation:Advanced Techniques(iOS核心動(dòng)畫高級(jí)技巧)-上篇
iOS Core Animation:Advanced Techniques(iOS核心動(dòng)畫高級(jí)技巧)-中篇
iOS Core Animation:Advanced Techniques(iOS核心動(dòng)畫高級(jí)技巧)-下篇
目錄
- 專用圖層
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 - 隱式動(dòng)畫
7.1 事務(wù)
7.2 完成塊
7.3 圖層行為
7.4 呈現(xiàn)與模型 - 顯示動(dòng)畫
8.1 屬性動(dòng)畫
8.2 動(dòng)畫組
8.3 過(guò)渡
8.4 在動(dòng)畫過(guò)程中取消動(dòng)畫 - 圖層時(shí)間
9.1 CAMediaTiming協(xié)議
9.2 層級(jí)關(guān)系時(shí)間
9.3 手動(dòng)動(dòng)畫 - 緩沖
10.1 動(dòng)畫速度
10.2 自定義緩沖函數(shù)
第六章:專用圖層
前言:
到目前為止,我們已經(jīng)探討過(guò)CALayer類了,同時(shí)我們也了解到了一些非常有用的繪圖和動(dòng)畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會(huì)學(xué)習(xí)其他的一些圖層類,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力。
第1篇:CAShapeLayer
CAShapeLayer是一個(gè)通過(guò)矢量圖形而不是bitmap來(lái)繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來(lái)定義想要繪制的圖形,最后CAShapeLayer就自動(dòng)渲染出來(lái)了。當(dāng)然,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個(gè)路徑,相比直下,使用CAShapeLayer有以下一些優(yōu)點(diǎn):
- 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會(huì)比用Core Graphics快很多。
- 高效使用內(nèi)存。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形,所以無(wú)論有多大,都不會(huì)占用太多的內(nèi)存。
- 不會(huì)被圖層邊界剪裁掉。一個(gè)CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會(huì)像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們?cè)诘诙滤?jiàn))。
- 不會(huì)出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時(shí),它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化。
創(chuàng)建一個(gè)CGPath
CAShapeLayer可以用來(lái)繪制所有能夠通過(guò)CGPath來(lái)表示的形狀。這個(gè)形狀不一定要閉合,圖層路徑也不一定要不可破,事實(shí)上你可以在一個(gè)圖層上繪制好幾個(gè)不同的形狀。你可以控制一些屬性比如lineWith(線寬,用點(diǎn)表示單位),lineCap(線條結(jié)尾的樣子),和lineJoin(線條之間的結(jié)合點(diǎn)的樣子);但是在圖層層面你只有一次機(jī)會(huì)設(shè)置這些屬性。如果你想用不同顏色或風(fēng)格來(lái)繪制多個(gè)形狀,就不得不為每個(gè)形狀準(zhǔn)備一個(gè)圖層了。
下面的代碼用一個(gè)CAShapeLayer渲染一個(gè)簡(jiǎn)單的火柴人。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創(chuàng)建了圖層路徑,這樣我們就不用考慮人工釋放CGPath了。
UIBezierPath *path = [[UIBezierPath alloc] init];
[path moveToPoint:CGPointMake(175, 100)];
[path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
[path moveToPoint:CGPointMake(150, 125)];
[path addLineToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(125, 225)];
[path moveToPoint:CGPointMake(150, 175)];
[path addLineToPoint:CGPointMake(175, 225)];
[path moveToPoint:CGPointMake(100, 150)];
[path addLineToPoint:CGPointMake(200, 150)];
//create shape layer
CAShapeLayer *shapeLayer = [CAShapeLayer layer];
shapeLayer.strokeColor = [UIColor redColor].CGColor;
shapeLayer.fillColor = [UIColor clearColor].CGColor;
shapeLayer.lineWidth = 5;
shapeLayer.lineJoin = kCALineJoinRound;
shapeLayer.lineCap = kCALineCapRound;
shapeLayer.path = path.CGPath;
//add it to our view
[self.containerView.layer addSublayer:shapeLayer];
圓角
下面這段代碼使用UIBezierPath和CAShapeLayer繪制了一個(gè)有三個(gè)圓角一個(gè)直角的矩形:
//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
我們可以通過(guò)這個(gè)圖層路徑繪制一個(gè)既有直角又有圓角的視圖。如果我們想依照此圖形來(lái)剪裁視圖內(nèi)容,我們可以把CAShapeLayer作為視圖的宿主圖層,而不是添加一個(gè)子視圖(圖層蒙板的詳細(xì)解釋見(jiàn)第四章『視覺(jué)效果』)。
第2篇:CATextLayer
如果你想在一個(gè)圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內(nèi)容(這就是UILabel的精髓)。
Core Animation提供了一個(gè)CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎所有的繪制特性,并且額外提供了一些新的特性。
同樣,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其實(shí)是通過(guò)WebKit來(lái)實(shí)現(xiàn)繪制的,這樣就造成了當(dāng)有很多文字的時(shí)候就會(huì)有極大的性能壓力。而CATextLayer使用了Core text,并且渲染得非常快。
用CATextLayer來(lái)實(shí)現(xiàn)一個(gè)UILabel
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
[self.labelView.layer addSublayer:textLayer];
//set text attributes
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
//choose a font
UIFont *font = [UIFont systemFontOfSize:15];
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
textLayer.font = fontRef;
textLayer.fontSize = font.pointSize;
CGFontRelease(fontRef);
//choose some text
NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
//set layer text
textLayer.string = text;
如果你實(shí)現(xiàn)了代碼并且界面顯示出來(lái)了,你會(huì)發(fā)現(xiàn)一個(gè)奇怪的地方:這些文本有一些像素化了。這是因?yàn)椴](méi)有以Retina的方式渲染,第二章提到了這個(gè)contentScale屬性,用來(lái)決定圖層內(nèi)容應(yīng)該以怎樣的分辨率來(lái)渲染。contentsScale并不關(guān)心屏幕的拉伸因素而總是默認(rèn)為1.0。如果我們想以Retina的質(zhì)量來(lái)顯示文字,我們就得手動(dòng)地設(shè)置CATextLayer的contentsScale屬性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
注意:CATextLayer的string屬性并不是你想象的NSString類型,而是id類型。這樣你既可以用NSString也可以用NSAttributedString來(lái)指定文本了(注意,NSAttributedString并不是NSString的子類)。屬性化字符串是iOS用來(lái)渲染字體風(fēng)格的機(jī)制,它以特定的方式來(lái)決定指定范圍內(nèi)的字符串的原始信息,比如字體,顏色,字重,斜體等。
富文本
用CATextLayer實(shí)現(xiàn)一個(gè)富文本標(biāo)簽。
UILabel的替代品
我們想要一個(gè)用CATextLayer作為宿主圖層的UILabel子類,這樣就可以沒(méi)有冗余的寄宿圖啦。
就像我們?cè)诘谝徽隆簣D層樹(shù)』討論的一樣,每一個(gè)UIView都是寄宿在一個(gè)CALayer的示例上。這個(gè)圖層是由視圖自動(dòng)創(chuàng)建和管理的,那我們可以用別的圖層類型替代它么?一旦被創(chuàng)建,我們就無(wú)法代替這個(gè)圖層了。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創(chuàng)建的時(shí)候能返回一個(gè)不同的圖層子類。UIView會(huì)在初始化的時(shí)候調(diào)用+layerClass方法,然后用它的返回類型來(lái)創(chuàng)建宿主圖層。
下面代碼演示了一個(gè)UILabel子類LayerLabel用CATextLayer繪制它的問(wèn)題,而不是調(diào)用一般的UILabel使用的較慢的-drawRect:方法
#import "LayerLabel.h"
@implementation LayerLabel
+ (Class)layerClass {
//this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
return [CATextLayer class];
}
- (CATextLayer *)textLayer {
return (CATextLayer *)self.layer;
}
- (void)setUp {
//set defaults from UILabel settings
self.text = self.text;
self.textColor = self.textColor;
self.font = self.font;
//we should really derive these from the UILabel settings too
//but that's complicated, so for now we'll just hard-code them
[self textLayer].alignmentMode = kCAAlignmentJustified;
[self textLayer].wrapped = YES;
[self.layer display];
}
- (id)initWithFrame:(CGRect)frame {
//called when creating label programmatically
if (self = [super initWithFrame:frame]) {
[self setUp];
}
return self;
}
- (void)awakeFromNib {
//called when creating label using Interface Builder
[self setUp];
}
- (void)setText:(NSString *)text {
super.text = text;
//set layer text
[self textLayer].string = text;
}
- (void)setTextColor:(UIColor *)textColor {
super.textColor = textColor;
//set layer text color
[self textLayer].foregroundColor = textColor.CGColor;
}
- (void)setFont:(UIFont *)font {
super.font = font;
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
[self textLayer].font = fontRef;
[self textLayer].fontSize = font.pointSize;
CGFontRelease(fontRef);
}
@end
如果你運(yùn)行代碼,你會(huì)發(fā)現(xiàn)文本并沒(méi)有像素化,而我們也沒(méi)有設(shè)置contentsScale屬性。把CATextLayer作為宿主圖層的另一好處就是視圖自動(dòng)設(shè)置了contentsScale屬性。
總得來(lái)說(shuō),如果想在app里面充分利用CALayer子類,用+layerClass來(lái)創(chuàng)建基于不同圖層的視圖是一個(gè)簡(jiǎn)單可復(fù)用的方法。
第3篇:CATransformLayer
CATransformLayer不同于普通的CALayer,因?yàn)樗荒茱@示它自己的內(nèi)容。只有當(dāng)存在了一個(gè)能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層,所以它能夠用于構(gòu)造一個(gè)層級(jí)的3D結(jié)構(gòu)
第4篇:CAGradientLayer
CAGradientLayer是用來(lái)生成兩種或更多顏色平滑漸變的。用Core Graphics復(fù)制一個(gè)CAGradientLayer并將內(nèi)容繪制到一個(gè)普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。
基礎(chǔ)漸變:
用CAGradientLayer實(shí)現(xiàn)簡(jiǎn)單的兩種顏色的對(duì)角線漸變
//create gradient layer and add it to our container view
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:gradientLayer];
//set gradient colors
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];
//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
多重漸變:
可以用locations屬性來(lái)調(diào)整空間。locations屬性是一個(gè)浮點(diǎn)數(shù)值的數(shù)組(以NSNumber包裝)。這些浮點(diǎn)數(shù)定義了colors屬性中每個(gè)不同顏色的位置,同樣的,也是以單位坐標(biāo)系進(jìn)行標(biāo)定。0.0代表著漸變的開(kāi)始,1.0代表著結(jié)束。
locations數(shù)組并不是強(qiáng)制要求的,但是如果你給它賦值了就一定要確保locations的數(shù)組大小和colors數(shù)組大小一定要相同,否則你將會(huì)得到一個(gè)空白的漸變。
用locations構(gòu)造偏移至左上角的三色漸變
//create gradient layer and add it to our container view
CAGradientLayer *gradientLayer = [CAGradientLayer layer];
gradientLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:gradientLayer];
//set gradient colors
gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id) [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];
//set locations
gradientLayer.locations = @[@0.0, @0.25, @0.5];
//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
第5篇:CAReplicatorLayer
CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會(huì)繪制一個(gè)或多個(gè)圖層的子圖層,并在每個(gè)復(fù)制體上應(yīng)用不同的變換。
重復(fù)圖層(Repeating Layers)
第6篇:CAScrollLayer
第7篇:CATiledLayer
第8篇:CAEmitterLayer
第9篇:CAEAGLLayer
第10篇:AVPlayerLayer
總結(jié):
這一章我們簡(jiǎn)要概述了一些專用圖層以及用他們實(shí)現(xiàn)的一些效果,我們只是了解到這些圖層的皮毛,像CATiledLayer和CAEMitterLayer這些類可以單獨(dú)寫一章的。但是,重點(diǎn)是記住CALayer是用處很大的,而且它并沒(méi)有為所有可能的場(chǎng)景進(jìn)行優(yōu)化。為了獲得Core Animation最好的性能,你需要為你的工作選對(duì)正確的工具,希望你能夠挖掘這些不同的CALayer子類的功能。 這一章我們通過(guò)CAEmitterLayer和AVPlayerLayer類簡(jiǎn)單地接觸到了一些動(dòng)畫,在第二章,我們將繼續(xù)深入研究動(dòng)畫,就從隱式動(dòng)畫開(kāi)始。
第七章:隱式動(dòng)畫
前言:
我們?cè)诘谝徊糠钟懻摿薈ore Animation除了動(dòng)畫之外可以做到的任何事情。但是動(dòng)畫是Core Animation庫(kù)一個(gè)非常顯著的特性。這一章我們來(lái)看看它是怎么做到的。具體來(lái)說(shuō),我們先來(lái)討論框架自動(dòng)完成的隱式動(dòng)畫(除非你明確禁用了這個(gè)功能)。
第1篇:事務(wù)
Core Animation基于一個(gè)假設(shè),說(shuō)屏幕上的任何東西都可以(或者可能)做動(dòng)畫。動(dòng)畫并不需要你在Core Animation中手動(dòng)打開(kāi),相反需要明確地關(guān)閉,否則他會(huì)一直存在。
當(dāng)你改變CALayer的一個(gè)可做動(dòng)畫的屬性,它并不能立刻在屏幕上體現(xiàn)出來(lái)。相反,它是從先前的值平滑過(guò)渡到新的值。這一切都是默認(rèn)的行為,你不需要做額外的操作。
這其實(shí)就是所謂的隱式動(dòng)畫。之所以叫隱式是因?yàn)槲覀儾](méi)有指定任何動(dòng)畫的類型 。我們僅僅改變了一個(gè)屬性,然后Core Animation來(lái)決定如何并且何時(shí)去做動(dòng)畫。Core Animaiton同樣支持顯式動(dòng)畫,下章詳細(xì)說(shuō)明。
但當(dāng)你改變一個(gè)屬性,Core Animation是如何判斷動(dòng)畫類型和持續(xù)時(shí)間的呢?實(shí)際上動(dòng)畫執(zhí)行的時(shí)間取決于當(dāng)前事務(wù)的設(shè)置,動(dòng)畫類型取決于圖層行為。
事務(wù)實(shí)際上是Core Animation用來(lái)包含一系列屬性動(dòng)畫集合的機(jī)制,任何用指定事務(wù)去改變可以做動(dòng)畫的圖層屬性都不會(huì)立刻發(fā)生變化,而是當(dāng)事務(wù)一旦提交的時(shí)候開(kāi)始用一個(gè)動(dòng)畫過(guò)渡到新值。
事務(wù)是通過(guò)CATransaction類來(lái)做管理,CATransaction沒(méi)有屬性或者實(shí)例方法,并且也不能用+alloc和-init方法創(chuàng)建它。但是可以用+begin和+commit分別來(lái)入棧或者出棧。
任何可以做動(dòng)畫的圖層屬性都會(huì)被添加到棧頂?shù)氖聞?wù),你可以通過(guò)+setAnimationDuration:方法設(shè)置當(dāng)前事務(wù)的動(dòng)畫時(shí)間,或者通過(guò)+animationDuration方法來(lái)獲取值(默認(rèn)0.25秒)。
Core Animation在每個(gè)run loop周期中自動(dòng)開(kāi)始一次新的事務(wù)(run loop是iOS負(fù)責(zé)收集用戶輸入,處理定時(shí)器或者網(wǎng)絡(luò)事件并且重新繪制屏幕的東西),即使你不顯式的用[CATransaction begin]開(kāi)始一次事務(wù),任何在一次run loop循環(huán)中屬性的改變都會(huì)被集中起來(lái),然后做一次0.25秒的動(dòng)畫。
使用CATransaction控制動(dòng)畫時(shí)間
- (IBAction)changeColor {
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
}
如果你用過(guò)UIView的動(dòng)畫方法做過(guò)一些動(dòng)畫效果,那么應(yīng)該對(duì)這個(gè)模式不陌生。UIView有兩個(gè)方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin和+commit方法類似。實(shí)際上在+beginAnimations:context:和+commitAnimations之間所有視圖或者圖層屬性的改變而做的動(dòng)畫都是由于設(shè)置了CATransaction的原因。
在iOS4中,蘋果對(duì)UIView添加了一種基于block的動(dòng)畫方法:+animateWithDuration:animations:。這樣寫對(duì)做一堆的屬性動(dòng)畫在語(yǔ)法上會(huì)更加簡(jiǎn)單,但實(shí)質(zhì)上它們都是在做同樣的事情。
CATransaction的+begin和+commit方法在+animateWithDuration:animations:內(nèi)部自動(dòng)調(diào)用,這樣block中所有屬性的改變都會(huì)被事務(wù)所包含。這樣也可以避免開(kāi)發(fā)者由于對(duì)+begin和+commit匹配的失誤造成的風(fēng)險(xiǎn)。
第2篇:完成塊
基于UIView的block的動(dòng)畫允許你在動(dòng)畫結(jié)束的時(shí)候提供一個(gè)完成的動(dòng)作。CATranscation接口提供的+setCompletionBlock:方法也有同樣的功能。
在每次顏色變化結(jié)束之后切換到另一個(gè)旋轉(zhuǎn)90的動(dòng)畫
- (IBAction)changeColor {
//begin a new transaction
[CATransaction begin];
//set the animation duration to 1 second
[CATransaction setAnimationDuration:1.0];
//add the spin animation on completion
[CATransaction setCompletionBlock:^{
//rotate the layer 90 degrees
CGAffineTransform transform = self.colorLayer.affineTransform;
transform = CGAffineTransformRotate(transform, M_PI_2);
self.colorLayer.affineTransform = transform;
}];
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
//commit the transaction
[CATransaction commit];
}
注意旋轉(zhuǎn)動(dòng)畫要比顏色漸變快得多,這是因?yàn)橥瓿蓧K是在顏色漸變的事務(wù)提交并出棧之后才被執(zhí)行
第3篇:圖層行為
Core Animation通常對(duì)CALayer的所有屬性(可動(dòng)畫的屬性)做動(dòng)畫,但是UIView把它關(guān)聯(lián)的圖層的這個(gè)特性關(guān)閉了。為了更好說(shuō)明這一點(diǎn),我們需要知道隱式動(dòng)畫是如何實(shí)現(xiàn)的。
我們把改變屬性時(shí)CALayer自動(dòng)應(yīng)用的動(dòng)畫稱作行為,當(dāng)CALayer的屬性被修改時(shí)候,它會(huì)調(diào)用-actionForKey:方法,傳遞屬性的名稱。剩下的操作都在CALayer的頭文件中有詳細(xì)的說(shuō)明,實(shí)質(zhì)上是如下幾步:
- 圖層首先檢測(cè)它是否有委托,并且是否實(shí)現(xiàn)CALayerDelegate協(xié)議指定的-actionForLayer:forKey方法。如果有,直接調(diào)用并返回結(jié)果。
- 如果沒(méi)有委托,或者委托沒(méi)有實(shí)現(xiàn)-actionForLayer:forKey方法,圖層接著檢查包含屬性名稱對(duì)應(yīng)行為映射的actions字典。
- 如果actions字典沒(méi)有包含對(duì)應(yīng)的屬性,那么圖層接著在它的style字典接著搜索屬性名。
- 最后,如果在style里面也找不到對(duì)應(yīng)的行為,那么圖層將會(huì)直接調(diào)用定義了每個(gè)屬性的標(biāo)準(zhǔn)行為的-defaultActionForKey:方法。
所以一輪完整的搜索結(jié)束之后,-actionForKey:要么返回空(這種情況下將不會(huì)有動(dòng)畫發(fā)生),要么是CAAction協(xié)議對(duì)應(yīng)的對(duì)象,最后CALayer拿這個(gè)結(jié)果去對(duì)先前和當(dāng)前的值做動(dòng)畫。
于是這就解釋了UIKit是如何禁用隱式動(dòng)畫的:每個(gè)UIView對(duì)它關(guān)聯(lián)的圖層都扮演了一個(gè)委托,并且提供了-actionForLayer:forKey的實(shí)現(xiàn)方法。當(dāng)不在一個(gè)動(dòng)畫塊的實(shí)現(xiàn)中,UIView對(duì)所有圖層行為返回nil,但是在動(dòng)畫block范圍之內(nèi),它就返回了一個(gè)非空值,我們可以用一個(gè)demo做個(gè)簡(jiǎn)單的實(shí)驗(yàn):
//test layer action when outside of animation block
NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//begin animation block
[UIView beginAnimations:nil context:nil];
//test layer action when inside of animation block
NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]);
//end animation block
[UIView commitAnimations];
運(yùn)行程序,控制臺(tái)顯示結(jié)果如下:
$ LayerTest[21215:c07] Outside: <null>
$ LayerTest[21215:c07] Inside: <CABasicAnimation: 0x757f090>
于是我們可以預(yù)言,當(dāng)屬性在動(dòng)畫塊之外發(fā)生改變,UIView直接通過(guò)返回nil來(lái)禁用隱式動(dòng)畫。但如果在動(dòng)畫塊范圍之內(nèi),根據(jù)動(dòng)畫具體類型返回相應(yīng)的屬性,在這個(gè)例子就是CABasicAnimation(第八章“顯式動(dòng)畫”將會(huì)提到)。
當(dāng)然返回nil并不是禁用隱式動(dòng)畫唯一的辦法,CATransacition有個(gè)方法叫做+setDisableActions:,可以用來(lái)對(duì)所有屬性打開(kāi)或者關(guān)閉隱式動(dòng)畫。如果在清單7.2的[CATransaction begin]之后添加下面的代碼,同樣也會(huì)阻止動(dòng)畫的發(fā)生:
[CATransaction setDisableActions:YES];
總結(jié)一下,我們知道了如下幾點(diǎn)
UIView關(guān)聯(lián)的圖層禁用了隱式動(dòng)畫,對(duì)這種圖層做動(dòng)畫的唯一辦法就是使用UIView的動(dòng)畫函數(shù)(而不是依賴CATransaction),或者繼承UIView,并覆蓋-actionForLayer:forKey:方法,或者直接創(chuàng)建一個(gè)顯式動(dòng)畫(具體細(xì)節(jié)見(jiàn)第八章)。
對(duì)于單獨(dú)存在的圖層,我們可以通過(guò)實(shí)現(xiàn)圖層的-actionForLayer:forKey:委托方法,或者提供一個(gè)actions字典來(lái)控制隱式動(dòng)畫。
第4篇:呈現(xiàn)與模型
CALayer的屬性行為其實(shí)很不正常,因?yàn)楦淖円粋€(gè)圖層的屬性并沒(méi)有立刻生效,而是通過(guò)一段時(shí)間漸變更新。這是怎么做到的呢?
當(dāng)你改變一個(gè)圖層的屬性,屬性值的確是立刻更新的(如果你讀取它的數(shù)據(jù),你會(huì)發(fā)現(xiàn)它的值在你設(shè)置它的那一刻就已經(jīng)生效了),但是屏幕上并沒(méi)有馬上發(fā)生改變。這是因?yàn)槟阍O(shè)置的屬性并沒(méi)有直接調(diào)整圖層的外觀,相反,他只是定義了圖層動(dòng)畫結(jié)束之后將要變化的外觀。
在iOS中,屏幕每秒鐘重繪60次。如果動(dòng)畫時(shí)長(zhǎng)比60分之一秒要長(zhǎng),Core Animation就需要在設(shè)置一次新值和新值生效之間,對(duì)屏幕上的圖層進(jìn)行重新組織。這意味著CALayer除了“真實(shí)”值(就是你設(shè)置的值)之外,必須要知道當(dāng)前顯示在屏幕上的屬性值的記錄。
每個(gè)圖層屬性的顯示值都被存儲(chǔ)在一個(gè)叫做呈現(xiàn)圖層的獨(dú)立圖層當(dāng)中,他可以通過(guò)-presentationLayer方法來(lái)訪問(wèn)。這個(gè)呈現(xiàn)圖層實(shí)際上是模型圖層的復(fù)制,但是它的屬性值代表了在任何指定時(shí)刻當(dāng)前外觀效果。換句話說(shuō),你可以通過(guò)呈現(xiàn)圖層的值來(lái)獲取當(dāng)前屏幕上真正顯示出來(lái)的值
你可能注意到有一個(gè)叫做–modelLayer的方法。在呈現(xiàn)圖層上調(diào)用–modelLayer將會(huì)返回它正在呈現(xiàn)所依賴的CALayer。通常在一個(gè)圖層上調(diào)用-modelLayer會(huì)返回–self(實(shí)際上我們已經(jīng)創(chuàng)建的原始圖層就是一種數(shù)據(jù)模型)。
大多數(shù)情況下,你不需要直接訪問(wèn)呈現(xiàn)圖層,你可以通過(guò)和模型圖層的交互,來(lái)讓Core Animation更新顯示。兩種情況下呈現(xiàn)圖層會(huì)變得很有用,一個(gè)是同步動(dòng)畫,一個(gè)是處理用戶交互。
- 如果你在實(shí)現(xiàn)一個(gè)基于定時(shí)器的動(dòng)畫(見(jiàn)第11章“基于定時(shí)器的動(dòng)畫”),而不僅僅是基于事務(wù)的動(dòng)畫,這個(gè)時(shí)候準(zhǔn)確地知道在某一時(shí)刻圖層顯示在什么位置就會(huì)對(duì)正確擺放圖層很有用了。
- 如果你想讓你做動(dòng)畫的圖層響應(yīng)用戶輸入,你可以使用-hitTest:方法(見(jiàn)第三章“圖層幾何學(xué)”)來(lái)判斷指定圖層是否被觸摸,這時(shí)候?qū)Τ尸F(xiàn)圖層而不是模型圖層調(diào)用-hitTest:會(huì)顯得更有意義,因?yàn)槌尸F(xiàn)圖層代表了用戶當(dāng)前看到的圖層位置,而不是當(dāng)前動(dòng)畫結(jié)束之后的位置。
我們可以用一個(gè)簡(jiǎn)單的案例來(lái)證明后者(見(jiàn)下面代碼)。在這個(gè)例子中,點(diǎn)擊屏幕上的任意位置將會(huì)讓圖層平移到那里。點(diǎn)擊圖層本身可以隨機(jī)改變它的顏色。我們通過(guò)對(duì)呈現(xiàn)圖層調(diào)用-hitTest:來(lái)判斷是否被點(diǎn)擊。
如果修改代碼讓-hitTest:直接作用于colorLayer而不是呈現(xiàn)圖層,你會(huì)發(fā)現(xiàn)當(dāng)圖層移動(dòng)的時(shí)候它并不能正確顯示。這時(shí)候你就需要點(diǎn)擊圖層將要移動(dòng)到的位置而不是圖層本身來(lái)響應(yīng)點(diǎn)擊(這就是為什么用呈現(xiàn)圖層來(lái)響應(yīng)交互的原因)。
使用presentationLayer圖層來(lái)判斷當(dāng)前圖層位置
- (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, self.view.bounds.size.height / 2);
self.colorLayer.backgroundColor = [UIColor redColor].CGColor;
[self.view.layer addSublayer:self.colorLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event {
//get the touch point
CGPoint point = [[touches anyObject] locationInView:self.view];
//check if we've tapped the moving layer
if ([self.colorLayer.presentationLayer hitTest:point]) {
//randomize the layer background color
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
} else {
//otherwise (slowly) move the layer to new position
[CATransaction begin];
[CATransaction setAnimationDuration:4.0];
self.colorLayer.position = point;
[CATransaction commit];
}
}
總結(jié):
這一章討論了隱式動(dòng)畫,還有Core Animation對(duì)指定屬性選擇合適的動(dòng)畫行為的機(jī)制。同時(shí)你知道了UIKit是如何充分利用Core Animation的隱式動(dòng)畫機(jī)制來(lái)強(qiáng)化它的顯式系統(tǒng),以及動(dòng)畫是如何被默認(rèn)禁用并且當(dāng)需要的時(shí)候啟用的。最后,你了解了呈現(xiàn)和模型圖層,以及Core Animation是如何通過(guò)它們來(lái)判斷出圖層當(dāng)前位置以及將要到達(dá)的位置。
在下一章中,我們將研究Core Animation提供的顯式動(dòng)畫類型,既可以直接對(duì)圖層屬性做動(dòng)畫,也可以覆蓋默認(rèn)的圖層行為。
第八章:顯式動(dòng)畫
前言:
上一章介紹了隱式動(dòng)畫的概念。隱式動(dòng)畫是在iOS平臺(tái)創(chuàng)建動(dòng)態(tài)用戶界面的一種直接方式,也是UIKit動(dòng)畫機(jī)制的基礎(chǔ),不過(guò)它并不能涵蓋所有的動(dòng)畫類型。在這一章中,我們將要研究一下顯式動(dòng)畫,它能夠?qū)σ恍傩宰鲋付ǖ淖远x動(dòng)畫,或者創(chuàng)建非線性動(dòng)畫,比如沿著任意一條曲線移動(dòng)。
第1篇:屬性動(dòng)畫
當(dāng)使用-addAnimation:forKey:把動(dòng)畫添加到圖層,這里有一個(gè)到目前為止我們都設(shè)置為nil的key參數(shù)。這里的鍵是-animationForKey:方法找到對(duì)應(yīng)動(dòng)畫的唯一標(biāo)識(shí)符,而當(dāng)前動(dòng)畫的所有鍵都可以用animationKeys獲取。如果我們對(duì)每個(gè)動(dòng)畫都關(guān)聯(lián)一個(gè)唯一的鍵,就可以對(duì)每個(gè)圖層循環(huán)所有鍵,然后調(diào)用-animationForKey:來(lái)比對(duì)結(jié)果。盡管這不是一個(gè)優(yōu)雅的實(shí)現(xiàn)。
幸運(yùn)的是,還有一種更加簡(jiǎn)單的方法。像所有的NSObject子類一樣,CAAnimation實(shí)現(xiàn)了KVC(鍵-值-編碼)協(xié)議,于是你可以用-setValue:forKey:和-valueForKey:方法來(lái)存取屬性。但是CAAnimation有一個(gè)不同的性能:它更像一個(gè)NSDictionary,可以讓你隨意設(shè)置鍵值對(duì),即使和你使用的動(dòng)畫類所聲明的屬性并不匹配。
這意味著你可以對(duì)動(dòng)畫用任意類型打標(biāo)簽。在這里,我們給UIView類型的指針添加的動(dòng)畫,所以可以簡(jiǎn)單地判斷動(dòng)畫到底屬于哪個(gè)視圖,然后在委托方法中用這個(gè)信息正確地更新鐘的指針(見(jiàn)下面的代碼)。
使用KVC對(duì)動(dòng)畫打標(biāo)簽
CABasicAnimation *animation = [CABasicAnimation animation];
[self updateHandsAnimated:NO];
animation.keyPath = @"transform";
animation.toValue = [NSValue valueWithCATransform3D:transform];
animation.duration = 0.5;
animation.delegate = self;
[animation setValue:handView forKey:@"handView"];
[handView.layer addAnimation:animation forKey:nil];
- (void)animationDidStop:(CABasicAnimation *)anim finished:(BOOL)flag {
//set final position for hand view
UIView *handView = [anim valueForKey:@"handView"];
handView.layer.transform = [anim.toValue CATransform3DValue];
}
我們成功的識(shí)別出每個(gè)圖層停止動(dòng)畫的時(shí)間,然后更新它的變換到一個(gè)新值,很好。
不幸的是,即使做了這些,還是有個(gè)問(wèn)題,該代碼在模擬器上運(yùn)行的很好,但當(dāng)真正跑在iOS設(shè)備上時(shí),我們發(fā)現(xiàn)在-animationDidStop:finished:委托方法調(diào)用之前,指針會(huì)迅速返回到原始值,這個(gè)清單8.3圖層顏色發(fā)生的情況一樣。
問(wèn)題在于回調(diào)方法在動(dòng)畫完成之前已經(jīng)被調(diào)用了,但不能保證這發(fā)生在屬性動(dòng)畫返回初始狀態(tài)之前。這同時(shí)也很好地說(shuō)明了為什么要在真實(shí)的設(shè)備上測(cè)試動(dòng)畫代碼,而不僅僅是模擬器。
我們可以用一個(gè)fillMode屬性來(lái)解決這個(gè)問(wèn)題,下一章會(huì)詳細(xì)說(shuō)明,這里知道在動(dòng)畫之前設(shè)置它比在動(dòng)畫結(jié)束之后更新屬性更加方便。
關(guān)鍵幀動(dòng)畫
CABasicAnimation揭示了大多數(shù)隱式動(dòng)畫背后依賴的機(jī)制,這的確很有趣,但是顯式地給圖層添加CABasicAnimation相較于隱式動(dòng)畫而言,只能說(shuō)費(fèi)力不討好。
CAKeyframeAnimation是另一種UIKit沒(méi)有暴露出來(lái)但功能強(qiáng)大的類。和CABasicAnimation類似,CAKeyframeAnimation同樣是CAPropertyAnimation的一個(gè)子類,它依然作用于單一的一個(gè)屬性,但是和CABasicAnimation不一樣的是,它不限制于設(shè)置一個(gè)起始和結(jié)束的值,而是可以根據(jù)一連串隨意的值來(lái)做動(dòng)畫。
設(shè)置一個(gè)顏色的數(shù)組,然后通過(guò)關(guān)鍵幀動(dòng)畫播放出來(lái)(見(jiàn)下面的代碼)
使用CAKeyframeAnimation應(yīng)用一系列顏色的變化
- (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 ];
//apply animation to layer
[self.colorLayer addAnimation:animation forKey:nil];
}
運(yùn)行它,你會(huì)發(fā)現(xiàn)動(dòng)畫通過(guò)顏色不斷循環(huán),但效果看起來(lái)有些奇怪。原因在于動(dòng)畫以一個(gè)恒定的步調(diào)在運(yùn)行。當(dāng)在每個(gè)動(dòng)畫之間過(guò)渡的時(shí)候并沒(méi)有減速,這就產(chǎn)生了一個(gè)略微奇怪的效果,為了讓動(dòng)畫看起來(lái)更自然,我們需要調(diào)整一下緩沖,第十章將會(huì)詳細(xì)說(shuō)明。
提供一個(gè)數(shù)組的值就可以按照顏色變化做動(dòng)畫,但一般來(lái)說(shuō)用數(shù)組來(lái)描述動(dòng)畫運(yùn)動(dòng)并不直觀。CAKeyframeAnimation有另一種方式去指定動(dòng)畫,就是使用CGPath。path屬性可以用一種直觀的方式,使用Core Graphics函數(shù)定義運(yùn)動(dòng)序列來(lái)繪制動(dòng)畫。
虛擬屬性
之前提到過(guò)屬性動(dòng)畫實(shí)際上是針對(duì)于關(guān)鍵路徑而不是一個(gè)鍵,這就意味著可以對(duì)子屬性甚至是虛擬屬性做動(dòng)畫。但是虛擬屬性到底是什么呢?
考慮一個(gè)旋轉(zhuǎn)的動(dòng)畫:如果想要對(duì)一個(gè)物體做旋轉(zhuǎn)的動(dòng)畫,那就需要作用于transform屬性,因?yàn)镃ALayer沒(méi)有顯式提供角度或者方向之類的屬性。
解決方案:為了旋轉(zhuǎn)圖層,我們可以對(duì)transform.rotation關(guān)鍵路徑應(yīng)用動(dòng)畫,而不是transform本身。
對(duì)虛擬的transform.rotation屬性做動(dòng)畫
//add the ship
CALayer *shipLayer = [CALayer layer];
shipLayer.frame = CGRectMake(0, 0, 128, 128);
shipLayer.position = CGPointMake(150, 150);
shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage;
[self.containerView.layer addSublayer:shipLayer];
//animate the ship rotation
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation";
animation.duration = 2.0;
animation.byValue = @(M_PI * 2);
[shipLayer addAnimation:animation forKey:nil];”
用transform.rotation而不是transform做動(dòng)畫的好處如下:
- 我們可以不通過(guò)關(guān)鍵幀一步旋轉(zhuǎn)多于180度的動(dòng)畫。
- 可以用相對(duì)值而不是絕對(duì)值旋轉(zhuǎn)(設(shè)置byValue而不是toValue)。
- 可以不用創(chuàng)建CATransform3D,而是使用一個(gè)簡(jiǎn)單的數(shù)值來(lái)指定角度。
- 不會(huì)和transform.position或者transform.scale沖突(同樣是使用關(guān)鍵路徑來(lái)做獨(dú)立的動(dòng)畫屬性)。
transform.rotation屬性有一個(gè)奇怪的問(wèn)題是它其實(shí)并不存在。這是因?yàn)镃ATransform3D并不是一個(gè)對(duì)象,它實(shí)際上是一個(gè)結(jié)構(gòu)體,也沒(méi)有符合KVC相關(guān)屬性,transform.rotation實(shí)際上是一個(gè)CALayer用于處理動(dòng)畫變換的虛擬屬性。
第2篇:動(dòng)畫組
CABasicAnimation和CAKeyframeAnimation僅僅作用于單獨(dú)的屬性,而CAAnimationGroup可以把這些動(dòng)畫組合在一起。CAAnimationGroup是另一個(gè)繼承于CAAnimation的子類,它添加了一個(gè)animations數(shù)組的屬性,用來(lái)組合別的動(dòng)畫。
第3篇:過(guò)渡
屬性動(dòng)畫只對(duì)圖層的可動(dòng)畫屬性起作用,所以如果要改變一個(gè)不能動(dòng)畫的屬性(比如圖片),或者從層級(jí)關(guān)系中添加或者移除圖層,屬性動(dòng)畫將不起作用。
于是就有了過(guò)渡的概念。過(guò)渡并不像屬性動(dòng)畫那樣平滑地在兩個(gè)值之間做動(dòng)畫,而是影響到整個(gè)圖層的變化。過(guò)渡動(dòng)畫首先展示之前的圖層外觀,然后通過(guò)一個(gè)交換過(guò)渡到新的外觀。
為了創(chuàng)建一個(gè)過(guò)渡動(dòng)畫,我們將使用CATransition,同樣是另一個(gè)CAAnimation的子類,和別的子類不同,CATransition有一個(gè)type和subtype來(lái)標(biāo)識(shí)變換效果。type屬性是一個(gè)NSString類型,可以被設(shè)置成如下類型:
- kCATransitionFade
- kCATransitionMoveIn
- kCATransitionPush
- kCATransitionReveal
到目前為止你只能使用上述四種類型,但你可以通過(guò)一些別的方法來(lái)自定義過(guò)渡效果,后續(xù)會(huì)詳細(xì)介紹。
默認(rèn)的過(guò)渡類型是kCATransitionFade,當(dāng)你在改變圖層屬性之后,就創(chuàng)建了一個(gè)平滑的淡入淡出效果。
我們?cè)诘谄哒碌睦又芯鸵呀?jīng)用到過(guò)kCATransitionPush,它創(chuàng)建了一個(gè)新的圖層,從邊緣的一側(cè)滑動(dòng)進(jìn)來(lái),把舊圖層從另一側(cè)推出去的效果。
kCATransitionMoveIn和kCATransitionReveal與kCATransitionPush類似,都實(shí)現(xiàn)了一個(gè)定向滑動(dòng)的動(dòng)畫,但是有一些細(xì)微的不同,kCATransitionMoveIn從頂部滑動(dòng)進(jìn)入,但不像推送動(dòng)畫那樣把老土層推走,然而kCATransitionReveal把原始的圖層滑動(dòng)出去來(lái)顯示新的外觀,而不是把新的圖層滑動(dòng)進(jìn)入。
后面三種過(guò)渡類型都有一個(gè)默認(rèn)的動(dòng)畫方向,它們都從左側(cè)滑入,但是你可以通過(guò)subtype來(lái)控制它們的方向,提供了如下四種類型:
- kCATransitionFromRight
- kCATransitionFromLeft
- kCATransitionFromTop
- kCATransitionFromBottom
一個(gè)簡(jiǎn)單的用CATransition來(lái)對(duì)非動(dòng)畫屬性做動(dòng)畫的例子如下面所示,這里我們對(duì)UIImage的image屬性做修改,但是隱式動(dòng)畫或者CAPropertyAnimation都不能對(duì)它做動(dòng)畫,因?yàn)镃ore Animation不知道如何在插圖圖片。通過(guò)對(duì)圖層應(yīng)用一個(gè)淡入淡出的過(guò)渡,我們可以忽略它的內(nèi)容來(lái)做平滑動(dòng)畫,我們來(lái)嘗試修改過(guò)渡的type常量來(lái)觀察其它效果。
使用CATransition對(duì)圖像平滑淡入淡出
interface ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *imageView;
@property (nonatomic, copy) NSArray *images;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//set up images
self.images = @[[UIImage imageNamed:@"Anchor.png"],
[UIImage imageNamed:@"Cone.png"],
[UIImage imageNamed:@"Igloo.png"],
[UIImage imageNamed:@"Spaceship.png"]];
}
- (IBAction)switchImage {
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to imageview backing layer
[self.imageView.layer addAnimation:transition forKey:nil];
//cycle to next image
UIImage *currentImage = self.imageView.image;
NSUInteger index = [self.images indexOfObject:currentImage];
index = (index + 1) % [self.images count];
self.imageView.image = self.images[index];
}
@end
你可以從代碼中看出,過(guò)渡動(dòng)畫和之前的屬性動(dòng)畫或者動(dòng)畫組添加到圖層上的方式一致,都是通過(guò)-addAnimation:forKey:方法。但是和屬性動(dòng)畫不同的是,對(duì)指定的圖層一次只能使用一次CATransition,因此,無(wú)論你對(duì)動(dòng)畫的鍵設(shè)置什么值,過(guò)渡動(dòng)畫都會(huì)對(duì)它的鍵設(shè)置成“transition”,也就是常量kCATransition。
隱式過(guò)渡
CATransision可以對(duì)圖層任何變化平滑過(guò)渡的事實(shí)使得它成為那些不好做動(dòng)畫的屬性圖層行為的理想候選。蘋果當(dāng)然意識(shí)到了這點(diǎn),并且當(dāng)設(shè)置了CALayer的content屬性的時(shí)候,CATransition的確是默認(rèn)的行為。但是對(duì)于視圖關(guān)聯(lián)的圖層,或者是其他隱式動(dòng)畫的行為,這個(gè)特性依然是被禁用的,但是對(duì)于你自己創(chuàng)建的圖層,這意味著對(duì)圖層contents圖片做的改動(dòng)都會(huì)自動(dòng)附上淡入淡出的動(dòng)畫。
對(duì)圖層樹(shù)的動(dòng)畫
CATransition并不作用于指定的圖層屬性,這就是說(shuō)你可以在即使不能準(zhǔn)確得知改變了什么的情況下對(duì)圖層做動(dòng)畫,例如,在不知道UITableView哪一行被添加或者刪除的情況下,直接就可以平滑地刷新它,或者在不知道UIViewController內(nèi)部的視圖層級(jí)的情況下對(duì)兩個(gè)不同的實(shí)例做過(guò)渡動(dòng)畫。
這些例子和我們之前所討論的情況完全不同,因?yàn)樗鼈儾粌H涉及到圖層的屬性,而且是整個(gè)圖層樹(shù)的改變--我們?cè)谶@種動(dòng)畫的過(guò)程中手動(dòng)在層級(jí)關(guān)系中添加或者移除圖層。
這里用到了一個(gè)小詭計(jì),要確保CATransition添加到的圖層在過(guò)渡動(dòng)畫發(fā)生時(shí)不會(huì)在樹(shù)狀結(jié)構(gòu)中被移除,否則CATransition將會(huì)和圖層一起被移除。一般來(lái)說(shuō),你只需要將動(dòng)畫添加到被影響圖層的superlayer。
在下列代碼中,我們展示了如何在UITabBarController切換標(biāo)簽的時(shí)候添加淡入淡出的動(dòng)畫。這里我們建立了默認(rèn)的標(biāo)簽應(yīng)用程序模板,然后用UITabBarControllerDelegate的-tabBarController:didSelectViewController:方法來(lái)應(yīng)用過(guò)渡動(dòng)畫。我們把動(dòng)畫添加到UITabBarController的視圖圖層上,于是在標(biāo)簽被替換的時(shí)候動(dòng)畫不會(huì)被移除。
對(duì)UITabBarController做動(dòng)畫
- (void)tabBarController:(UITabBarController *)tabBarController didSelectViewController:(UIViewController *)viewController {
//set up crossfade transition
CATransition *transition = [CATransition animation];
transition.type = kCATransitionFade;
//apply transition to tab bar controller's view
[self.tabBarController.view.layer addAnimation:transition forKey:nil];
}
自定義動(dòng)畫
我們證實(shí)了過(guò)渡是一種對(duì)那些不太好做平滑動(dòng)畫屬性的強(qiáng)大工具,但是CATransition的提供的動(dòng)畫類型太少了。
更奇怪的是蘋果通過(guò)UIView +transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:方法提供了Core Animation的過(guò)渡特性。但是這里的可用的過(guò)渡選項(xiàng)和CATransition的type屬性提供的常量完全不同。
但這并不意味著在iOS上就不能實(shí)現(xiàn)自定義的過(guò)渡效果了。這只是意味著你需要做一些額外的工作。就像之前提到的那樣,過(guò)渡動(dòng)畫做基礎(chǔ)的原則就是對(duì)原始的圖層外觀截圖,然后添加一段動(dòng)畫,平滑過(guò)渡到圖層改變之后那個(gè)截圖的效果。如果我們知道如何對(duì)圖層截圖,我們就可以使用屬性動(dòng)畫來(lái)代替CATransition或者是UIKit的過(guò)渡方法來(lái)實(shí)現(xiàn)動(dòng)畫。
事實(shí)證明,對(duì)圖層做截圖還是很簡(jiǎn)單的。CALayer有一個(gè)-renderInContext:方法,可以通過(guò)把它繪制到Core Graphics的上下文中捕獲當(dāng)前內(nèi)容的圖片,然后在另外的視圖中顯示出來(lái)。如果我們把這個(gè)截屏視圖置于原始視圖之上,就可以遮住真實(shí)視圖的所有變化,于是重新創(chuàng)建了一個(gè)簡(jiǎn)單的過(guò)渡效果。
清單8.14演示了一個(gè)基本的實(shí)現(xiàn)。我們對(duì)當(dāng)前視圖狀態(tài)截圖,然后在我們改變?cè)家晥D的背景色的時(shí)候?qū)貓D快速轉(zhuǎn)動(dòng)并且淡出,圖8.5展示了我們自定義的過(guò)渡效果。
為了讓事情更簡(jiǎn)單,我們用UIView -animateWithDuration:completion:方法來(lái)實(shí)現(xiàn)。雖然用CABasicAnimation可以達(dá)到同樣的效果,但是那樣的話我們就需要對(duì)圖層的變換和不透明屬性創(chuàng)建單獨(dú)的動(dòng)畫,然后當(dāng)動(dòng)畫結(jié)束的是哦戶在CAAnimationDelegate中把coverView從屏幕中移除。
用renderInContext:創(chuàng)建自定義過(guò)渡效果
@implementation ViewController
- (IBAction)performTransition {
//preserve the current view snapshot
UIGraphicsBeginImageContextWithOptions(self.view.bounds.size, YES, 0.0);
[self.view.layer renderInContext:UIGraphicsGetCurrentContext()];
UIImage *coverImage = UIGraphicsGetImageFromCurrentImageContext();
//insert snapshot view in front of this one
UIView *coverView = [[UIImageView alloc] initWithImage:coverImage];
coverView.frame = self.view.bounds;
[self.view addSubview:coverView];
//update the view (we'll simply randomize the layer background color)
CGFloat red = arc4random() / (CGFloat)INT_MAX;
CGFloat green = arc4random() / (CGFloat)INT_MAX;
CGFloat blue = arc4random() / (CGFloat)INT_MAX;
self.view.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0];
//perform animation (anything you like)
[UIView animateWithDuration:1.0 animations:^{
//scale, rotate and fade the view
CGAffineTransform transform = CGAffineTransformMakeScale(0.01, 0.01);
transform = CGAffineTransformRotate(transform, M_PI_2);
coverView.transform = transform;
coverView.alpha = 0.0;
} completion:^(BOOL finished) {
//remove the cover view now we're finished with it
[coverView removeFromSuperview];
}];
}
@end
這里有個(gè)警告:-renderInContext:捕獲了圖層的圖片和子圖層,但是不能對(duì)子圖層正確地處理變換效果,而且對(duì)視頻和OpenGL內(nèi)容也不起作用。但是用CATransition,或者用私有的截屏方式就沒(méi)有這個(gè)限制了。
第4篇:在動(dòng)畫過(guò)程中取消動(dòng)畫
之前提到過(guò),你可以用-addAnimation:forKey:方法中的key參數(shù)來(lái)在添加動(dòng)畫之后檢索一個(gè)動(dòng)畫,使用如下方法:
- (CAAnimation *)animationForKey:(NSString *)key;
但并不支持在動(dòng)畫運(yùn)行過(guò)程中修改動(dòng)畫,所以這個(gè)方法主要用來(lái)檢測(cè)動(dòng)畫的屬性,或者判斷它是否被添加到當(dāng)前圖層中。
為了終止一個(gè)指定的動(dòng)畫,你可以用如下方法把它從圖層移除掉:
- (void)removeAnimationForKey:(NSString *)key;
或者移除所有動(dòng)畫:
- (void)removeAllAnimations;
動(dòng)畫一旦被移除,圖層的外觀就立刻更新到當(dāng)前的模型圖層的值。一般說(shuō)來(lái),動(dòng)畫在結(jié)束之后被自動(dòng)移除,除非設(shè)置removedOnCompletion為NO,如果你設(shè)置動(dòng)畫在結(jié)束之后不被自動(dòng)移除,那么當(dāng)它不需要的時(shí)候你要手動(dòng)移除它;否則它會(huì)一直存在于內(nèi)存中,直到圖層被銷毀。
我們來(lái)擴(kuò)展之前旋轉(zhuǎn)飛船的示例,這里添加一個(gè)按鈕來(lái)停止或者啟動(dòng)動(dòng)畫。這一次我們用一個(gè)非nil的值作為動(dòng)畫的鍵,以便之后可以移除它。-animationDidStop:finished:方法中的flag參數(shù)表明了動(dòng)畫是自然結(jié)束還是被打斷,我們可以在控制臺(tái)打印出來(lái)。如果你用停止按鈕來(lái)終止動(dòng)畫,它會(huì)打印NO,如果允許它完成,它會(huì)打印YES。
總結(jié):
這一章中,我們涉及了屬性動(dòng)畫(你可以對(duì)單獨(dú)的圖層屬性動(dòng)畫有更加具體的控制),動(dòng)畫組(把多個(gè)屬性動(dòng)畫組合成一個(gè)獨(dú)立單元)以及過(guò)度(影響整個(gè)圖層,可以用來(lái)對(duì)圖層的任何內(nèi)容做任何類型的動(dòng)畫,包括子圖層的添加和移除)。
在第九章中,我們繼續(xù)學(xué)習(xí)CAMediaTiming協(xié)議,來(lái)看一看Core Animation是怎樣處理逝去的時(shí)間。
第九章:圖層時(shí)間
前言:
在上面兩章中,我們探討了可以用CAAnimation和它的子類實(shí)現(xiàn)的多種圖層動(dòng)畫。動(dòng)畫的發(fā)生是需要持續(xù)一段時(shí)間的,所以計(jì)時(shí)對(duì)整個(gè)概念來(lái)說(shuō)至關(guān)重要。在這一章中,我們來(lái)看看CAMediaTiming,看看Core Animation是如何跟蹤時(shí)間的。
第1篇: CAMediaTiming協(xié)議
CAMediaTiming協(xié)議定義了在一段動(dòng)畫內(nèi)用來(lái)控制逝去時(shí)間的屬性的集合,CALayer和CAAnimation都實(shí)現(xiàn)了這個(gè)協(xié)議,所以時(shí)間可以被任意基于一個(gè)圖層或者一段動(dòng)畫的類控制。
持續(xù)和重復(fù):
duration(CAMediaTiming的屬性之一),duration是一個(gè)CFTimeInterval的類型(類似于NSTimeInterval的一種雙精度浮點(diǎn)類型),對(duì)將要進(jìn)行的動(dòng)畫的一次迭代指定了時(shí)間。
這里的一次迭代是什么意思呢?CAMediaTiming另外還有一個(gè)屬性叫做repeatCount,代表動(dòng)畫重復(fù)的迭代次數(shù)。
創(chuàng)建重復(fù)動(dòng)畫的另一種方式是使用repeatDuration屬性,它讓動(dòng)畫重復(fù)一個(gè)指定的時(shí)間,而不是指定次數(shù)。你甚至設(shè)置一個(gè)叫做autoreverses的屬性(BOOL類型)在每次間隔交替循環(huán)過(guò)程中自動(dòng)回放。這對(duì)于播放一段連續(xù)非循環(huán)的動(dòng)畫很有用,例如打開(kāi)一扇門,然后關(guān)上它
把repeatDuration設(shè)置為INFINITY,于是動(dòng)畫無(wú)限循環(huán)播放,設(shè)置repeatCount為INFINITY也有同樣的效果。注意repeatCount和repeatDuration可能會(huì)相互沖突,所以你只要對(duì)其中一個(gè)指定非零值。對(duì)兩個(gè)屬性都設(shè)置非0值的行為沒(méi)有被定義。
相對(duì)時(shí)間:
每次討論到Core Animation,時(shí)間都是相對(duì)的,每個(gè)動(dòng)畫都有它自己描述的時(shí)間,可以獨(dú)立地加速,延時(shí)或者偏移。
beginTime指定了動(dòng)畫開(kāi)始之前的的延遲時(shí)間。這里的延遲從動(dòng)畫添加到可見(jiàn)圖層的那一刻開(kāi)始測(cè)量,默認(rèn)是0(就是說(shuō)動(dòng)畫會(huì)立刻執(zhí)行)。
speed是一個(gè)時(shí)間的倍數(shù),默認(rèn)1.0,減少它會(huì)減慢圖層/動(dòng)畫的時(shí)間,增加它會(huì)加快速度。如果2.0的速度,那么對(duì)于一個(gè)duration為1的動(dòng)畫,實(shí)際上在0.5秒的時(shí)候就已經(jīng)完成了。
timeOffset和beginTime類似,但是和增加beginTime導(dǎo)致的延遲動(dòng)畫不同,增加timeOffset只是讓動(dòng)畫快進(jìn)到某一點(diǎn),例如,對(duì)于一個(gè)持續(xù)1秒的動(dòng)畫來(lái)說(shuō),設(shè)置timeOffset為0.5意味著動(dòng)畫將從一半的地方開(kāi)始。
和beginTime不同的是,timeOffset并不受speed的影響。所以如果你把speed設(shè)為2.0,把timeOffset設(shè)置為0.5,那么你的動(dòng)畫將從動(dòng)畫最后結(jié)束的地方開(kāi)始,因?yàn)?秒的動(dòng)畫實(shí)際上被縮短到了0.5秒。然而即使使用了timeOffset讓動(dòng)畫從結(jié)束的地方開(kāi)始,它仍然播放了一個(gè)完整的時(shí)長(zhǎng),這個(gè)動(dòng)畫僅僅是循環(huán)了一圈,然后從頭開(kāi)始播放。
fillMode:
對(duì)于beginTime非0的一段動(dòng)畫來(lái)說(shuō),會(huì)出現(xiàn)一個(gè)當(dāng)動(dòng)畫添加到圖層上但什么也沒(méi)發(fā)生的狀態(tài)。類似的,removeOnCompletion被設(shè)置為NO的動(dòng)畫將會(huì)在動(dòng)畫結(jié)束的時(shí)候仍然保持之前的狀態(tài)。這就產(chǎn)生了一個(gè)問(wèn)題,當(dāng)動(dòng)畫開(kāi)始之前和動(dòng)畫結(jié)束之后,被設(shè)置動(dòng)畫的屬性將會(huì)是什么值呢?
一種可能是屬性和動(dòng)畫沒(méi)被添加之前保持一致,也就是在模型圖層定義的值(見(jiàn)第七章“隱式動(dòng)畫”,模型圖層和呈現(xiàn)圖層的解釋)。
另一種可能是保持動(dòng)畫開(kāi)始之前那一幀,或者動(dòng)畫結(jié)束之后的那一幀。這就是所謂的填充,因?yàn)閯?dòng)畫開(kāi)始和結(jié)束的值用來(lái)填充開(kāi)始之前和結(jié)束之后的時(shí)間。
這種行為就交給開(kāi)發(fā)者了,它可以被CAMediaTiming的fillMode來(lái)控制。fillMode是一個(gè)NSString類型,可以接受如下四種常量:
- kCAFillModeForwards
- kCAFillModeBackwards
- kCAFillModeBoth
- kCAFillModeRemoved
默認(rèn)是kCAFillModeRemoved,當(dāng)動(dòng)畫不再播放的時(shí)候就顯示圖層模型指定的值剩下的三種類型向前,向后或者即向前又向后去填充動(dòng)畫狀態(tài),使得動(dòng)畫在開(kāi)始前或者結(jié)束后仍然保持開(kāi)始和結(jié)束那一刻的值。
這就對(duì)避免在動(dòng)畫結(jié)束的時(shí)候急速返回提供另一種方案(見(jiàn)第八章)。但是記住了,當(dāng)用它來(lái)解決這個(gè)問(wèn)題的時(shí)候,需要把removeOnCompletion設(shè)置為NO,另外需要給動(dòng)畫添加一個(gè)非空的鍵,于是可以在不需要?jiǎng)赢嫷臅r(shí)候把它從圖層上移除。
第2篇: 層級(jí)關(guān)系時(shí)間(書(shū)中無(wú))
在第三章“圖層幾何學(xué)”中,你已經(jīng)了解到每個(gè)圖層是如何相對(duì)在圖層樹(shù)中的父圖層定義 它的坐標(biāo)系的。動(dòng)畫時(shí)間和它類似,每個(gè)動(dòng)畫和圖層在時(shí)間上都有它自己的層級(jí)概念,相對(duì)于它的父親來(lái)測(cè)量。對(duì)圖層調(diào)整時(shí)間將會(huì)影響到它本身和子圖層的動(dòng)畫, 但不會(huì)影響到父圖層。另一個(gè)相似點(diǎn)是所有的動(dòng)畫都被按照層級(jí)組合(使用CAAnimationGroup實(shí)例)。
對(duì)CALayer或者 CAGroupAnimation調(diào)整duration和repeatCount/repeatDuration屬性并不會(huì)影響到子動(dòng)畫。但是 beginTime,timeOffset和speed屬性將會(huì)影響到子動(dòng)畫。然而在層級(jí)關(guān)系中,beginTime指定了父圖層開(kāi)始動(dòng)畫(或者組合關(guān)系 中的父動(dòng)畫)和對(duì)象將要開(kāi)始自己動(dòng)畫之間的偏移。類似的,調(diào)整CALayer和CAGroupAnimation的speed屬性將會(huì)對(duì)動(dòng)畫以及子動(dòng)畫速 度應(yīng)用一個(gè)縮放的因子。
全局時(shí)間和本地時(shí)間
CoreAnimation 有一個(gè)全局時(shí)間的概念,也就是所謂的馬赫時(shí)間(“馬赫”實(shí)際上是iOS和Mac OS系統(tǒng)內(nèi)核的命名)。馬赫時(shí)間在設(shè)備上所有進(jìn)程都是全局的--但是在不同設(shè)備上并不是全局的--不過(guò)這已經(jīng)足夠?qū)?dòng)畫的參考點(diǎn)提供便利了,你可以使用 CACurrentMediaTime函數(shù)來(lái)訪問(wèn)馬赫時(shí)間:
CFTimeInterval time = CACurrentMediaTime();
這個(gè)函數(shù)返回的值其實(shí)無(wú)關(guān)緊要(它返回了設(shè)備自從上次啟動(dòng)后的秒數(shù),并不是你所關(guān)心的),它真實(shí)的作用在于對(duì)動(dòng)畫的時(shí)間測(cè)量提供了一個(gè)相對(duì)值。注意當(dāng)設(shè)備休眠的時(shí)候馬赫時(shí)間會(huì)暫停,也就是所有的CAAnimations(基于馬赫時(shí)間)同樣也會(huì)暫停。
因此馬赫時(shí)間對(duì)長(zhǎng)時(shí)間測(cè)量并不有用。比如用CACurrentMediaTime去更新一個(gè)實(shí)時(shí)鬧鐘并不明智。(可以用[NSDate date]代替,就像第三章例子所示)。
每 個(gè)CALayer和CAAnimation實(shí)例都有自己本地時(shí)間的概念,是根據(jù)父圖層/動(dòng)畫層級(jí)關(guān)系中的beginTime,timeOffset和 speed屬性計(jì)算。就和轉(zhuǎn)換不同圖層之間坐標(biāo)關(guān)系一樣,CALayer同樣也提供了方法來(lái)轉(zhuǎn)換不同圖層之間的本地時(shí)間。如下:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
當(dāng)用來(lái)同步不同圖層之間有不同的speed,timeOffset和beginTime的動(dòng)畫,這些方法會(huì)很有用。
暫停,倒回和快進(jìn)
設(shè) 置動(dòng)畫的speed屬性為0可以暫停動(dòng)畫,但在動(dòng)畫被添加到圖層之后不太可能再修改它了,所以不能對(duì)正在進(jìn)行的動(dòng)畫使用這個(gè)屬性。給圖層添加一個(gè) CAAnimation實(shí)際上是給動(dòng)畫對(duì)象做了一個(gè)不可改變的拷貝,所以對(duì)原始動(dòng)畫對(duì)象屬性的改變對(duì)真實(shí)的動(dòng)畫并沒(méi)有作用。相反,直接用 -animationForKey:來(lái)檢索圖層正在進(jìn)行的動(dòng)畫可以返回正確的動(dòng)畫對(duì)象,但是修改它的屬性將會(huì)拋出異常。
如果移除圖層正在進(jìn)行的動(dòng)畫,圖層將會(huì)急速返回動(dòng)畫之前的狀態(tài)。但如果在動(dòng)畫移除之前拷貝呈現(xiàn)圖層到模型圖層,動(dòng)畫將會(huì)看起來(lái)暫停在那里。但是不好的地方在于之后就不能再恢復(fù)動(dòng)畫了。
一個(gè)簡(jiǎn)單的方法是可以利用CAMediaTiming來(lái)暫停圖層本身。如果把圖層的speed設(shè)置成0,它會(huì)暫停任何添加到圖層上的動(dòng)畫。類似的,設(shè)置speed大于1.0將會(huì)快進(jìn),設(shè)置成一個(gè)負(fù)值將會(huì)倒回動(dòng)畫。
通過(guò)增加主窗口圖層的speed,可以暫停整個(gè)應(yīng)用程序的動(dòng)畫。這對(duì)UI自動(dòng)化提供了好處,我們可以加速所有的視圖動(dòng)畫來(lái)進(jìn)行自動(dòng)化測(cè)試(注意對(duì)于在主窗口之外的視圖并不會(huì)被影響,比如UIAlertview)。可以在app delegate設(shè)置如下進(jìn)行驗(yàn)證:
self.window.layer.speed = 100;
你也可以通過(guò)這種方式來(lái)減速,但其實(shí)也可以在模擬器通過(guò)切換慢速動(dòng)畫來(lái)實(shí)現(xiàn)。
第3篇:手動(dòng)動(dòng)畫
timeOffset一個(gè)很有用的功能在于你可以它可以讓你手動(dòng)控制動(dòng)畫進(jìn)程,通過(guò)設(shè)置speed為0,可以禁用動(dòng)畫的自動(dòng)播放,然后來(lái)使用timeOffset來(lái)來(lái)回顯示動(dòng)畫序列。這可以使得運(yùn)用手勢(shì)來(lái)手動(dòng)控制動(dòng)畫變得很簡(jiǎn)單。
舉個(gè)簡(jiǎn)單的例子:還是之前關(guān)門的動(dòng)畫,修改代碼來(lái)用手勢(shì)控制動(dòng)畫。我們給視圖添加一個(gè)UIPanGestureRecognizer,然后用timeOffset左右搖晃。
因?yàn)樵趧?dòng)畫添加到圖層之后不能再做修改了,我們來(lái)通過(guò)調(diào)整layer的timeOffset達(dá)到同樣的效果。
通過(guò)觸摸手勢(shì)手動(dòng)控制動(dòng)畫
@interface ViewController ()
@property (nonatomic, weak) UIView *containerView;
@property (nonatomic, strong) CALayer *doorLayer;
@end
@implementation ViewController
- (void)viewDidLoad {
[super viewDidLoad];
//add the door
self.doorLayer = [CALayer layer];
self.doorLayer.frame = CGRectMake(0, 0, 128, 256);
self.doorLayer.position = CGPointMake(150 - 64, 150);
self.doorLayer.anchorPoint = CGPointMake(0, 0.5);
self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage;
[self.containerView.layer addSublayer:self.doorLayer];
//apply perspective transform
CATransform3D perspective = CATransform3DIdentity;
perspective.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = perspective;
//add pan gesture recognizer to handle swipes
UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init];
[pan addTarget:self action:@selector(pan:)];
[self.view addGestureRecognizer:pan];
//pause all layer animations
self.doorLayer.speed = 0.0;
//apply swinging animation (which won't play because layer is paused)
CABasicAnimation *animation = [CABasicAnimation animation];
animation.keyPath = @"transform.rotation.y";
animation.toValue = @(-M_PI_2);
animation.duration = 1.0;
[self.doorLayer addAnimation:animation forKey:nil];
}
- (void)pan:(UIPanGestureRecognizer *)pan {
//get horizontal component of pan gesture
CGFloat x = [pan translationInView:self.view].x;
//convert from points to animation duration //using a reasonable scale factor
x /= 200.0f;
//update timeOffset and clamp result
CFTimeInterval timeOffset = self.doorLayer.timeOffset;
timeOffset = MIN(0.999, MAX(0.0, timeOffset - x));
self.doorLayer.timeOffset = timeOffset;
//reset pan gesture
[pan setTranslation:CGPointZero inView:self.view];
}
@end
這其實(shí)是個(gè)小詭計(jì),也許相對(duì)于設(shè)置個(gè)動(dòng)畫然后每次顯示一幀而言,用移動(dòng)手勢(shì)來(lái)直接設(shè)置門的transform會(huì)更簡(jiǎn)單。
在這個(gè)例子中的確是這樣,但是對(duì)于比如說(shuō)關(guān)鍵這這樣更加復(fù)雜的情況,或者有多個(gè)圖層的動(dòng)畫組,相對(duì)于實(shí)時(shí)計(jì)算每個(gè)圖層的屬性而言,這就顯得方便的多了。
總結(jié):
在這一章,我們了解了CAMediaTiming協(xié)議,以及Core Animation用來(lái)操作時(shí)間控制動(dòng)畫的機(jī)制。在下一章,我們將要接觸緩沖,另一個(gè)用來(lái)使動(dòng)畫更加真實(shí)的操作時(shí)間的技術(shù)。
第十章:緩沖
前言:
在第九章“圖層時(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)畫控制和自定義緩沖曲線。
第1篇:動(dòng)畫速度
動(dòng)畫實(shí)際上就是一段時(shí)間內(nèi)的變化,這就暗示了變化一定是隨著某個(gè)特定的速率進(jìn)行。速率由以下公式計(jì)算而來(lái):
velocity = change / time
上面的等式假設(shè)了速度在整個(gè)動(dòng)畫過(guò)程中都是恒定不變的(就如同第八章“顯式動(dòng)畫”的情況),對(duì)于這種恒定速度的動(dòng)畫我們稱之為“線性步調(diào)”,而且從技術(shù)的角度而言這也是實(shí)現(xiàn)動(dòng)畫最簡(jiǎn)單的方式,但也是完全不真實(shí)的一種效果。
我們?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
這里傳入如下幾個(gè)常量之一:
- kCAMediaTimingFunctionLinear
- kCAMediaTimingFunctionEaseIn
- kCAMediaTimingFunctionEaseOut
- kCAMediaTimingFunctionEaseInEaseOut
- kCAMediaTimingFunctionDefault
kCAMediaTimingFunctionLinear選項(xiàng)創(chuàng)建了一個(gè)線性的計(jì)時(shí)函數(shù),同樣也是CAAnimation的timingFunction屬性為空時(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è)全速開(kāi)始,然后慢慢減速停止。它有一個(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ū)別很難察覺(jué),可能是蘋果覺(jué)得它對(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í)方法)。
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)值(這里沒(méi)有kCAMediaTimingFunctionDefault相對(duì)應(yīng)的值了)。
//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];
緩沖和關(guān)鍵幀動(dòng)畫
或許你會(huì)回想起第八章里面顏色切換的關(guān)鍵幀動(dòng)畫由于線性變換的原因(見(jiàn)清單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ù)組就可以了。
運(yùn)行更新后的代碼,你會(huì)發(fā)現(xiàn)動(dòng)畫看起來(lái)更加自然了。
對(duì)CAKeyframeAnimation使用CAMediaTimingFunction
- (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];
}
第2篇:自定義緩沖函數(shù)
在第八章中,我們給時(shí)鐘項(xiàng)目添加了動(dòng)畫。看起來(lái)很贊,但是如果有合適的緩沖函數(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ǔ)法,并沒(méi)有包含具體每個(gè)參數(shù)的名稱,這在objective-C中是合法的,但是卻違反了蘋果對(duì)方法命名的指導(dǎo)方針,而且看起來(lái)是一個(gè)奇怪的設(shè)計(jì))。
使用這個(gè)方法,我們可以創(chuàng)建一個(gè)自定義的緩沖函數(shù)。
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è)類似于CGPoint,CGColorRef或者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á)到很好的效果)。
總結(jié):
在這一章中,我們了解了有關(guān)緩沖和CAMediaTimingFunction類,它可以允許我們創(chuàng)建自定義的緩沖函數(shù)來(lái)完善我們的動(dòng)畫,同樣了解了如何用CAKeyframeAnimation來(lái)避開(kāi)CAMediaTimingFunction的限制,創(chuàng)建完全自定義的緩沖函數(shù)。
在下一章中,我們將要研究基于定時(shí)器的動(dòng)畫--另一個(gè)給我們對(duì)動(dòng)畫更多控制的選擇,并且實(shí)現(xiàn)對(duì)動(dòng)畫的實(shí)時(shí)操縱。