本文轉(zhuǎn)載自:http://www.cocoachina.com/ios/20150105/10827.html? 為了防止cocochina以后刪除該文章副砍,故轉(zhuǎn)載至此瑞凑;
(六)專用圖層
復(fù)雜的組織都是專門化的--Catharine R. Stimpson
到目前為止限番,我們已經(jīng)探討過CALayer類了,同時我們也了解到了一些非常有用的繪圖和動畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已五辽。本章就會學(xué)習(xí)其他的一些圖層類,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力外恕。
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變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化遭贸。
創(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
圖6.1 用CAShapeLayer繪制一個簡單的火柴人
圓角
第二章里面提到了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ì)解釋見第四章『視覺效果』)树碱。
CATextLayer
用戶界面是無法從一個單獨(dú)的圖片里面構(gòu)建的肯适。一個設(shè)計良好的圖標(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)有很多文字的時候就會有極大的性能壓力聘鳞。而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
*********************
圖6.2 用CATextLayer來顯示一個純文本標(biāo)簽
如果你自習(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)
圖6.3 設(shè)置contentsScale來匹配屏幕
CATextLayer的font屬性不是一個UIFont類型悍赢,而是一個CFTypeRef類型决瞳。這樣可以根據(jù)你的具體需要來決定字體屬性應(yīng)該是用CGFontRef類型還是CTFontRef類型(Core Text字體)。同時字體大小也是用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添加到你的項目中。否則惕鼓,編譯器是無法識別屬性常量的筋现。
圖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
圖6.4 用CATextLayer實(shí)現(xiàn)一個富文本標(biāo)簽。
行距和字距
有必要提一下的是矾飞,由于繪制的實(shí)現(xiàn)機(jī)制不同(Core Text和WebKit)一膨,用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。
二者的差異程度(由使用的字體和字符決定)總的來說挺小洒沦,但是如果你想正確的顯示普通便簽和CATextLayer就一定要記住這一點(diǎn)豹绪。
UILabel的替代品
我們已經(jīng)證實(shí)了CATextLayer比UILabel有著更好的性能表現(xiàn),同時還有額外的布局選項并且在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)建的時候能返回一個不同的圖層子類轧铁。UIView會在初始化的時候調(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類(你可以在一些線上的開源項目中找到)茶袒。
如果你打算支持iOS 6及以上梯刚,基于CATextLayer的標(biāo)簽可能就有有些局限性。但是總得來說薪寓,如果想在app里面充分利用CALayer子類亡资,用+layerClass來創(chuàng)建基于不同圖層的視圖是一個簡單可復(fù)用的方法。
CATransformLayer
當(dāng)我們在構(gòu)造復(fù)雜的3D事物的時候向叉,如果能夠組織獨(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.5 同一視角下的倆不同變換的立方體
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
圖6.6 用CAGradientLayer實(shí)現(xiàn)簡單的兩種顏色的對角線漸變
多重漸變
如果你愿意把曼,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.7 用locations構(gòu)造偏移至左上角的三色漸變
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
圖6.8 用CAReplicatorLayer創(chuàng)建一圈圖層
注意到當(dāng)圖層在重復(fù)的時候疫衩,他們的顏色也在變化:這是用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í)現(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í)時生成子視圖的反射,而不需要別的代碼(見圖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
圖6.9 在Interface Builder中使用ReflectionView
圖6.10 ReflectionView自動實(shí)時產(chǎn)生反射效果歹苦。
開源代碼ReflectionView完成了一個自適應(yīng)的漸變淡出效果(用CAGradientLayer和圖層蒙板實(shí)現(xiàn))青伤,代碼見 https://github.com/nicklockwood/ReflectionView
CAScrollLayer
對于一個未轉(zhuǎn)換的圖層,它的bounds和它的frame是一樣的殴瘦,frame屬性是由bounds屬性自動計算而出的狠角,所以更改任意一個值都會更新其他值。
但是如果你只想顯示一個大圖層里面的一小部分呢蚪腋。比如說丰歌,你可能有一個很大的圖片,你希望用戶能夠隨意滑動屉凯,或者是一個數(shù)據(jù)或文本的長列表立帖。在一個典型的iOS應(yīng)用中,你可能會用到UITableView或是UIScrollView悠砚,但是對于獨(dú)立的圖層來說晓勇,什么會等價于剛剛提到的UITableView和UIScrollView呢?
在第二章中灌旧,我們探索了圖層的contentsRect屬性的用法绑咱,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個非常好的解決方案节榜,因?yàn)橄勐辏@樣做的話每次你想『滑動』可視區(qū)域的時候,你就需要手工重新計算并更新所有的子圖層位置宗苍。
這個時候就需要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滑動的時候完全沒有一個全局的可滑動區(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)圖層滑動的時候就可以用上了。