本書翻譯自:iOS Core Animation: Advanced Techniques
知識是人類進(jìn)步的階梯,重在分享
翻譯觉至,喵~
譯者為:
(排名不分先后涮瞻,感謝他倆的付出!)
小兵將ZsIsMe的gitbook搬運(yùn)至簡書上嘁酿,方便大家查看律杠。
如果在閱讀過程中發(fā)現(xiàn)有什么問題莽鸭,請到這里(本電子書在github上的地址)開issue,我會盡快改過來珠移。
復(fù)雜的組織都是專門化的
Catharine R. Stimpson
到目前為止,我們已經(jīng)探討過CALayer類了末融,同時(shí)我們也了解到了一些非常有用的繪圖和動畫功能钧惧。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會學(xué)習(xí)其他的一些圖層類勾习,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力浓瞪。
6.1 CAShapeLayer
在第四章『視覺效果』我們學(xué)習(xí)到了不使用圖片的情況下用CGPath去構(gòu)造任意形狀的陰影。如果我們能用同樣的方式創(chuàng)建相同形狀的圖層就好了巧婶。
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類乾颁。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形艺栈,最后CAShapeLayer就自動渲染出來了英岭。當(dāng)然,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個路徑湿右,相比直下诅妹,使用CAShapeLayer有以下一些優(yōu)點(diǎn):
渲染快速。CAShapeLayer使用了硬件加速毅人,繪制同一圖形會比用Core Graphics快很多吭狡。
- 高效使用內(nèi)存尖殃。一個CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個寄宿圖形,所以無論有多大赵刑,都不會占用太多的內(nèi)存分衫。
- 不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制般此。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們在第二章所見)蚪战。
- 不會出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時(shí)铐懊,它不像一個有寄宿圖的普通圖層一樣變得像素化邀桑。
創(chuàng)建一個CGPath
CAShapeLayer可以用來繪制所有能夠通過CGPath來表示的形狀。這個形狀不一定要閉合科乎,圖層路徑也不一定要不可破壁畸,事實(shí)上你可以在一個圖層上繪制好幾個不同的形狀。你可以控制一些屬性比如lineWith(線寬茅茂,用點(diǎn)表示單位)捏萍,lineCap(線條結(jié)尾的樣子),和lineJoin(線條之間的結(jié)合點(diǎn)的樣子)空闲;但是在圖層層面你只有一次機(jī)會設(shè)置這些屬性令杈。如果你想用不同顏色或風(fēng)格來繪制多個形狀,就不得不為每個形狀準(zhǔn)備一個圖層了碴倾。
清單6.1 的代碼用一個CAShapeLayer渲染一個簡單的火柴人逗噩。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創(chuàng)建了圖層路徑跌榔,這樣我們就不用考慮人工釋放CGPath了异雁。圖6.1是代碼運(yùn)行的結(jié)果。雖然還不是很完美僧须,但是總算知道了大意對吧纲刀!
清單6.1 用CAShapeLayer繪制一個火柴人
#import "DrawingView.h"
#import
@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
圓角
第二章里面提到了CAShapeLayer為創(chuàng)建圓角視圖提供了一個方法,就是CALayer的cornerRadius屬性(譯者注:其實(shí)是在第四章提到的)担平。雖然使用CAShapeLayer類需要更多的工作柑蛇,但是它有一個優(yōu)勢就是可以單獨(dú)指定每個角。
我們創(chuàng)建圓角矩形其實(shí)就是人工繪制單獨(dú)的直線和弧度驱闷,但是事實(shí)上UIBezierPath有自動繪制圓角矩形的構(gòu)造方法耻台,下面這段代碼繪制了一個有三個圓角一個直角的矩形:
//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];
我們可以通過這個圖層路徑繪制一個既有直角又有圓角的視圖。如果我們想依照此圖形來剪裁視圖內(nèi)容空另,我們可以把CAShapeLayer作為視圖的宿主圖層盆耽,而不是添加一個子視圖(圖層蒙板的詳細(xì)解釋見第四章『視覺效果』)。
6.2 CATextLayer
用戶界面是無法從一個單獨(dú)的圖片里面構(gòu)建的。一個設(shè)計(jì)良好的圖標(biāo)能夠很好地表現(xiàn)一個按鈕或控件的意圖摄杂,不過你遲早都要需要一個不錯的老式風(fēng)格的文本標(biāo)簽坝咐。
如果你想在一個圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內(nèi)容(這就是UILabel的精髓)析恢。如果越過寄宿于圖層的視圖墨坚,直接在圖層上操作,那其實(shí)相當(dāng)繁瑣映挂。你要為每一個顯示文字的圖層創(chuàng)建一個能像圖層代理一樣工作的類泽篮,還要邏輯上判斷哪個圖層需要顯示哪個字符串,更別提還要記錄不同的字體柑船,顏色等一系列亂七八糟的東西帽撑。
萬幸的是這些都是不必要的,Core Animation提供了一個CALayer的子類CATextLayer鞍时,它以圖層的形式包含了UILabel幾乎所有的繪制特性亏拉,并且額外提供了一些新的特性。
同樣逆巍,CATextLayer也要比UILabel渲染得快得多及塘。很少有人知道在iOS 6及之前的版本,UILabel其實(shí)是通過WebKit來實(shí)現(xiàn)繪制的锐极,這樣就造成了當(dāng)有很多文字的時(shí)候就會有極大的性能壓力磷蛹。而CATextLayer使用了Core text,并且渲染得非诚荆快。
讓我們來嘗試用CATextLayer來顯示一些文字庇勃。清單6.2的代碼實(shí)現(xiàn)了這一功能檬嘀,結(jié)果如圖6.2所示。
清單6.2 用CATextLayer來實(shí)現(xiàn)一個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
如果你仔細(xì)看這個文本,你會發(fā)現(xiàn)一個奇怪的地方:這些文本有一些像素化了。這是因?yàn)椴]有以Retina的方式渲染眼坏,第二章提到了這個contentScale屬性誓焦,用來決定圖層內(nèi)容應(yīng)該以怎樣的分辨率來渲染。contentsScale并不關(guān)心屏幕的拉伸因素而總是默認(rèn)為1.0现拒。如果我們想以Retina的質(zhì)量來顯示文字,我們就得手動地設(shè)置CATextLayer的contentsScale屬性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
這樣就解決了這個問題(如圖6.3)
CATextLayer的font屬性不是一個UIFont類型衷掷,而是一個CFTypeRef類型。這樣可以根據(jù)你的具體需要來決定字體屬性應(yīng)該是用CGFontRef類型還是CTFontRef類型(Core Text字體)柿菩。同時(shí)字體大小也是用fontSize屬性單獨(dú)設(shè)置的戚嗅,因?yàn)镃TFontRef和CGFontRef并不像UIFont一樣包含點(diǎn)大小。這個例子會告訴你如何將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)該說這是一個很方便的特性锥债。不過事實(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)目中。否則分唾,編譯器是無法識別屬性常量的抗碰。
圖6.4是代碼運(yùn)行結(jié)果(注意那個紅色的下劃線文本)
清單6.3 用NSAttributedString實(shí)現(xiàn)一個富文本標(biāo)簽。
#import "DrawingView.h"
#import
#import
@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
行距和字距
有必要提一下的是弧蝇,由于繪制的實(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)簽比較而言會更加繁瑣一些。如果我們真的在需求一個UILabel的可用替代品去枷,最好是能夠在Interface Builder上創(chuàng)建我們的標(biāo)簽盗扇,而且盡可能地像一般的視圖一樣正常工作祷肯。
我們應(yīng)該繼承UILabel,然后添加一個子圖層CATextLayer并重寫顯示文本的方法疗隶。但是仍然會有由UILabel的-drawRect:方法創(chuàng)建的空寄宿圖佑笋。而且由于CALayer不支持自動縮放和自動布局,子視圖并不是主動跟蹤視圖邊界的大小斑鼻,所以每次視圖大小被更改蒋纬,我們不得不手動更新子圖層的邊界。
我們真正想要的是一個用CATextLayer作為宿主圖層的UILabel子類坚弱,這樣就可以隨著視圖自動調(diào)整大小而且也沒有冗余的寄宿圖啦蜀备。
就像我們在第一章『圖層樹』討論的一樣,每一個UIView都是寄宿在一個CALayer的示例上荒叶。這個圖層是由視圖自動創(chuàng)建和管理的碾阁,那我們可以用別的圖層類型替代它么?一旦被創(chuàng)建些楣,我們就無法代替這個圖層了脂凶。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創(chuàng)建的時(shí)候能返回一個不同的圖層子類愁茁。UIView會在初始化的時(shí)候調(diào)用+layerClass方法蚕钦,然后用它的返回類型來創(chuàng)建宿主圖層。
清單6.4 演示了一個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
@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)行代碼菠齿,你會發(fā)現(xiàn)文本并沒有像素化佑吝,而我們也沒有設(shè)置contentsScale屬性。把CATextLayer作為宿主圖層的另一好處就是視圖自動設(shè)置了contentsScale屬性泞当。
在這個簡單的例子中,我們只是實(shí)現(xiàn)了UILabel的一部分風(fēng)格和布局屬性民珍,不過稍微再改進(jìn)一下我們就可以創(chuàng)建一個支持UILabel所有功能甚至更多功能的LayerLabel類(你可以在一些線上的開源項(xiàng)目中找到)襟士。
如果你打算支持iOS 6及以上,基于CATextLayer的標(biāo)簽可能就有有些局限性嚷量。但是總得來說陋桂,如果想在app里面充分利用CALayer子類,用+layerClass來創(chuàng)建基于不同圖層的視圖是一個簡單可復(fù)用的方法蝶溶。
6.3 CATransformLayer
當(dāng)我們在構(gòu)造復(fù)雜的3D事物的時(shí)候嗜历,如果能夠組織獨(dú)立元素就太方便了宣渗。比如說,你想創(chuàng)造一個孩子的手臂:你就需要確定哪一部分是孩子的手腕梨州,哪一部分是孩子的前臂痕囱,哪一部分是孩子的肘,哪一部分是孩子的上臂暴匠,哪一部分是孩子的肩膀等等鞍恢。
當(dāng)然是允許獨(dú)立地移動每個區(qū)域的啦。以肘為指點(diǎn)會移動前臂和手每窖,而不是肩膀帮掉。Core Animation圖層很容易就可以讓你在2D環(huán)境下做出這樣的層級體系下的變換,但是3D情況下就不太可能窒典,因?yàn)樗械膱D層都把他的孩子都平面化到一個場景中(第五章『變換』有提到)蟆炊。
CATransformLayer解決了這個問題,CATransformLayer不同于普通的CALayer瀑志,因?yàn)樗荒茱@示它自己的內(nèi)容涩搓。只有當(dāng)存在了一個能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層后室,所以它能夠用于構(gòu)造一個層級的3D結(jié)構(gòu)缩膝,比如我的手臂示例。
用代碼創(chuàng)建一個手臂需要相當(dāng)多的代碼岸霹,所以我就演示得更簡單一些吧:在第五章的立方體示例疾层,我們將通過旋轉(zhuǎn)camara來解決圖層平面化問題而不是像立方體示例代碼中用的sublayerTransform。這是一個非常不錯的技巧贡避,但是只能作用域單個對象上痛黎,如果你的場景包含兩個立方體,那我們就不能用這個技巧單獨(dú)旋轉(zhuǎn)他們了刮吧。
那么湖饱,就讓我們來試一試CATransformLayer吧,第一個問題就來了:在第五章杀捻,我們是用多個視圖來構(gòu)造了我們的立方體井厌,而不是單獨(dú)的圖層。我們不能在不打亂已有的視圖層次的前提下在一個本身不是有寄宿圖的圖層中放置一個寄宿圖圖層致讥。我們可以創(chuàng)建一個新的UIView子類寄宿
在CATransformLayer(用+layerClass方法)之上仅仆。但是,為了簡化案例垢袱,我們僅僅重建了一個單獨(dú)的圖層墓拜,而不是使用視圖。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標(biāo)簽请契,不過我們現(xiàn)在也用不到這個特性咳榜。
清單6.5就是代碼夏醉。我們以我們在第五章使用過的相同基本邏輯放置立方體。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層涌韩,我們將他們放置到一個CATransformLayer中創(chuàng)建一個獨(dú)立的立方體對象畔柔,然后將兩個這樣的立方體放進(jìn)容器中。我們隨機(jī)地給立方面染色以將他們區(qū)分開來贸辈,這樣就不用靠標(biāo)簽或是光亮來區(qū)分他們释树。圖6.5是運(yùn)行結(jié)果。
清單6.5 用CATransformLayer裝配一個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.4 CAGradientLayer
CAGradientLayer是用來生成兩種或更多顏色平滑漸變的擎淤。用Core Graphics復(fù)制一個CAGradientLayer并將內(nèi)容繪制到一個普通圖層的寄宿圖也是有可能的奢啥,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。
基礎(chǔ)漸變
我們將從一個簡單的紅變藍(lán)的對角線漸變開始(見清單6.6).這些漸變色彩放在一個數(shù)組中嘴拢,并賦給colors屬性桩盲。這個數(shù)組成員接受CGColorRef類型的值(并不是從NSObject派生而來),所以我們要用通過bridge轉(zhuǎn)換以確保編譯正常席吴。
CAGradientLayer也有startPoint和endPoint屬性赌结,他們決定了漸變的方向。這兩個參數(shù)是以單位坐標(biāo)系進(jìn)行的定義孝冒,所以左上角坐標(biāo)是{0, 0}柬姚,右下角坐標(biāo)是{1, 1}。代碼運(yùn)行結(jié)果如圖6.6
清單6.6 簡單的兩種顏色的對角線漸變
@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
多重漸變
如果你愿意庄涡,colors屬性可以包含很多顏色量承,所以創(chuàng)建一個彩虹一樣的多重漸變也是很簡單的。默認(rèn)情況下穴店,這些顏色在空間上均勻地被渲染撕捍,但是我們可以用locations屬性來調(diào)整空間。locations屬性是一個浮點(diǎn)數(shù)值的數(shù)組(以NSNumber包裝)泣洞。這些浮點(diǎn)數(shù)定義了colors屬性中每個不同顏色的位置忧风,同樣的,也是以單位坐標(biāo)系進(jìn)行標(biāo)定球凰。0.0代表著漸變的開始狮腿,1.0代表著結(jié)束。
locations數(shù)組并不是強(qiáng)制要求的呕诉,但是如果你給它賦值了就一定要確保locations的數(shù)組大小和colors數(shù)組大小一定要相同缘厢,否則你將會得到一個空白的漸變。
清單6.7展示了一個基于清單6.6的對角線漸變的代碼改造∫宥ぃ現(xiàn)在變成了從紅到黃最后到綠色的漸變昧绣。locations數(shù)組指定了0.0规肴,0.25和0.5三個數(shù)值捶闸,這樣這三個漸變就有點(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.5 CAReplicatorLayer
CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會繪制一個或多個圖層的子圖層删壮,并在每個復(fù)制體上應(yīng)用不同的變換贪绘。看上去演示能夠更加解釋這些央碟,我們來寫個例子吧税灌。
重復(fù)圖層(Repeating Layers)
清單6.8中,我們在屏幕的中間創(chuàng)建了一個小白色方塊圖層亿虽,然后用CAReplicatorLayer生成十個圖層組成一個圓圈菱涤。instanceCount屬性指定了圖層需要重復(fù)多少次。instanceTransform指定了一個CATransform3D3D變換(這種情況下洛勉,下一圖層的位移和旋轉(zhuǎn)將會移動到圓圈的下一個點(diǎn))粘秆。
變換是逐步增加的,每個實(shí)例都是相對于前一實(shí)例布局收毫。這就是為什么這些復(fù)制體最終不會出現(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
注意到當(dāng)圖層在重復(fù)的時(shí)候此再,他們的顏色也在變化:這是用instanceBlueOffset和instanceGreenOffset屬性實(shí)現(xiàn)的昔搂。通過逐步減少藍(lán)色和綠色通道,我們逐漸將圖層顏色轉(zhuǎn)換成了紅色输拇。這個復(fù)制效果看起來很酷摘符,但是CAReplicatorLayer真正應(yīng)用到實(shí)際程序上的場景比如:一個游戲中導(dǎo)彈的軌跡云,或者粒子爆炸(盡管iOS 5已經(jīng)引入了CAEmitterLayer淳附,它更適合創(chuàng)建任意的粒子效果)议慰。除此之外,還有一個實(shí)際應(yīng)用是:反射奴曙。
反射
使用CAReplicatorLayer并應(yīng)用一個負(fù)比例變換于一個復(fù)制圖層别凹,你就可以創(chuàng)建指定視圖(或整個視圖層次)內(nèi)容的鏡像圖片,這樣就創(chuàng)建了一個實(shí)時(shí)的『反射』效果洽糟。讓我們來嘗試實(shí)現(xiàn)這個創(chuàng)意:指定一個繼承于UIView的ReflectionView炉菲,它會自動產(chǎn)生內(nèi)容的反射效果。實(shí)現(xiàn)這個效果的代碼很簡單(見清單6.9)坤溃,實(shí)際上用ReflectionView實(shí)現(xiàn)這個效果會更簡單拍霜,我們只需要把ReflectionView的實(shí)例放置于Interface Builder(見圖6.9)文虏,它就會實(shí)時(shí)生成子視圖的反射诫硕,而不需要別的代碼(見圖6.10).
清單6.9 用CAReplicatorLayer自動繪制反射
#import "ReflectionView.h"
#import
@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
開源代碼ReflectionView完成了一個自適應(yīng)的漸變淡出效果(用CAGradientLayer和圖層蒙板實(shí)現(xiàn))叽掘,代碼見 https://github.com/nicklockwood/ReflectionView
6.6 CAScrollLayer
對于一個未轉(zhuǎn)換的圖層汁政,它的bounds和它的frame是一樣的道偷,frame屬性是由bounds屬性自動計(jì)算而出的缀旁,所以更改任意一個值都會更新其他值。
但是如果你只想顯示一個大圖層里面的一小部分呢勺鸦。比如說并巍,你可能有一個很大的圖片,你希望用戶能夠隨意滑動换途,或者是一個數(shù)據(jù)或文本的長列表懊渡。在一個典型的iOS應(yīng)用中,你可能會用到UITableView或是UIScrollView军拟,但是對于獨(dú)立的圖層來說剃执,什么會等價(jià)于剛剛提到的UITableView和UIScrollView呢?
在第二章中懈息,我們探索了圖層的contentsRect屬性的用法忠蝗,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個非常好的解決方案漓拾,因?yàn)楦笞睿@樣做的話每次你想『滑動』可視區(qū)域的時(shí)候,你就需要手工重新計(jì)算并更新所有的子圖層位置骇两。
這個時(shí)候就需要CAScrollLayer了速种。CAScrollLayer有一個-scrollToPoint:方法,它自動適應(yīng)bounds的原點(diǎn)以便圖層內(nèi)容出現(xiàn)在滑動的地方低千。注意配阵,這就是它做的所有事情。前面提到過示血,Core Animation并不處理用戶輸入棋傍,所以CAScrollLayer并不負(fù)責(zé)將觸摸事件轉(zhuǎn)換為滑動事件,既不渲染滾動條难审,也不實(shí)現(xiàn)任何iOS指定行為例如滑動反彈(當(dāng)視圖滑動超多了它的邊界的將會反彈回正確的地方)瘫拣。
讓我們來用CAScrollLayer來常見一個基本的UIScrollView替代品。我們將會用CAScrollLayer作為視圖的宿主圖層告喊,并創(chuàng)建一個自定義的UIView麸拄,然后用UIPanGestureRecognizer實(shí)現(xiàn)觸摸事件響應(yīng)。這段代碼見清單6.10. 圖6.11是運(yùn)行效果:ScrollView顯示了一個大于它的frame的UIImageView黔姜。
清單6.10 用CAScrollLayer實(shí)現(xiàn)滑動視圖
#import "ScrollView.h"
#import @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)建一個湊合的滑動視圖
不同于UIScrollView拢切,我們定制的滑動視圖類并沒有實(shí)現(xiàn)任何形式的邊界檢查(bounds checking)。圖層內(nèi)容極有可能滑出視圖的邊界并無限滑下去秆吵。CAScrollLayer并沒有等同于UIScrollView中contentSize的屬性淮椰,所以當(dāng)CAScrollLayer滑動的時(shí)候完全沒有一個全局的可滑動區(qū)域的概念,也無法自適應(yīng)它的邊界原點(diǎn)至你指定的值。它之所以不能自適應(yīng)邊界大小是因?yàn)樗恍枰魉耄瑑?nèi)容完全可以超過邊界豺撑。
那你一定會奇怪用CAScrollLayer的意義到底何在,因?yàn)槟憧梢院唵蔚赜靡粋€普通的CALayer然后手動適應(yīng)邊界原點(diǎn)啊黔牵。真相其實(shí)并不復(fù)雜,UIScrollView并沒有用CAScrollLayer爷肝,事實(shí)上猾浦,就是簡單的通過直接操作圖層邊界來實(shí)現(xiàn)滑動。
CAScrollLayer有一個潛在的有用特性灯抛。如果你查看CAScrollLayer的頭文件金赦,你就會注意到有一個擴(kuò)展分類實(shí)現(xiàn)了一些方法和屬性:
- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;
看到這些方法和屬性名,你也許會以為這些方法給每個CALayer實(shí)例增加了滑動功能对嚼。但是事實(shí)上他們只是放置在CAScrollLayer中的圖層的實(shí)用方法夹抗。scrollPoint:方法從圖層樹中查找并找到第一個可用的CAScrollLayer,然后滑動它使得指定點(diǎn)成為可視的纵竖。scrollRectToVisible:方法實(shí)現(xiàn)了同樣的事情只不過是作用在一個矩形上的漠烧。visibleRect屬性決定圖層(如果存在的話)的哪部分是當(dāng)前的可視區(qū)域。如果你自己實(shí)現(xiàn)這些方法就會相對容易明白一點(diǎn)靡砌,但是CAScrollLayer幫你省了這些麻煩已脓,所以當(dāng)涉及到實(shí)現(xiàn)圖層滑動的時(shí)候就可以用上了。
6.7 CATiledLayer
有些時(shí)候你可能需要繪制一個很大的圖片通殃,常見的例子就是一個高像素的照片或者是地球表面的詳細(xì)地圖度液。iOS應(yīng)用通暢運(yùn)行在內(nèi)存受限的設(shè)備上,所以讀取整個圖片到內(nèi)存中是不明智的画舌。載入大圖可能會相當(dāng)?shù)芈榈#切δ憧瓷先ケ容^方便的做法(在主線程調(diào)用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)將會阻塞你的用戶界面,至少會引起動畫卡頓現(xiàn)象曲聂。
能高效繪制在iOS上的圖片也有一個大小限制霹购。所有顯示在屏幕上的圖片最終都會被轉(zhuǎn)化為OpenGL紋理,同時(shí)OpenGL有一個最大的紋理尺寸(通常是20482048朋腋,或40964096厕鹃,這個取決于設(shè)備型號)。如果你想在單個紋理中顯示一個比這大的圖乍丈,即便圖片已經(jīng)存在于內(nèi)存中了剂碴,你仍然會遇到很大的性能問題,因?yàn)镃ore Animation強(qiáng)制用CPU處理圖片而不是更快的GPU(見第12章『速度的曲調(diào)』轻专,和第13章『高效繪圖』忆矛,它更加詳細(xì)地解釋了軟件繪制和硬件繪制)。
CATiledLayer為載入大圖造成的性能問題提供了一個解決方案:將大圖分解成小片然后將他們單獨(dú)按需載入。讓我們用實(shí)驗(yàn)來證明一下催训。
小片裁剪
這個示例中洽议,我們將會從一個2048*2048分辨率的雪人圖片入手。為了能夠從CATiledLayer中獲益漫拭,我們需要把這個圖片裁切成許多小一些的圖片亚兄。你可以通過代碼來完成這件事情,但是如果你在運(yùn)行時(shí)讀入整個圖片并裁切采驻,那CATiledLayer這些所有的性能優(yōu)點(diǎn)就損失殆盡了审胚。理想情況下來說,最好能夠逐個步驟來實(shí)現(xiàn)礼旅。
清單6.11 演示了一個簡單的Mac OS命令行程序膳叨,它用CATiledLayer將一個圖片裁剪成小圖并存儲到不同的文件中。
清單6.11 裁剪圖片成小圖的終端程序
#import
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;
}
這個程序?qū)?0482048分辨率的雪人圖案裁剪成了64個不同的256256的小圖痘系。(256*256是CATiledLayer的默認(rèn)小圖大小菲嘴,默認(rèn)大小可以通過tileSize屬性更改)。程序接受一個圖片路徑作為命令行的第一個參數(shù)汰翠。我們可以在編譯的scheme將路徑參數(shù)硬編碼然后就可以在Xcode中運(yùn)行了龄坪,但是以后作用在另一個圖片上就不方便了。所以复唤,我們編譯了這個程序并把它保存到敏感的地方悉默,然后從終端調(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:
這個程序相當(dāng)基礎(chǔ)苟穆,但是能夠輕易地?cái)U(kuò)展支持額外的參數(shù)比如小圖大小抄课,或者導(dǎo)出格式等等。運(yùn)行結(jié)果是64個新圖的序列雳旅,如下面命名:
Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg
既然我們有了裁切后的小圖跟磨,我們就要讓iOS程序用到他們。CATiledLayer很好地和UIScrollView集成在一起攒盈。除了設(shè)置圖層和滑動視圖邊界以適配整個圖片大小抵拘,我們真正要做的就是實(shí)現(xiàn)-drawLayer:inContext:方法,當(dāng)需要載入新的小圖時(shí)型豁,CATiledLayer就會調(diào)用到這個方法僵蛛。
清單6.12演示了代碼。圖6.12是代碼運(yùn)行結(jié)果迎变。
清單6.12 一個簡單的滾動CATiledLayer實(shí)現(xiàn)
#import "ViewController.h"
#import
@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
當(dāng)你滑動這個圖片充尉,你會發(fā)現(xiàn)當(dāng)CATiledLayer載入小圖的時(shí)候,他們會淡入到界面中衣形。這是CATiledLayer的默認(rèn)行為驼侠。(你可能已經(jīng)在iOS 6之前的蘋果地圖程序中見過這個效果)你可以用fadeDuration屬性改變淡入時(shí)長或直接禁用掉姿鸿。CATiledLayer(不同于大部分的UIKit和Core Animation方法)支持多線程繪制,-drawLayer:inContext:方法可以在多個線程中同時(shí)地并發(fā)調(diào)用倒源,所以請小心謹(jǐn)慎地確保你在這個方法中實(shí)現(xiàn)的繪制代碼是線程安全的苛预。
Retina小圖
你也許已經(jīng)注意到了這些小圖并不是以Retina的分辨率顯示的。為了以屏幕的原生分辨率來渲染CATiledLayer笋熬,我們需要設(shè)置圖層的contentsScale來匹配UIScreen的scale屬性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
有趣的是热某,tileSize是以像素為單位,而不是點(diǎn)胳螟,所以增大了contentsScale就自動有了默認(rèn)的小圖尺寸(現(xiàn)在它是128128的點(diǎn)而不是256256).所以昔馋,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個不同的小圖。我們需要做的是適應(yīng)小圖渲染代碼以對應(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);
通過這個方法糾正scale也意味著我們的雪人圖將以一半的大小渲染在Retina設(shè)備上(總尺寸是10241024,而不是20482048)骏令。這個通常都不會影響到用CATiledLayer正常顯示的圖片類型(比如照片和地圖蔬捷,他們在設(shè)計(jì)上就是要支持放大縮小,能夠在不同的縮放條件下顯示)榔袋,但是也需要在心里明白周拐。
6.8 CAEmitterLayer
在iOS 5中,蘋果引入了一個新的CALayer子類叫做CAEmitterLayer凰兑。CAEmitterLayer是一個高性能的粒子引擎妥粟,被用來創(chuàng)建實(shí)時(shí)例子動畫如:煙霧,火吏够,雨等等這些效果勾给。
CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitierCell定義了一個例子效果锅知。你將會為不同的例子效果定義一個或多個CAEmitterCell作為模版播急,同時(shí)CAEmitterLayer負(fù)責(zé)基于這些模版實(shí)例化一個粒子流。一個CAEmitterCell類似于一個CALayer:它有一個contents屬性可以定義為一個CGImage售睹,另外還有一些可設(shè)置屬性控制著表現(xiàn)和行為桩警。我們不會對這些屬性逐一進(jìn)行詳細(xì)的描述,你們可以在CAEmitterCell類的頭文件中找到昌妹。1
我們來舉個例子捶枢。我們將利用在一圓中發(fā)射不同速度和透明度的粒子創(chuàng)建一個火爆炸的效果。清單6.13包含了生成爆炸的代碼飞崖。圖6.13是運(yùn)行結(jié)果
清單6.13 用CAEmitterLayer創(chuàng)建爆炸效果
#import "ViewController.h"
#import
@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屬性指定了一個可以混合圖片內(nèi)容顏色的混合色固歪。在示例中长已,我們將它設(shè)置為桔色。
- 例子某一屬性的變化范圍。比如emissionRange屬性的值是2π术瓮,這意味著例子可以從360度任意位置反射出來康聂。如果指定一個小一些的值,就可以創(chuàng)造出一個圓錐形
- 指定值在時(shí)間線上的變化胞四。比如恬汁,在示例中,我們將alphaSpeed設(shè)置為-0.4辜伟,就是說例子的透明度每過一秒就是減少0.4氓侧,這樣就有發(fā)射出去之后逐漸小時(shí)的效果。
CAEmitterLayer的屬性它自己控制著整個例子系統(tǒng)的位置和形狀导狡。一些屬性比如birthRate约巷,lifetime和celocity,這些屬性在CAEmitterCell中也有旱捧。這些屬性會以相乘的方式作用在一起独郎,這樣你就可以用一個值來加速或者擴(kuò)大整個例子系統(tǒng)。其他值得提到的屬性有以下這些:
- preservesDepth枚赡,是否將3D例子系統(tǒng)平面化到一個圖層(默認(rèn)值)或者可以在3D空間中混合其他的圖層
- renderMode氓癌,控制著在視覺上粒子圖片是如何混合的。你可能已經(jīng)注意到了示例中我們把它設(shè)置為kCAEmitterLayerAdditive贫橙,它實(shí)現(xiàn)了這樣一個效果:合并例子重疊部分的亮度使得看上去更亮贪婉。如果我們把它設(shè)置為默認(rèn)的kCAEmitterLayerUnordered,效果就沒那么好看了(見圖6.14).
6.9 CAEAGLLayer
當(dāng)iOS要處理高性能圖形繪制卢肃,必要時(shí)就是OpenGL疲迂。應(yīng)該說它應(yīng)該是最后的殺手锏,至少對于非游戲的應(yīng)用來說是的莫湘。因?yàn)橄啾菴ore Animation和UIkit框架鬼譬,它不可思議地復(fù)雜。
OpenGL提供了Core Animation的基礎(chǔ)逊脯,它是底層的C接口优质,直接和iPhone,iPad的硬件通信军洼,極少地抽象出來的方法巩螃。OpenGL沒有對象或是圖層的繼承概念。它只是簡單地處理三角形匕争。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根本不會對你的內(nèi)容進(jìn)行假設(shè)愧驱,它能夠繪制得相當(dāng)快慰技。利用OpenGL,你可以繪制任何你知道必要的集合信息和形狀邏輯的內(nèi)容组砚。所以很多游戲都喜歡用OpenGL(這些情況下吻商,Core Animation的限制就明顯了:它優(yōu)化過的內(nèi)容類型并不一定能滿足需求),但是這樣依賴糟红,方便的高度抽象接口就沒了艾帐。
在iOS 5中,蘋果引入了一個新的框架叫做GLKit改化,它去掉了一些設(shè)置OpenGL的復(fù)雜性掩蛤,提供了一個叫做CLKView的UIView的子類枉昏,幫你處理大部分的設(shè)置和繪制工作陈肛。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項(xiàng)仍然需要你用CAEAGLLayer完成,它是CALayer的一個子類兄裂,用來顯示任意的OpenGL圖形句旱。
大部分情況下你都不需要手動設(shè)置CAEAGLLayer(假設(shè)用GLKView),過去的日子就不要再提了晰奖。特別的谈撒,我們將設(shè)置一個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類將著色邏輯抽象出來裆悄。其他的事情,我們還是會有以往的方式棘伴。
在開始之前螃诅,你需要將GLKit和OpenGLES框架加入到你的項(xiàng)目中,然后就可以實(shí)現(xiàn)清單6.14中的代碼肴敛,里面是設(shè)置一個GAEAGLLayer的最少工作艾君,它使用了OpenGL ES 2.0 的繪圖上下文采够,并渲染了一個有色三角(見圖6.15).
清單6.14 用CAEAGLLayer繪制一個三角形
#import "ViewController.h"
#import
#import
@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
在一個真正的OpenGL應(yīng)用中,我們可能會用NSTimer或CADisplayLink周期性地每秒鐘調(diào)用-drawRrame方法60次腻贰,同時(shí)會將幾何圖形生成和繪制分開以便不會每次都重新生成三角形的頂點(diǎn)(這樣也可以讓我們繪制其他的一些東西而不是一個三角形而已)吁恍,不過上面這個例子已經(jīng)足夠演示了繪圖原則了。
6.10 AVPlayerLayer
最后一個圖層類型是AVPlayerLayer播演。盡管它不是Core Animation框架的一部分(AV前綴看上去像)冀瓦,AVPlayerLayer是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結(jié)合在一起写烤,提供了一個CALayer子類來顯示自定義的內(nèi)容類型翼闽。
AVPlayerLayer是用來在iOS上播放視頻的。他是高級接口例如MPMoivePlayer的底層實(shí)現(xiàn)洲炊,提供了顯示視頻的底層控制感局。AVPlayerLayer的使用相當(dāng)簡單:你可以用+playerLayerWithPlayer:方法創(chuàng)建一個已經(jīng)綁定了視頻播放器的圖層,或者你可以先創(chuàng)建一個圖層暂衡,然后用player屬性綁定一個AVPlayer實(shí)例询微。
在我們開始之前,我們需要添加AVFoundation到我們的項(xiàng)目中狂巢。然后撑毛,清單6.15創(chuàng)建了一個簡單的電影播放器,圖6.16是代碼運(yùn)行結(jié)果唧领。
清單6.15 用AVPlayerLayer播放視頻
#import "ViewController.h"
#import
#import
@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
我們用代碼創(chuàng)建了一個AVPlayerLayer藻雌,但是我們?nèi)匀话阉砑拥搅艘粋€容器視圖中,而不是直接在controller中的主視圖上添加斩个。這樣其實(shí)是為了可以使用自動布局限制使得圖層在最中間胯杭;否則,一旦設(shè)備被旋轉(zhuǎn)了我們就要手動重新放置位置受啥,因?yàn)镃ore Animation并不支持自動大小和自動布局(見第三章『圖層幾何學(xué)』)做个。
當(dāng)然,因?yàn)锳VPlayerLayer是CALayer的子類滚局,它繼承了父類的所有特性居暖。我們并不會受限于要在一個矩形中播放視頻;清單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.png](http://upload-images.jianshu.io/upload_images/728237-5c7029c616a42797.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
---
#總結(jié)
這一章我們簡要概述了一些專用圖層以及用他們實(shí)現(xiàn)的一些效果莺奸,我們只是了解到這些圖層的皮毛,像CATiledLayer和CAEMitterLayer這些類可以單獨(dú)寫一章的冀宴。但是灭贷,重點(diǎn)是記住CALayer是用處很大的,而且它并沒有為所有可能的場景進(jìn)行優(yōu)化略贮。為了獲得Core Animation最好的性能甚疟,你需要為你的工作選對正確的工具,希望你能夠挖掘這些不同的CALayer子類的功能逃延。 這一章我們通過CAEmitterLayer和AVPlayerLayer類簡單地接觸到了一些動畫览妖,在第二章,我們將繼續(xù)深入研究動畫揽祥,就從隱式動畫開始讽膏。
[轉(zhuǎn)載 - ios核心動畫高級技巧(圖層樹)](http://www.reibang.com/p/4f101d7a7f50)
[轉(zhuǎn)載 - ios核心動畫高級技巧(寄宿圖)](http://www.reibang.com/p/3fe38a1377a4)
[轉(zhuǎn)載 - ios核心動畫高級技巧(圖層幾何學(xué))](http://www.reibang.com/p/b8753bbe22e6)
[轉(zhuǎn)載 - ios核心動畫高級技巧(視覺效果)](http://www.reibang.com/p/0f8dda24468c)
[轉(zhuǎn)載 - ios核心動畫高級技巧(變換)](http://www.reibang.com/p/b52510c7ad36)
[轉(zhuǎn)載 - ios核心動畫高級技巧(專用圖層)](http://www.reibang.com/p/f5872eebebc1)
---
#*隨手點(diǎn)個喜歡吧~*
#*關(guān)注我*
#*QQ--iOS 交流群:107548668*