復(fù)雜的組織都是專門化的
Catharine R. Stimpson
到目前為止,我們已經(jīng)探討過CALayer
類了,同時(shí)我們也了解到了一些非常有用的繪圖和動(dòng)畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會(huì)學(xué)習(xí)其他的一些圖層類,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力。
在第四章『視覺效果』我們學(xué)習(xí)到了不使用圖片的情況下用CGPath
去構(gòu)造任意形狀的陰影。如果我們能用同樣的方式創(chuàng)建相同形狀的圖層就好了。
CAShapeLayer
是一個(gè)通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath
來定義想要繪制的圖形,最后CAShapeLayer
就自動(dòng)渲染出來了。當(dāng)然,你也可以用Core Graphics直接向原始的CALyer
的內(nèi)容中繪制一個(gè)路徑,相比直下,使用CAShapeLayer
有以下一些優(yōu)點(diǎn):
CAShapeLayer
使用了硬件加速,繪制同一圖形會(huì)比用Core Graphics快很多。CAShapeLayer
不需要像普通CALayer
一樣創(chuàng)建一個(gè)寄宿圖形,所以無論有多大,都不會(huì)占用太多的內(nèi)存。CAShapeLayer
可以在邊界之外繪制。你的圖層路徑不會(huì)像在使用Core Graphics的普通CALayer
一樣被剪裁掉(如我們?cè)诘诙滤姡?/li>CAShapeLayer
做3D變換時(shí),它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化。CGPath
CAShapeLayer
可以用來繪制所有能夠通過CGPath
來表示的形狀。這個(gè)形狀不一定要閉合,圖層路徑也不一定要不可破,事實(shí)上你可以在一個(gè)圖層上繪制好幾個(gè)不同的形狀。你可以控制一些屬性比如lineWith
(線寬,用點(diǎn)表示單位),lineCap
(線條結(jié)尾的樣子),和lineJoin
(線條之間的結(jié)合點(diǎn)的樣子);但是在圖層層面你只有一次機(jī)會(huì)設(shè)置這些屬性。如果你想用不同顏色或風(fēng)格來繪制多個(gè)形狀,就不得不為每個(gè)形狀準(zhǔn)備一個(gè)圖層了。
清單6.1 的代碼用一個(gè)CAShapeLayer
渲染一個(gè)簡(jiǎn)單的火柴人。CAShapeLayer
屬性是CGPathRef
類型,但是我們用UIBezierPath
幫助類創(chuàng)建了圖層路徑,這樣我們就不用考慮人工釋放CGPath
了。圖6.1是代碼運(yùn)行的結(jié)果。雖然還不是很完美,但是總算知道了大意對(duì)吧!
清單6.1 用CAShapeLayer
繪制一個(gè)火柴人
#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create path
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];
}
@end
圖6.1 用CAShapeLayer
繪制一個(gè)簡(jiǎn)單的火柴人
第二章里面提到了CAShapeLayer
為創(chuàng)建圓角視圖提供了一個(gè)方法,就是CALayer
的cornerRadius
屬性(譯者注:其實(shí)是在第四章提到的)。雖然使用CAShapeLayer
類需要更多的工作,但是它有一個(gè)優(yōu)勢(shì)就是可以單獨(dú)指定每個(gè)角。
我們創(chuàng)建圓角矩形其實(shí)就是人工繪制單獨(dú)的直線和弧度,但是事實(shí)上UIBezierPath
有自動(dòng)繪制圓角矩形的構(gòu)造方法,下面這段代碼繪制了一個(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];
我們可以通過這個(gè)圖層路徑繪制一個(gè)既有直角又有圓角的視圖。如果我們想依照此圖形來剪裁視圖內(nèi)容,我們可以把CAShapeLayer
作為視圖的宿主圖層,而不是添加一個(gè)子視圖(圖層蒙板的詳細(xì)解釋見第四章『視覺效果』)。
用戶界面是無法從一個(gè)單獨(dú)的圖片里面構(gòu)建的。一個(gè)設(shè)計(jì)良好的圖標(biāo)能夠很好地表現(xiàn)一個(gè)按鈕或控件的意圖,不過你遲早都要需要一個(gè)不錯(cuò)的老式風(fēng)格的文本標(biāo)簽。
如果你想在一個(gè)圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內(nèi)容(這就是UILabel的精髓)。如果越過寄宿于圖層的視圖,直接在圖層上操作,那其實(shí)相當(dāng)繁瑣。你要為每一個(gè)顯示文字的圖層創(chuàng)建一個(gè)能像圖層代理一樣工作的類,還要邏輯上判斷哪個(gè)圖層需要顯示哪個(gè)字符串,更別提還要記錄不同的字體,顏色等一系列亂七八糟的東西。
萬幸的是這些都是不必要的,Core Animation提供了一個(gè)CALayer
的子類CATextLayer
,它以圖層的形式包含了UILabel
幾乎所有的繪制特性,并且額外提供了一些新的特性。
同樣,CATextLayer
也要比UILabel
渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel
其實(shí)是通過WebKit來實(shí)現(xiàn)繪制的,這樣就造成了當(dāng)有很多文字的時(shí)候就會(huì)有極大的性能壓力。而CATextLayer
使用了Core text,并且渲染得非???。
讓我們來嘗試用CATextLayer
來顯示一些文字。清單6.2的代碼實(shí)現(xiàn)了這一功能,結(jié)果如圖6.2所示。
清單6.2 用CATextLayer
來實(shí)現(xiàn)一個(gè)UILabel
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//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;
}
@end
圖6.2 用CATextLayer
來顯示一個(gè)純文本標(biāo)簽
如果你仔細(xì)看這個(gè)文本,你會(huì)發(fā)現(xiàn)一個(gè)奇怪的地方:這些文本有一些像素化了。這是因?yàn)椴]有以Retina的方式渲染,第二章提到了這個(gè)contentScale
屬性,用來決定圖層內(nèi)容應(yīng)該以怎樣的分辨率來渲染。contentsScale
并不關(guān)心屏幕的拉伸因素而總是默認(rèn)為1.0。如果我們想以Retina的質(zhì)量來顯示文字,我們就得手動(dòng)地設(shè)置CATextLayer
的contentsScale
屬性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
這樣就解決了這個(gè)問題(如圖6.3)
圖6.3 設(shè)置contentsScale
來匹配屏幕
CATextLayer
的font
屬性不是一個(gè)UIFont
類型,而是一個(gè)CFTypeRef
類型。這樣可以根據(jù)你的具體需要來決定字體屬性應(yīng)該是用CGFontRef
類型還是CTFontRef
類型(Core Text字體)。同時(shí)字體大小也是用fontSize
屬性單獨(dú)設(shè)置的,因?yàn)?code>CTFontRef和CGFontRef
并不像UIFont一樣包含點(diǎn)大小。這個(gè)例子會(huì)告訴你如何將UIFont
轉(zhuǎn)換成CGFontRef
。
另外,CATextLayer
的string
屬性并不是你想象的NSString
類型,而是id
類型。這樣你既可以用NSString
也可以用NSAttributedString
來指定文本了(注意,NSAttributedString
并不是NSString
的子類)。屬性化字符串是iOS用來渲染字體風(fēng)格的機(jī)制,它以特定的方式來決定指定范圍內(nèi)的字符串的原始信息,比如字體,顏色,字重,斜體等。
iOS 6中,Apple給UILabel
和其他UIKit文本視圖添加了直接的屬性化字符串的支持,應(yīng)該說這是一個(gè)很方便的特性。不過事實(shí)上從iOS3.2開始CATextLayer
就已經(jīng)支持屬性化字符串了。這樣的話,如果你想要支持更低版本的iOS系統(tǒng),CATextLayer
無疑是你向界面中增加富文本的好辦法,而且也不用去跟復(fù)雜的Core Text打交道,也省了用UIWebView
的麻煩。
讓我們編輯一下示例使用到NSAttributedString
(見清單6.3).iOS 6及以上我們可以用新的NSTextAttributeName
實(shí)例來設(shè)置我們的字符串屬性,但是練習(xí)的目的是為了演示在iOS 5及以下,所以我們用了Core Text,也就是說你需要把Core Text framework添加到你的項(xiàng)目中。否則,編譯器是無法識(shí)別屬性常量的。
圖6.4是代碼運(yùn)行結(jié)果(注意那個(gè)紅色的下劃線文本)
清單6.3 用NSAttributedString實(shí)現(xiàn)一個(gè)富文本標(biāo)簽。
#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>
#import <CoreText/CoreText.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
textLayer.contentsScale = [UIScreen mainScreen].scale;
[self.labelView.layer addSublayer:textLayer];
//set text attributes
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
//choose a font
UIFont *font = [UIFont systemFontOfSize:15];
//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";
?
//create attributed string
NSMutableAttributedString *string = nil;
string = [[NSMutableAttributedString alloc] initWithString:text];
//convert UIFont to a CTFont
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFloat fontSize = font.pointSize;
CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);
//set text attributes
NSDictionary *attribs = @{
(__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
(__bridge id)kCTFontAttributeName: (__bridge id)fontRef
};
[string setAttributes:attribs range:NSMakeRange(0, [text length])];
attribs = @{
(__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
(__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
(__bridge id)kCTFontAttributeName: (__bridge id)fontRef
};
[string setAttributes:attribs range:NSMakeRange(6, 5)];
//release the CTFont we created earlier
CFRelease(fontRef);
//set layer text
textLayer.string = string;
}
@end
圖6.4 用CATextLayer實(shí)現(xiàn)一個(gè)富文本標(biāo)簽。
有必要提一下的是,由于繪制的實(shí)現(xiàn)機(jī)制不同(Core Text和WebKit),用CATextLayer
渲染和用UILabel
渲染出的文本行距和字距也不是不盡相同的。
二者的差異程度(由使用的字體和字符決定)總的來說挺小,但是如果你想正確的顯示普通便簽和CATextLayer
就一定要記住這一點(diǎn)。
UILabel
的替代品我們已經(jīng)證實(shí)了CATextLayer
比UILabel
有著更好的性能表現(xiàn),同時(shí)還有額外的布局選項(xiàng)并且在iOS 5上支持富文本。但是與一般的標(biāo)簽比較而言會(huì)更加繁瑣一些。如果我們真的在需求一個(gè)UILabel
的可用替代品,最好是能夠在Interface Builder上創(chuàng)建我們的標(biāo)簽,而且盡可能地像一般的視圖一樣正常工作。
我們應(yīng)該繼承UILabel
,然后添加一個(gè)子圖層CATextLayer
并重寫顯示文本的方法。但是仍然會(huì)有由UILabel
的-drawRect:
方法創(chuàng)建的空寄宿圖。而且由于CALayer
不支持自動(dòng)縮放和自動(dòng)布局,子視圖并不是主動(dòng)跟蹤視圖邊界的大小,所以每次視圖大小被更改,我們不得不手動(dòng)更新子圖層的邊界。
我們真正想要的是一個(gè)用CATextLayer
作為宿主圖層的UILabel
子類,這樣就可以隨著視圖自動(dòng)調(diào)整大小而且也沒有冗余的寄宿圖啦。
就像我們?cè)诘谝徽隆簣D層樹』討論的一樣,每一個(gè)UIView
都是寄宿在一個(gè)CALayer
的示例上。這個(gè)圖層是由視圖自動(dòng)創(chuàng)建和管理的,那我們可以用別的圖層類型替代它么?一旦被創(chuàng)建,我們就無法代替這個(gè)圖層了。但是如果我們繼承了UIView
,那我們就可以重寫+layerClass
方法使得在創(chuàng)建的時(shí)候能返回一個(gè)不同的圖層子類。UIView
會(huì)在初始化的時(shí)候調(diào)用+layerClass
方法,然后用它的返回類型來創(chuàng)建宿主圖層。
清單6.4 演示了一個(gè)UILabel
子類LayerLabel
用CATextLayer
繪制它的問題,而不是調(diào)用一般的UILabel
使用的較慢的-drawRect:
方法。LayerLabel
示例既可以用代碼實(shí)現(xiàn),也可以在Interface Builder實(shí)現(xiàn),只要把普通的標(biāo)簽拖入視圖之中,然后設(shè)置它的類是LayerLabel就可以了。
清單6.4 使用CATextLayer
的UILabel
子類:LayerLabel
#import "LayerLabel.h"
#import <QuartzCore/QuartzCore.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)文本并沒有像素化,而我們也沒有設(shè)置contentsScale
屬性。把CATextLayer
作為宿主圖層的另一好處就是視圖自動(dòng)設(shè)置了contentsScale
屬性。
在這個(gè)簡(jiǎn)單的例子中,我們只是實(shí)現(xiàn)了UILabel
的一部分風(fēng)格和布局屬性,不過稍微再改進(jìn)一下我們就可以創(chuàng)建一個(gè)支持UILabel
所有功能甚至更多功能的LayerLabel
類(你可以在一些線上的開源項(xiàng)目中找到)。
如果你打算支持iOS 6及以上,基于CATextLayer
的標(biāo)簽可能就有有些局限性。但是總得來說,如果想在app里面充分利用CALayer
子類,用+layerClass
來創(chuàng)建基于不同圖層的視圖是一個(gè)簡(jiǎn)單可復(fù)用的方法。
當(dāng)我們?cè)跇?gòu)造復(fù)雜的3D事物的時(shí)候,如果能夠組織獨(dú)立元素就太方便了。比如說,你想創(chuàng)造一個(gè)孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。
當(dāng)然是允許獨(dú)立地移動(dòng)每個(gè)區(qū)域的啦。以肘為指點(diǎn)會(huì)移動(dòng)前臂和手,而不是肩膀。Core Animation圖層很容易就可以讓你在2D環(huán)境下做出這樣的層級(jí)體系下的變換,但是3D情況下就不太可能,因?yàn)樗械膱D層都把他的孩子都平面化到一個(gè)場(chǎng)景中(第五章『變換』有提到)。
CATransformLayer
解決了這個(gè)問題,CATransformLayer
不同于普通的CALayer
,因?yàn)樗荒茱@示它自己的內(nèi)容。只有當(dāng)存在了一個(gè)能作用域子圖層的變換它才真正存在。CATransformLayer
并不平面化它的子圖層,所以它能夠用于構(gòu)造一個(gè)層級(jí)的3D結(jié)構(gòu),比如我的手臂示例。
用代碼創(chuàng)建一個(gè)手臂需要相當(dāng)多的代碼,所以我就演示得更簡(jiǎn)單一些吧:在第五章的立方體示例,我們將通過旋轉(zhuǎn)camara
來解決圖層平面化問題而不是像立方體示例代碼中用的sublayerTransform
。這是一個(gè)非常不錯(cuò)的技巧,但是只能作用域單個(gè)對(duì)象上,如果你的場(chǎng)景包含兩個(gè)立方體,那我們就不能用這個(gè)技巧單獨(dú)旋轉(zhuǎn)他們了。
那么,就讓我們來試一試CATransformLayer
吧,第一個(gè)問題就來了:在第五章,我們是用多個(gè)視圖來構(gòu)造了我們的立方體,而不是單獨(dú)的圖層。我們不能在不打亂已有的視圖層次的前提下在一個(gè)本身不是有寄宿圖的圖層中放置一個(gè)寄宿圖圖層。我們可以創(chuàng)建一個(gè)新的UIView
子類寄宿在CATransformLayer
(用+layerClass
方法)之上。但是,為了簡(jiǎn)化案例,我們僅僅重建了一個(gè)單獨(dú)的圖層,而不是使用視圖。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標(biāo)簽,不過我們現(xiàn)在也用不到這個(gè)特性。
清單6.5就是代碼。我們以我們?cè)诘谖逭率褂眠^的相同基本邏輯放置立方體。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個(gè)CATransformLayer
中創(chuàng)建一個(gè)獨(dú)立的立方體對(duì)象,然后將兩個(gè)這樣的立方體放進(jìn)容器中。我們隨機(jī)地給立方面染色以將他們區(qū)分開來,這樣就不用靠標(biāo)簽或是光亮來區(qū)分他們。圖6.5是運(yùn)行結(jié)果。
清單6.5 用CATransformLayer
裝配一個(gè)3D圖層體系
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (CALayer *)faceWithTransform:(CATransform3D)transform
{
//create cube face layer
CALayer *face = [CALayer layer];
face.frame = CGRectMake(-50, -50, 100, 100);
//apply a random color
CGFloat red = (rand() / (double)INT_MAX);
CGFloat green = (rand() / (double)INT_MAX);
CGFloat blue = (rand() / (double)INT_MAX);
face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;
?//apply the transform and return
face.transform = transform;
return face;
}
- (CALayer *)cubeWithTransform:(CATransform3D)transform
{
//create cube layer
CATransformLayer *cube = [CATransformLayer layer];
//add cube face 1
CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 2
ct = CATransform3DMakeTranslation(50, 0, 0);
ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 3
ct = CATransform3DMakeTranslation(0, -50, 0);
ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 4
ct = CATransform3DMakeTranslation(0, 50, 0);
ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 5
ct = CATransform3DMakeTranslation(-50, 0, 0);
ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//add cube face 6
ct = CATransform3DMakeTranslation(0, 0, -50);
ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
[cube addSublayer:[self faceWithTransform:ct]];
//center the cube layer within the container
CGSize containerSize = self.containerView.bounds.size;
cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);
//apply the transform and return
cube.transform = transform;
return cube;
}
- (void)viewDidLoad
{?
[super viewDidLoad];
//set up the perspective transform
CATransform3D pt = CATransform3DIdentity;
pt.m34 = -1.0 / 500.0;
self.containerView.layer.sublayerTransform = pt;
//set up the transform for cube 1 and add it
CATransform3D c1t = CATransform3DIdentity;
c1t = CATransform3DTranslate(c1t, -100, 0, 0);
CALayer *cube1 = [self cubeWithTransform:c1t];
[self.containerView.layer addSublayer:cube1];
//set up the transform for cube 2 and add it
CATransform3D c2t = CATransform3DIdentity;
c2t = CATransform3DTranslate(c2t, 100, 0, 0);
c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
CALayer *cube2 = [self cubeWithTransform:c2t];
[self.containerView.layer addSublayer:cube2];
}
@end
圖6.5 同一視角下的倆不同變換的立方體
CAGradientLayer
是用來生成兩種或更多顏色平滑漸變的。用Core Graphics復(fù)制一個(gè)CAGradientLayer
并將內(nèi)容繪制到一個(gè)普通圖層的寄宿圖也是有可能的,但是CAGradientLayer
的真正好處在于繪制使用了硬件加速。
我們將從一個(gè)簡(jiǎn)單的紅變藍(lán)的對(duì)角線漸變開始(見清單6.6).這些漸變色彩放在一個(gè)數(shù)組中,并賦給colors
屬性。這個(gè)數(shù)組成員接受CGColorRef
類型的值(并不是從NSObject
派生而來),所以我們要用通過bridge轉(zhuǎn)換以確保編譯正常。
CAGradientLayer
也有startPoint
和endPoint
屬性,他們決定了漸變的方向。這兩個(gè)參數(shù)是以單位坐標(biāo)系進(jìn)行的定義,所以左上角坐標(biāo)是{0, 0},右下角坐標(biāo)是{1, 1}。代碼運(yùn)行結(jié)果如圖6.6
清單6.6 簡(jiǎn)單的兩種顏色的對(duì)角線漸變
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//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);
}
@end
圖6.6 用CAGradientLayer
實(shí)現(xiàn)簡(jiǎn)單的兩種顏色的對(duì)角線漸變
如果你愿意,colors
屬性可以包含很多顏色,所以創(chuàng)建一個(gè)彩虹一樣的多重漸變也是很簡(jiǎn)單的。默認(rèn)情況下,這些顏色在空間上均勻地被渲染,但是我們可以用locations
屬性來調(diào)整空間。locations
屬性是一個(gè)浮點(diǎn)數(shù)值的數(shù)組(以NSNumber
包裝)。這些浮點(diǎn)數(shù)定義了colors
屬性中每個(gè)不同顏色的位置,同樣的,也是以單位坐標(biāo)系進(jìn)行標(biāo)定。0.0代表著漸變的開始,1.0代表著結(jié)束。
locations
數(shù)組并不是強(qiáng)制要求的,但是如果你給它賦值了就一定要確保locations
的數(shù)組大小和colors
數(shù)組大小一定要相同,否則你將會(huì)得到一個(gè)空白的漸變。
清單6.7展示了一個(gè)基于清單6.6的對(duì)角線漸變的代碼改造。現(xiàn)在變成了從紅到黃最后到綠色的漸變。locations
數(shù)組指定了0.0,0.25和0.5三個(gè)數(shù)值,這樣這三個(gè)漸變就有點(diǎn)像擠在了左上角。(如圖6.7).
清單6.7 在漸變上使用locations
- (void)viewDidLoad {
[super viewDidLoad];
//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);
}
圖6.7 用locations
構(gòu)造偏移至左上角的三色漸變
CAReplicatorLayer
的目的是為了高效生成許多相似的圖層。它會(huì)繪制一個(gè)或多個(gè)圖層的子圖層,并在每個(gè)復(fù)制體上應(yīng)用不同的變換??瓷先パ菔灸軌蚋咏忉屵@些,我們來寫個(gè)例子吧。
清單6.8中,我們?cè)谄聊坏闹虚g創(chuàng)建了一個(gè)小白色方塊圖層,然后用CAReplicatorLayer
生成十個(gè)圖層組成一個(gè)圓圈。instanceCount
屬性指定了圖層需要重復(fù)多少次。instanceTransform
指定了一個(gè)CATransform3D
3D變換(這種情況下,下一圖層的位移和旋轉(zhuǎn)將會(huì)移動(dòng)到圓圈的下一個(gè)點(diǎn))。
變換是逐步增加的,每個(gè)實(shí)例都是相對(duì)于前一實(shí)例布局。這就是為什么這些復(fù)制體最終不會(huì)出現(xiàn)在同意位置上,圖6.8是代碼運(yùn)行結(jié)果。
清單6.8 用CAReplicatorLayer
重復(fù)圖層
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a replicator layer and add it to our view
CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
replicator.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:replicator];
//configure the replicator
replicator.instanceCount = 10;
//apply a transform for each instance
CATransform3D transform = CATransform3DIdentity;
transform = CATransform3DTranslate(transform, 0, 200, 0);
transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
transform = CATransform3DTranslate(transform, 0, -200, 0);
replicator.instanceTransform = transform;
//apply a color shift for each instance
replicator.instanceBlueOffset = -0.1;
replicator.instanceGreenOffset = -0.1;
//create a sublayer and place it inside the replicator
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
layer.backgroundColor = [UIColor whiteColor].CGColor;
[replicator addSublayer:layer];
}
@end
圖6.8 用CAReplicatorLayer
創(chuàng)建一圈圖層
注意到當(dāng)圖層在重復(fù)的時(shí)候,他們的顏色也在變化:這是用instanceBlueOffset
和instanceGreenOffset
屬性實(shí)現(xiàn)的。通過逐步減少藍(lán)色和綠色通道,我們逐漸將圖層顏色轉(zhuǎn)換成了紅色。這個(gè)復(fù)制效果看起來很酷,但是CAReplicatorLayer
真正應(yīng)用到實(shí)際程序上的場(chǎng)景比如:一個(gè)游戲中導(dǎo)彈的軌跡云,或者粒子爆炸(盡管iOS 5已經(jīng)引入了CAEmitterLayer
,它更適合創(chuàng)建任意的粒子效果)。除此之外,還有一個(gè)實(shí)際應(yīng)用是:反射。
使用CAReplicatorLayer
并應(yīng)用一個(gè)負(fù)比例變換于一個(gè)復(fù)制圖層,你就可以創(chuàng)建指定視圖(或整個(gè)視圖層次)內(nèi)容的鏡像圖片,這樣就創(chuàng)建了一個(gè)實(shí)時(shí)的『反射』效果。讓我們來嘗試實(shí)現(xiàn)這個(gè)創(chuàng)意:指定一個(gè)繼承于UIView
的ReflectionView
,它會(huì)自動(dòng)產(chǎn)生內(nèi)容的反射效果。實(shí)現(xiàn)這個(gè)效果的代碼很簡(jiǎn)單(見清單6.9),實(shí)際上用ReflectionView
實(shí)現(xiàn)這個(gè)效果會(huì)更簡(jiǎn)單,我們只需要把ReflectionView
的實(shí)例放置于Interface Builder(見圖6.9),它就會(huì)實(shí)時(shí)生成子視圖的反射,而不需要?jiǎng)e的代碼(見圖6.10).
清單6.9 用CAReplicatorLayer
自動(dòng)繪制反射
#import "ReflectionView.h"
#import <QuartzCore/QuartzCore.h>
@implementation ReflectionView
+ (Class)layerClass
{
return [CAReplicatorLayer class];
}
- (void)setUp
{
//configure replicator
CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
layer.instanceCount = 2;
//move reflection instance below original and flip vertically
CATransform3D transform = CATransform3DIdentity;
CGFloat verticalOffset = self.bounds.size.height + 2;
transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
transform = CATransform3DScale(transform, 1, -1, 0);
layer.instanceTransform = transform;
//reduce alpha of reflection layer
layer.instanceAlphaOffset = -0.6;
}
?
- (id)initWithFrame:(CGRect)frame
{
//this is called when view is created in code
if ((self = [super initWithFrame:frame])) {
[self setUp];
}
return self;
}
- (void)awakeFromNib
{
//this is called when view is created from a nib
[self setUp];
}
@end
圖6.9 在Interface Builder中使用ReflectionView
圖6.10 ReflectionView
自動(dòng)實(shí)時(shí)產(chǎn)生反射效果。
開源代碼ReflectionView
完成了一個(gè)自適應(yīng)的漸變淡出效果(用CAGradientLayer
和圖層蒙板實(shí)現(xiàn)),代碼見 https://github.com/nicklockwood/ReflectionView
對(duì)于一個(gè)未轉(zhuǎn)換的圖層,它的bounds
和它的frame
是一樣的,frame
屬性是由bounds
屬性自動(dòng)計(jì)算而出的,所以更改任意一個(gè)值都會(huì)更新其他值。
但是如果你只想顯示一個(gè)大圖層里面的一小部分呢。比如說,你可能有一個(gè)很大的圖片,你希望用戶能夠隨意滑動(dòng),或者是一個(gè)數(shù)據(jù)或文本的長(zhǎng)列表。在一個(gè)典型的iOS應(yīng)用中,你可能會(huì)用到UITableView
或是UIScrollView
,但是對(duì)于獨(dú)立的圖層來說,什么會(huì)等價(jià)于剛剛提到的UITableView
和UIScrollView
呢?
在第二章中,我們探索了圖層的contentsRect
屬性的用法,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個(gè)非常好的解決方案,因?yàn)?,這樣做的話每次你想『滑動(dòng)』可視區(qū)域的時(shí)候,你就需要手工重新計(jì)算并更新所有的子圖層位置。
這個(gè)時(shí)候就需要CAScrollLayer
了。CAScrollLayer
有一個(gè)-scrollToPoint:
方法,它自動(dòng)適應(yīng)bounds
的原點(diǎn)以便圖層內(nèi)容出現(xiàn)在滑動(dòng)的地方。注意,這就是它做的所有事情。前面提到過,Core Animation并不處理用戶輸入,所以CAScrollLayer
并不負(fù)責(zé)將觸摸事件轉(zhuǎn)換為滑動(dòng)事件,既不渲染滾動(dòng)條,也不實(shí)現(xiàn)任何iOS指定行為例如滑動(dòng)反彈(當(dāng)視圖滑動(dòng)超多了它的邊界的將會(huì)反彈回正確的地方)。
讓我們來用CAScrollLayer
來常見一個(gè)基本的UIScrollView
替代品。我們將會(huì)用CAScrollLayer
作為視圖的宿主圖層,并創(chuàng)建一個(gè)自定義的UIView
,然后用UIPanGestureRecognizer
實(shí)現(xiàn)觸摸事件響應(yīng)。這段代碼見清單6.10. 圖6.11是運(yùn)行效果:ScrollView
顯示了一個(gè)大于它的frame
的UIImageView
。
清單6.10 用CAScrollLayer
實(shí)現(xiàn)滑動(dòng)視圖
#import "ScrollView.h"
#import <QuartzCore/QuartzCore.h> @implementation ScrollView
+ (Class)layerClass
{
return [CAScrollLayer class];
}
- (void)setUp
{
//enable clipping
self.layer.masksToBounds = YES;
//attach pan gesture recognizer
UIPanGestureRecognizer *recognizer = nil;
recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
[self addGestureRecognizer:recognizer];
}
- (id)initWithFrame:(CGRect)frame
{
//this is called when view is created in code
if ((self = [super initWithFrame:frame])) {
[self setUp];
}
return self;
}
- (void)awakeFromNib {
//this is called when view is created from a nib
[self setUp];
}
- (void)pan:(UIPanGestureRecognizer *)recognizer
{
//get the offset by subtracting the pan gesture
//translation from the current bounds origin
CGPoint offset = self.bounds.origin;
offset.x -= [recognizer translationInView:self].x;
offset.y -= [recognizer translationInView:self].y;
//scroll the layer
[(CAScrollLayer *)self.layer scrollToPoint:offset];
//reset the pan gesture translation
[recognizer setTranslation:CGPointZero inView:self];
}
@end
圖6.11 用UIScrollView
創(chuàng)建一個(gè)湊合的滑動(dòng)視圖
不同于UIScrollView
,我們定制的滑動(dòng)視圖類并沒有實(shí)現(xiàn)任何形式的邊界檢查(bounds checking)。圖層內(nèi)容極有可能滑出視圖的邊界并無限滑下去。CAScrollLayer
并沒有等同于UIScrollView
中contentSize
的屬性,所以當(dāng)CAScrollLayer
滑動(dòng)的時(shí)候完全沒有一個(gè)全局的可滑動(dòng)區(qū)域的概念,也無法自適應(yīng)它的邊界原點(diǎn)至你指定的值。它之所以不能自適應(yīng)邊界大小是因?yàn)樗恍枰瑑?nèi)容完全可以超過邊界。
那你一定會(huì)奇怪用CAScrollLayer
的意義到底何在,因?yàn)槟憧梢院?jiǎn)單地用一個(gè)普通的CALayer
然后手動(dòng)適應(yīng)邊界原點(diǎn)啊。真相其實(shí)并不復(fù)雜,UIScrollView
并沒有用CAScrollLayer
,事實(shí)上,就是簡(jiǎn)單的通過直接操作圖層邊界來實(shí)現(xiàn)滑動(dòng)。
CAScrollLayer
有一個(gè)潛在的有用特性。如果你查看CAScrollLayer
的頭文件,你就會(huì)注意到有一個(gè)擴(kuò)展分類實(shí)現(xiàn)了一些方法和屬性:
- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;
看到這些方法和屬性名,你也許會(huì)以為這些方法給每個(gè)CALayer
實(shí)例增加了滑動(dòng)功能。但是事實(shí)上他們只是放置在CAScrollLayer
中的圖層的實(shí)用方法。scrollPoint:
方法從圖層樹中查找并找到第一個(gè)可用的CAScrollLayer
,然后滑動(dòng)它使得指定點(diǎn)成為可視的。scrollRectToVisible:
方法實(shí)現(xiàn)了同樣的事情只不過是作用在一個(gè)矩形上的。visibleRect
屬性決定圖層(如果存在的話)的哪部分是當(dāng)前的可視區(qū)域。如果你自己實(shí)現(xiàn)這些方法就會(huì)相對(duì)容易明白一點(diǎn),但是CAScrollLayer
幫你省了這些麻煩,所以當(dāng)涉及到實(shí)現(xiàn)圖層滑動(dòng)的時(shí)候就可以用上了。
有些時(shí)候你可能需要繪制一個(gè)很大的圖片,常見的例子就是一個(gè)高像素的照片或者是地球表面的詳細(xì)地圖。iOS應(yīng)用通暢運(yùn)行在內(nèi)存受限的設(shè)備上,所以讀取整個(gè)圖片到內(nèi)存中是不明智的。載入大圖可能會(huì)相當(dāng)?shù)芈切?duì)你看上去比較方便的做法(在主線程調(diào)用UIImage
的-imageNamed:
方法或者-imageWithContentsOfFile:
方法)將會(huì)阻塞你的用戶界面,至少會(huì)引起動(dòng)畫卡頓現(xiàn)象。
能高效繪制在iOS上的圖片也有一個(gè)大小限制。所有顯示在屏幕上的圖片最終都會(huì)被轉(zhuǎn)化為OpenGL紋理,同時(shí)OpenGL有一個(gè)最大的紋理尺寸(通常是2048*2048,或4096*4096,這個(gè)取決于設(shè)備型號(hào))。如果你想在單個(gè)紋理中顯示一個(gè)比這大的圖,即便圖片已經(jīng)存在于內(nèi)存中了,你仍然會(huì)遇到很大的性能問題,因?yàn)镃ore Animation強(qiáng)制用CPU處理圖片而不是更快的GPU(見第12章『速度的曲調(diào)』,和第13章『高效繪圖』,它更加詳細(xì)地解釋了軟件繪制和硬件繪制)。
CATiledLayer
為載入大圖造成的性能問題提供了一個(gè)解決方案:將大圖分解成小片然后將他們單獨(dú)按需載入。讓我們用實(shí)驗(yàn)來證明一下。
這個(gè)示例中,我們將會(huì)從一個(gè)2048*2048分辨率的雪人圖片入手。為了能夠從CATiledLayer
中獲益,我們需要把這個(gè)圖片裁切成許多小一些的圖片。你可以通過代碼來完成這件事情,但是如果你在運(yùn)行時(shí)讀入整個(gè)圖片并裁切,那CATiledLayer
這些所有的性能優(yōu)點(diǎn)就損失殆盡了。理想情況下來說,最好能夠逐個(gè)步驟來實(shí)現(xiàn)。
清單6.11 演示了一個(gè)簡(jiǎn)單的Mac OS命令行程序,它用CATiledLayer
將一個(gè)圖片裁剪成小圖并存儲(chǔ)到不同的文件中。
清單6.11 裁剪圖片成小圖的終端程序
#import <AppKit/AppKit.h>
int main(int argc, const char * argv[])
{
@autoreleasepool{
?//handle incorrect arguments
if (argc < 2) {
NSLog(@"TileCutter arguments: inputfile");
return 0;
}
//input file
NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
//tile size
CGFloat tileSize = 256; //output path
NSString *outputPath = [inputFile stringByDeletingPathExtension];
//load image
NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];
NSSize size = [image size];
NSArray *representations = [image representations];
if ([representations count]){
NSBitmapImageRep *representation = representations[0];
size.width = [representation pixelsWide];
size.height = [representation pixelsHigh];
}
NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height);
CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];
//calculate rows and columns
NSInteger rows = ceil(size.height / tileSize);
NSInteger cols = ceil(size.width / tileSize);
//generate tiles
for (int y = 0; y < rows; ++y) {
for (int x = 0; x < cols; ++x) {
//extract tile image
CGRect tileRect = CGRectMake(x*tileSize, y*tileSize, tileSize, tileSize);
CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);
//convert to jpeg data
NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];
NSData *data = [imageRep representationUsingType: NSJPEGFileType properties:nil];
CGImageRelease(tileImage);
//save file
NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jpg", x, y];
[data writeToFile:path atomically:NO];
}
}
}
return 0;
}
這個(gè)程序?qū)?048*2048分辨率的雪人圖案裁剪成了64個(gè)不同的256*256的小圖。(256*256是CATiledLayer
的默認(rèn)小圖大小,默認(rèn)大小可以通過tileSize
屬性更改)。程序接受一個(gè)圖片路徑作為命令行的第一個(gè)參數(shù)。我們可以在編譯的scheme將路徑參數(shù)硬編碼然后就可以在Xcode中運(yùn)行了,但是以后作用在另一個(gè)圖片上就不方便了。所以,我們編譯了這個(gè)程序并把它保存到敏感的地方,然后從終端調(diào)用,如下面所示:
> path/to/TileCutterApp path/to/Snowman.jpg
The app is very basic, but could easily be extended to support additional arguments such as tile size, or to export images in formats other than JPEG. The result of running it is a sequence of 64 new images, named as follows:
這個(gè)程序相當(dāng)基礎(chǔ),但是能夠輕易地?cái)U(kuò)展支持額外的參數(shù)比如小圖大小,或者導(dǎo)出格式等等。運(yùn)行結(jié)果是64個(gè)新圖的序列,如下面命名:
Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg
既然我們有了裁切后的小圖,我們就要讓iOS程序用到他們。CATiledLayer
很好地和UIScrollView
集成在一起。除了設(shè)置圖層和滑動(dòng)視圖邊界以適配整個(gè)圖片大小,我們真正要做的就是實(shí)現(xiàn)-drawLayer:inContext:
方法,當(dāng)需要載入新的小圖時(shí),CATiledLayer
就會(huì)調(diào)用到這個(gè)方法。
清單6.12演示了代碼。圖6.12是代碼運(yùn)行結(jié)果。
清單6.12 一個(gè)簡(jiǎn)單的滾動(dòng)CATiledLayer
實(shí)現(xiàn)
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIScrollView *scrollView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//add the tiled layer
CATiledLayer *tileLayer = [CATiledLayer layer];?
tileLayer.frame = CGRectMake(0, 0, 2048, 2048);
tileLayer.delegate = self; [self.scrollView.layer addSublayer:tileLayer];
//configure the scroll view
self.scrollView.contentSize = tileLayer.frame.size;
//draw layer
[tileLayer setNeedsDisplay];
}
- (void)drawLayer:(CATiledLayer *)layer inContext:(CGContextRef)ctx
{
//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
NSInteger x = floor(bounds.origin.x / layer.tileSize.width);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height);
//load tile image
NSString *imageName = [NSString stringWithFormat: @"Snowman_%02i_%02i", x, y];
NSString *imagePath = [[NSBundle mainBundle] pathForResource:imageName ofType:@"jpg"];
UIImage *tileImage = [UIImage imageWithContentsOfFile:imagePath];
//draw tile
UIGraphicsPushContext(ctx);
[tileImage drawInRect:bounds];
UIGraphicsPopContext();
}
@end
圖6.12 用UIScrollView
滾動(dòng)CATiledLayer
當(dāng)你滑動(dòng)這個(gè)圖片,你會(huì)發(fā)現(xiàn)當(dāng)CATiledLayer
載入小圖的時(shí)候,他們會(huì)淡入到界面中。這是CATiledLayer
的默認(rèn)行為。(你可能已經(jīng)在iOS 6之前的蘋果地圖程序中見過這個(gè)效果)你可以用fadeDuration
屬性改變淡入時(shí)長(zhǎng)或直接禁用掉。CATiledLayer
(不同于大部分的UIKit
和Core Animation方法)支持多線程繪制,-drawLayer:inContext:
方法可以在多個(gè)線程中同時(shí)地并發(fā)調(diào)用,所以請(qǐng)小心謹(jǐn)慎地確保你在這個(gè)方法中實(shí)現(xiàn)的繪制代碼是線程安全的。
你也許已經(jīng)注意到了這些小圖并不是以Retina的分辨率顯示的。為了以屏幕的原生分辨率來渲染CATiledLayer
,我們需要設(shè)置圖層的contentsScale
來匹配UIScreen
的scale
屬性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
有趣的是,tileSize
是以像素為單位,而不是點(diǎn),所以增大了contentsScale
就自動(dòng)有了默認(rèn)的小圖尺寸(現(xiàn)在它是128*128的點(diǎn)而不是256*256).所以,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個(gè)不同的小圖。我們需要做的是適應(yīng)小圖渲染代碼以對(duì)應(yīng)安排scale
的變化,然而:
//determine tile coordinate
CGRect bounds = CGContextGetClipBoundingBox(ctx);
CGFloat scale = [UIScreen mainScreen].scale;
NSInteger x = floor(bounds.origin.x / layer.tileSize.width * scale);
NSInteger y = floor(bounds.origin.y / layer.tileSize.height * scale);
通過這個(gè)方法糾正scale
也意味著我們的雪人圖將以一半的大小渲染在Retina設(shè)備上(總尺寸是1024*1024,而不是2048*2048)。這個(gè)通常都不會(huì)影響到用CATiledLayer
正常顯示的圖片類型(比如照片和地圖,他們?cè)谠O(shè)計(jì)上就是要支持放大縮小,能夠在不同的縮放條件下顯示),但是也需要在心里明白。
在iOS 5中,蘋果引入了一個(gè)新的CALayer
子類叫做CAEmitterLayer
。CAEmitterLayer
是一個(gè)高性能的粒子引擎,被用來創(chuàng)建實(shí)時(shí)例子動(dòng)畫如:煙霧,火,雨等等這些效果。
CAEmitterLayer
看上去像是許多CAEmitterCell
的容器,這些CAEmitierCell
定義了一個(gè)例子效果。你將會(huì)為不同的例子效果定義一個(gè)或多個(gè)CAEmitterCell
作為模版,同時(shí)CAEmitterLayer
負(fù)責(zé)基于這些模版實(shí)例化一個(gè)粒子流。一個(gè)CAEmitterCell
類似于一個(gè)CALayer
:它有一個(gè)contents
屬性可以定義為一個(gè)CGImage
,另外還有一些可設(shè)置屬性控制著表現(xiàn)和行為。我們不會(huì)對(duì)這些屬性逐一進(jìn)行詳細(xì)的描述,你們可以在CAEmitterCell
類的頭文件中找到。
我們來舉個(gè)例子。我們將利用在一圓中發(fā)射不同速度和透明度的粒子創(chuàng)建一個(gè)火爆炸的效果。清單6.13包含了生成爆炸的代碼。圖6.13是運(yùn)行結(jié)果
清單6.13 用CAEmitterLayer
創(chuàng)建爆炸效果
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
?
//create particle emitter layer
CAEmitterLayer *emitter = [CAEmitterLayer layer];
emitter.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:emitter];
//configure emitter
emitter.renderMode = kCAEmitterLayerAdditive;
emitter.emitterPosition = CGPointMake(emitter.frame.size.width / 2.0, emitter.frame.size.height / 2.0);
//create a particle template
CAEmitterCell *cell = [[CAEmitterCell alloc] init];
cell.contents = (__bridge id)[UIImage imageNamed:@"Spark.png"].CGImage;
cell.birthRate = 150;
cell.lifetime = 5.0;
cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:1.0].CGColor;
cell.alphaSpeed = -0.4;
cell.velocity = 50;
cell.velocityRange = 50;
cell.emissionRange = M_PI * 2.0;
//add particle template to emitter
emitter.emitterCells = @[cell];
}
@end
圖6.13 火焰爆炸效果
CAEMitterCell
的屬性基本上可以分為三種:
color
屬性指定了一個(gè)可以混合圖片內(nèi)容顏色的混合色。在示例中,我們將它設(shè)置為桔色。emissionRange
屬性的值是2π,這意味著例子可以從360度任意位置反射出來。如果指定一個(gè)小一些的值,就可以創(chuàng)造出一個(gè)圓錐形alphaSpeed
設(shè)置為-0.4,就是說例子的透明度每過一秒就是減少0.4,這樣就有發(fā)射出去之后逐漸小時(shí)的效果。CAEmitterLayer
的屬性它自己控制著整個(gè)例子系統(tǒng)的位置和形狀。一些屬性比如birthRate
,lifetime
和celocity
,這些屬性在CAEmitterCell
中也有。這些屬性會(huì)以相乘的方式作用在一起,這樣你就可以用一個(gè)值來加速或者擴(kuò)大整個(gè)例子系統(tǒng)。其他值得提到的屬性有以下這些:
preservesDepth
,是否將3D例子系統(tǒng)平面化到一個(gè)圖層(默認(rèn)值)或者可以在3D空間中混合其他的圖層renderMode
,控制著在視覺上粒子圖片是如何混合的。你可能已經(jīng)注意到了示例中我們把它設(shè)置為kCAEmitterLayerAdditive
,它實(shí)現(xiàn)了這樣一個(gè)效果:合并例子重疊部分的亮度使得看上去更亮。如果我們把它設(shè)置為默認(rèn)的kCAEmitterLayerUnordered
,效果就沒那么好看了(見圖6.14).圖6.14 禁止混色之后的火焰粒子
當(dāng)iOS要處理高性能圖形繪制,必要時(shí)就是OpenGL。應(yīng)該說它應(yīng)該是最后的殺手锏,至少對(duì)于非游戲的應(yīng)用來說是的。因?yàn)橄啾菴ore Animation和UIkit框架,它不可思議地復(fù)雜。
OpenGL提供了Core Animation的基礎(chǔ),它是底層的C接口,直接和iPhone,iPad的硬件通信,極少地抽象出來的方法。OpenGL沒有對(duì)象或是圖層的繼承概念。它只是簡(jiǎn)單地處理三角形。OpenGL中所有東西都是3D空間中有顏色和紋理的三角形。用起來非常復(fù)雜和強(qiáng)大,但是用OpenGL繪制iOS用戶界面就需要很多很多的工作了。
為了能夠以高性能使用Core Animation,你需要判斷你需要繪制哪種內(nèi)容(矢量圖形,例子,文本,等等),但后選擇合適的圖層去呈現(xiàn)這些內(nèi)容,Core Animation中只有一些類型的內(nèi)容是被高度優(yōu)化的;所以如果你想繪制的東西并不能找到標(biāo)準(zhǔn)的圖層類,想要得到高性能就比較費(fèi)事情了。
因?yàn)镺penGL根本不會(huì)對(duì)你的內(nèi)容進(jìn)行假設(shè),它能夠繪制得相當(dāng)快。利用OpenGL,你可以繪制任何你知道必要的集合信息和形狀邏輯的內(nèi)容。所以很多游戲都喜歡用OpenGL(這些情況下,Core Animation的限制就明顯了:它優(yōu)化過的內(nèi)容類型并不一定能滿足需求),但是這樣依賴,方便的高度抽象接口就沒了。
在iOS 5中,蘋果引入了一個(gè)新的框架叫做GLKit,它去掉了一些設(shè)置OpenGL的復(fù)雜性,提供了一個(gè)叫做CLKView
的UIView
的子類,幫你處理大部分的設(shè)置和繪制工作。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項(xiàng)仍然需要你用CAEAGLLayer
完成,它是CALayer
的一個(gè)子類,用來顯示任意的OpenGL圖形。
大部分情況下你都不需要手動(dòng)設(shè)置CAEAGLLayer
(假設(shè)用GLKView),過去的日子就不要再提了。特別的,我們將設(shè)置一個(gè)OpenGL ES 2.0的上下文,它是現(xiàn)代的iOS設(shè)備的標(biāo)準(zhǔn)做法。
盡管不需要GLKit也可以做到這一切,但是GLKit囊括了很多額外的工作,比如設(shè)置頂點(diǎn)和片段著色器,這些都以類C語言叫做GLSL自包含在程序中,同時(shí)在運(yùn)行時(shí)載入到圖形硬件中。編寫GLSL代碼和設(shè)置EAGLayer
沒有什么關(guān)系,所以我們將用GLKBaseEffect
類將著色邏輯抽象出來。其他的事情,我們還是會(huì)有以往的方式。
在開始之前,你需要將GLKit和OpenGLES框架加入到你的項(xiàng)目中,然后就可以實(shí)現(xiàn)清單6.14中的代碼,里面是設(shè)置一個(gè)GAEAGLLayer
的最少工作,它使用了OpenGL ES 2.0 的繪圖上下文,并渲染了一個(gè)有色三角(見圖6.15).
清單6.14 用CAEAGLLayer
繪制一個(gè)三角形
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <GLKit/GLKit.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *glView;
@property (nonatomic, strong) EAGLContext *glContext;
@property (nonatomic, strong) CAEAGLLayer *glLayer;
@property (nonatomic, assign) GLuint framebuffer;
@property (nonatomic, assign) GLuint colorRenderbuffer;
@property (nonatomic, assign) GLint framebufferWidth;
@property (nonatomic, assign) GLint framebufferHeight;
@property (nonatomic, strong) GLKBaseEffect *effect;
?
@end
@implementation ViewController
- (void)setUpBuffers
{
//set up frame buffer
glGenFramebuffers(1, &_framebuffer);
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
//set up color render buffer
glGenRenderbuffers(1, &_colorRenderbuffer);
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorRenderbuffer);
[self.glContext renderbufferStorage:GL_RENDERBUFFER fromDrawable:self.glLayer];
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_framebufferWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_framebufferHeight);
//check success
if (glCheckFramebufferStatus(GL_FRAMEBUFFER) != GL_FRAMEBUFFER_COMPLETE) {
NSLog(@"Failed to make complete framebuffer object: %i", glCheckFramebufferStatus(GL_FRAMEBUFFER));
}
}
- (void)tearDownBuffers
{
if (_framebuffer) {
//delete framebuffer
glDeleteFramebuffers(1, &_framebuffer);
_framebuffer = 0;
}
if (_colorRenderbuffer) {
//delete color render buffer
glDeleteRenderbuffers(1, &_colorRenderbuffer);
_colorRenderbuffer = 0;
}
}
- (void)drawFrame {
//bind framebuffer & set viewport
glBindFramebuffer(GL_FRAMEBUFFER, _framebuffer);
glViewport(0, 0, _framebufferWidth, _framebufferHeight);
//bind shader program
[self.effect prepareToDraw];
//clear the screen
glClear(GL_COLOR_BUFFER_BIT); glClearColor(0.0, 0.0, 0.0, 1.0);
//set up vertices
GLfloat vertices[] = {
-0.5f, -0.5f, -1.0f, 0.0f, 0.5f, -1.0f, 0.5f, -0.5f, -1.0f,
};
//set up colors
GLfloat colors[] = {
0.0f, 0.0f, 1.0f, 1.0f, 0.0f, 1.0f, 0.0f, 1.0f, 1.0f, 0.0f, 0.0f, 1.0f,
};
//draw triangle
glEnableVertexAttribArray(GLKVertexAttribPosition);
glEnableVertexAttribArray(GLKVertexAttribColor);
glVertexAttribPointer(GLKVertexAttribPosition, 3, GL_FLOAT, GL_FALSE, 0, vertices);
glVertexAttribPointer(GLKVertexAttribColor,4, GL_FLOAT, GL_FALSE, 0, colors);
glDrawArrays(GL_TRIANGLES, 0, 3);
//present render buffer
glBindRenderbuffer(GL_RENDERBUFFER, _colorRenderbuffer);
[self.glContext presentRenderbuffer:GL_RENDERBUFFER];
}
- (void)viewDidLoad
{
[super viewDidLoad];
//set up context
self.glContext = [[EAGLContext alloc] initWithAPI: kEAGLRenderingAPIOpenGLES2];
[EAGLContext setCurrentContext:self.glContext];
//set up layer
self.glLayer = [CAEAGLLayer layer];
self.glLayer.frame = self.glView.bounds;
[self.glView.layer addSublayer:self.glLayer];
self.glLayer.drawableProperties = @{kEAGLDrawablePropertyRetainedBacking:@NO, kEAGLDrawablePropertyColorFormat: kEAGLColorFormatRGBA8};
//set up base effect
self.effect = [[GLKBaseEffect alloc] init];
//set up buffers
[self setUpBuffers];
//draw frame
[self drawFrame];
}
- (void)viewDidUnload
{
[self tearDownBuffers];
[super viewDidUnload];
}
- (void)dealloc
{
[self tearDownBuffers];
[EAGLContext setCurrentContext:nil];
}
@end
圖6.15 用OpenGL渲染的CAEAGLLayer
圖層
在一個(gè)真正的OpenGL應(yīng)用中,我們可能會(huì)用NSTimer
或CADisplayLink
周期性地每秒鐘調(diào)用-drawRrame
方法60次,同時(shí)會(huì)將幾何圖形生成和繪制分開以便不會(huì)每次都重新生成三角形的頂點(diǎn)(這樣也可以讓我們繪制其他的一些東西而不是一個(gè)三角形而已),不過上面這個(gè)例子已經(jīng)足夠演示了繪圖原則了。
最后一個(gè)圖層類型是AVPlayerLayer
。盡管它不是Core Animation框架的一部分(AV前綴看上去像),AVPlayerLayer
是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結(jié)合在一起,提供了一個(gè)CALayer
子類來顯示自定義的內(nèi)容類型。
AVPlayerLayer
是用來在iOS上播放視頻的。他是高級(jí)接口例如MPMoivePlayer
的底層實(shí)現(xiàn),提供了顯示視頻的底層控制。AVPlayerLayer
的使用相當(dāng)簡(jiǎn)單:你可以用+playerLayerWithPlayer:
方法創(chuàng)建一個(gè)已經(jīng)綁定了視頻播放器的圖層,或者你可以先創(chuàng)建一個(gè)圖層,然后用player
屬性綁定一個(gè)AVPlayer
實(shí)例。
在我們開始之前,我們需要添加AVFoundation到我們的項(xiàng)目中。然后,清單6.15創(chuàng)建了一個(gè)簡(jiǎn)單的電影播放器,圖6.16是代碼運(yùn)行結(jié)果。
清單6.15 用AVPlayerLayer
播放視頻
#import "ViewController.h"
#import <QuartzCore/QuartzCore.h>
#import <AVFoundation/AVFoundation.h>
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView; @end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//get video URL
NSURL *URL = [[NSBundle mainBundle] URLForResource:@"Ship" withExtension:@"mp4"];
//create player and player layer
AVPlayer *player = [AVPlayer playerWithURL:URL];
AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:player];
//set player layer frame and attach it to our view
playerLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:playerLayer];
//play the video
[player play];
}
@end
圖6.16 用AVPlayerLayer
圖層播放視頻的截圖
我們用代碼創(chuàng)建了一個(gè)AVPlayerLayer
,但是我們?nèi)匀话阉砑拥搅艘粋€(gè)容器視圖中,而不是直接在controller中的主視圖上添加。這樣其實(shí)是為了可以使用自動(dòng)布局限制使得圖層在最中間;否則,一旦設(shè)備被旋轉(zhuǎn)了我們就要手動(dòng)重新放置位置,因?yàn)镃ore Animation并不支持自動(dòng)大小和自動(dòng)布局(見第三章『圖層幾何學(xué)』)。
當(dāng)然,因?yàn)?code>AVPlayerLayer是CALayer
的子類,它繼承了父類的所有特性。我們并不會(huì)受限于要在一個(gè)矩形中播放視頻;清單6.16演示了在3D,圓角,有色邊框,蒙板,陰影等效果(見圖6.17).
清單6.16 給視頻增加變換,邊框和圓角
- (void)viewDidLoad
{
...
//set player layer frame and attach it to our view
playerLayer.frame = self.containerView.bounds;
[self.containerView.layer addSublayer:playerLayer];
//transform layer
CATransform3D transform = CATransform3DIdentity;
transform.m34 = -1.0 / 500.0;
transform = CATransform3DRotate(transform, M_PI_4, 1, 1, 0);
playerLayer.transform = transform;
?
//add rounded corners and border
playerLayer.masksToBounds = YES;
playerLayer.cornerRadius = 20.0;
playerLayer.borderColor = [UIColor redColor].CGColor;
playerLayer.borderWidth = 5.0;
//play the video
[player play];
}
圖6.17 3D視角下的邊框和圓角AVPlayerLayer
這一章我們簡(jiǎn)要概述了一些專用圖層以及用他們實(shí)現(xiàn)的一些效果,我們只是了解到這些圖層的皮毛,像CATiledLayer
和CAEMitterLayer
這些類可以單獨(dú)寫一章的。但是,重點(diǎn)是記住CALayer
是用處很大的,而且它并沒有為所有可能的場(chǎng)景進(jìn)行優(yōu)化。為了獲得Core Animation最好的性能,你需要為你的工作選對(duì)正確的工具,希望你能夠挖掘這些不同的CALayer
子類的功能。這一章我們通過CAEmitterLayer
和AVPlayerLayer
類簡(jiǎn)單地接觸到了一些動(dòng)畫,在第二章,我們將繼續(xù)深入研究動(dòng)畫,就從隱式動(dòng)畫開始。
更多建議: