本文轉載自:http://www.cocoachina.com/ios/20150105/10827.html? 為了防止cocochina以后刪除該文章,故轉載至此;
(六)專用圖層
復雜的組織都是專門化的--Catharine R. Stimpson
到目前為止,我們已經探討過CALayer類了,同時我們也了解到了一些非常有用的繪圖和動畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會學習其他的一些圖層類,進一步擴展使用Core Animation繪圖的能力。
CAShapeLayer
在第四章『視覺效果』我們學習到了不使用圖片的情況下用CGPath去構造任意形狀的陰影。如果我們能用同樣的方式創建相同形狀的圖層就好了。
CAShapeLayer是一個通過矢量圖形而不是bitmap來繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來定義想要繪制的圖形,最后CAShapeLayer就自動渲染出來了。當然,你也可以用Core Graphics直接向原始的CALyer的內容中繪制一個路徑,相比直下,使用CAShapeLayer有以下一些優點:
渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會比用Core Graphics快很多。
高效使用內存。一個CAShapeLayer不需要像普通CALayer一樣創建一個寄宿圖形,所以無論有多大,都不會占用太多的內存。
不會被圖層邊界剪裁掉。一個CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們在第二章所見)。
不會出現像素化。當你給CAShapeLayer做3D變換時,它不像一個有寄宿圖的普通圖層一樣變得像素化。
創建一個CGPath
CAShapeLayer可以用來繪制所有能夠通過CGPath來表示的形狀。這個形狀不一定要閉合,圖層路徑也不一定要不可破,事實上你可以在一個圖層上繪制好幾個不同的形狀。你可以控制一些屬性比如lineWith(線寬,用點表示單位),lineCap(線條結尾的樣子),和lineJoin(線條之間的結合點的樣子);但是在圖層層面你只有一次機會設置這些屬性。如果你想用不同顏色或風格來繪制多個形狀,就不得不為每個形狀準備一個圖層了。
清單6.1 的代碼用一個CAShapeLayer渲染一個簡單的火柴人。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創建了圖層路徑,這樣我們就不用考慮人工釋放CGPath了。圖6.1是代碼運行的結果。雖然還不是很完美,但是總算知道了大意對吧!
清單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為創建圓角視圖提供了一個方法,就是CALayer的cornerRadius屬性(譯者注:其實是在第四章提到的)。雖然使用CAShapeLayer類需要更多的工作,但是它有一個優勢就是可以單獨指定每個角。
我們創建圓角舉行其實就是人工繪制單獨的直線和弧度,但是事實上UIBezierPath有自動繪制圓角矩形的構造方法,下面這段代碼繪制了一個有三個圓角一個直角的矩形:
//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];
我們可以通過這個圖層路徑繪制一個既有直角又有圓角的視圖。如果我們想依照此圖形來剪裁視圖內容,我們可以把CAShapeLayer作為視圖的宿主圖層,而不是添加一個子視圖(圖層蒙板的詳細解釋見第四章『視覺效果』)。
CATextLayer
用戶界面是無法從一個單獨的圖片里面構建的。一個設計良好的圖標能夠很好地表現一個按鈕或控件的意圖,不過你遲早都要需要一個不錯的老式風格的文本標簽。
如果你想在一個圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內容(這就是UILabel的精髓)。如果越過寄宿于圖層的視圖,直接在圖層上操作,那其實相當繁瑣。你要為每一個顯示文字的圖層創建一個能像圖層代理一樣工作的類,還要邏輯上判斷哪個圖層需要顯示哪個字符串,更別提還要記錄不同的字體,顏色等一系列亂七八糟的東西。
萬幸的是這些都是不必要的,Core Animation提供了一個CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎所有的繪制特性,并且額外提供了一些新的特性。
同樣,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其實是通過WebKit來實現繪制的,這樣就造成了當有很多文字的時候就會有極大的性能壓力。而CATextLayer使用了Core text,并且渲染得非???。
讓我們來嘗試用CATextLayer來顯示一些文字。清單6.2的代碼實現了這一功能,結果如圖6.2所示。
清單6.2 用CATextLayer來實現一個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來顯示一個純文本標簽
如果你自習看這個文本,你會發現一個奇怪的地方:這些文本有一些像素化了。這是因為并沒有以Retina的方式渲染,第二章提到了這個contentScale屬性,用來決定圖層內容應該以怎樣的分辨率來渲染。contentsScale并不關心屏幕的拉伸因素而總是默認為1.0。如果我們想以Retina的質量來顯示文字,我們就得手動地設置CATextLayer的contentsScale屬性,如下:
textLayer.contentsScale?=?[UIScreen?mainScreen].scale;
這樣就解決了這個問題(如圖6.3)
圖6.3 設置contentsScale來匹配屏幕
CATextLayer的font屬性不是一個UIFont類型,而是一個CFTypeRef類型。這樣可以根據你的具體需要來決定字體屬性應該是用CGFontRef類型還是CTFontRef類型(Core Text字體)。同時字體大小也是用fontSize屬性單獨設置的,因為CTFontRef和CGFontRef并不像UIFont一樣包含點大小。這個例子會告訴你如何將UIFont轉換成CGFontRef。
另外,CATextLayer的string屬性并不是你想象的NSString類型,而是id類型。這樣你既可以用NSString也可以用NSAttributedString來指定文本了(注意,NSAttributedString并不是NSString的子類)。屬性化字符串是iOS用來渲染字體風格的機制,它以特定的方式來決定指定范圍內的字符串的原始信息,比如字體,顏色,字重,斜體等。
富文本
iOS 6中,Apple給UILabel和其他UIKit文本視圖添加了直接的屬性化字符串的支持,應該說這是一個很方便的特性。不過事實上從iOS3.2開始CATextLayer就已經支持屬性化字符串了。這樣的話,如果你想要支持更低版本的iOS系統,CATextLayer無疑是你向界面中增加富文本的好辦法,而且也不用去跟復雜的Core Text打交道,也省了用UIWebView的麻煩。
讓我們編輯一下示例使用到NSAttributedString(見清單6.3).iOS 6及以上我們可以用新的NSTextAttributeName實例來設置我們的字符串屬性,但是練習的目的是為了演示在iOS 5及以下,所以我們用了Core Text,也就是說你需要把Core Text framework添加到你的項目中。否則,編譯器是無法識別屬性常量的。
圖6.4是代碼運行結果(注意那個紅色的下劃線文本)
清單6.3 用NSAttributedString實現一個富文本標簽。
#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實現一個富文本標簽。
行距和字距
有必要提一下的是,由于繪制的實現機制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。
二者的差異程度(由使用的字體和字符決定)總的來說挺小,但是如果你想正確的顯示普通便簽和CATextLayer就一定要記住這一點。
UILabel的替代品
我們已經證實了CATextLayer比UILabel有著更好的性能表現,同時還有額外的布局選項并且在iOS 5上支持富文本。但是與一般的標簽比較而言會更加繁瑣一些。如果我們真的在需求一個UILabel的可用替代品,最好是能夠在Interface Builder上創建我們的標簽,而且盡可能地像一般的視圖一樣正常工作。
我們應該繼承UILabel,然后添加一個子圖層CATextLayer并重寫顯示文本的方法。但是仍然會有由UILabel的-drawRect:方法創建的空寄宿圖。而且由于CALayer不支持自動縮放和自動布局,子視圖并不是主動跟蹤視圖邊界的大小,所以每次視圖大小被更改,我們不得不手動更新子圖層的邊界。
我們真正想要的是一個用CATextLayer作為宿主圖層的UILabel子類,這樣就可以隨著視圖自動調整大小而且也沒有冗余的寄宿圖啦。
就像我們在第一章『圖層樹』討論的一樣,每一個UIView都是寄宿在一個CALayer的示例上。這個圖層是由視圖自動創建和管理的,那我們可以用別的圖層類型替代它么?一旦被創建,我們就無法代替這個圖層了。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創建的時候能返回一個不同的圖層子類。UIView會在初始化的時候調用+layerClass方法,然后用它的返回類型來創建宿主圖層。
清單6.4 演示了一個UILabel子類LayerLabel用CATextLayer繪制它的問題,而不是調用一般的UILabel使用的較慢的-drawRect:方法。LayerLabel示例既可以用代碼實現,也可以在Interface Builder實現,只要把普通的標簽拖入視圖之中,然后設置它的類是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
如果你運行代碼,你會發現文本并沒有像素化,而我們也沒有設置contentsScale屬性。把CATextLayer作為宿主圖層的另一好處就是視圖自動設置了contentsScale屬性。
在這個簡單的例子中,我們只是實現了UILabel的一部分風格和布局屬性,不過稍微再改進一下我們就可以創建一個支持UILabel所有功能甚至更多功能的LayerLabel類(你可以在一些線上的開源項目中找到)。
如果你打算支持iOS 6及以上,基于CATextLayer的標簽可能就有有些局限性。但是總得來說,如果想在app里面充分利用CALayer子類,用+layerClass來創建基于不同圖層的視圖是一個簡單可復用的方法。
CATransformLayer
當我們在構造復雜的3D事物的時候,如果能夠組織獨立元素就太方便了。比如說,你想創造一個孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。
當然是允許獨立地移動每個區域的啦。以肘為指點會移動前臂和手,而不是肩膀。Core Animation圖層很容易就可以讓你在2D環境下做出這樣的層級體系下的變換,但是3D情況下就不太可能,因為所有的圖層都把他的孩子都平面化到一個場景中(第五章『變換』有提到)。
CATransformLayer解決了這個問題,CATransformLayer不同于普通的CALayer,因為它不能顯示它自己的內容。只有當存在了一個能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層,所以它能夠用于構造一個層級的3D結構,比如我的手臂示例。
用代碼創建一個手臂需要相當多的代碼,所以我就演示得更簡單一些吧:在第五章的立方體示例,我們將通過旋轉camara來解決圖層平面化問題而不是像立方體示例代碼中用的sublayerTransform。這是一個非常不錯的技巧,但是只能作用域單個對象上,如果你的場景包含兩個立方體,那我們就不能用這個技巧單獨旋轉他們了。
那么,就讓我們來試一試CATransformLayer吧,第一個問題就來了:在第五章,我們是用多個視圖來構造了我們的立方體,而不是單獨的圖層。我們不能在不打亂已有的視圖層次的前提下在一個本身不是有寄宿圖的圖層中放置一個寄宿圖圖層。我們可以創建一個新的UIView子類寄宿在CATransformLayer(用+layerClass方法)之上。但是,為了簡化案例,我們僅僅重建了一個單獨的圖層,而不是使用視圖。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標簽,不過我們現在也用不到這個特性。
清單6.5就是代碼。我們以我們在第五章使用過的相同基本邏輯放置立方體。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個CATransformLayer中創建一個獨立的立方體對象,然后將兩個這樣的立方體放進容器中。我們隨機地給立方面染色以將他們區分開來,這樣就不用靠標簽或是光亮來區分他們。圖6.5是運行結果。
清單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復制一個CAGradientLayer并將內容繪制到一個普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。
基礎漸變
我們將從一個簡單的紅變藍的對角線漸變開始(見清單6.6).這些漸變色彩放在一個數組中,并賦給colors屬性。這個數組成員接受CGColorRef類型的值(并不是從NSObject派生而來),所以我們要用通過bridge轉換以確保編譯正常。
CAGradientLayer也有startPoint和endPoint屬性,他們決定了漸變的方向。這兩個參數是以單位坐標系進行的定義,所以左上角坐標是{0, 0},右下角坐標是{1, 1}。代碼運行結果如圖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實現簡單的兩種顏色的對角線漸變
多重漸變
如果你愿意,colors屬性可以包含很多顏色,所以創建一個彩虹一樣的多重漸變也是很簡單的。默認情況下,這些顏色在空間上均勻地被渲染,但是我們可以用locations屬性來調整空間。locations屬性是一個浮點數值的數組(以NSNumber包裝)。這些浮點數定義了colors屬性中每個不同顏色的位置,同樣的,也是以單位坐標系進行標定。0.0代表著漸變的開始,1.0代表著結束。
locations數組并不是強制要求的,但是如果你給它賦值了就一定要確保locations的數組大小和colors數組大小一定要相同,否則你將會得到一個空白的漸變。
清單6.7展示了一個基于清單6.6的對角線漸變的代碼改造?,F在變成了從紅到黃最后到綠色的漸變。locations數組指定了0.0,0.25和0.5三個數值,這樣這三個漸變就有點像擠在了左上角。(如圖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構造偏移至左上角的三色漸變
CAReplicatorLayer
CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會繪制一個或多個圖層的子圖層,并在每個復制體上應用不同的變換??瓷先パ菔灸軌蚋咏忉屵@些,我們來寫個例子吧。
重復圖層(Repeating Layers)
清單6.8中,我們在屏幕的中間創建了一個小白色方塊圖層,然后用CAReplicatorLayer生成十個圖層組成一個圓圈。instanceCount屬性指定了圖層需要重復多少次。instanceTransform指定了一個CATransform3D3D變換(這種情況下,下一圖層的位移和旋轉將會移動到圓圈的下一個點)。
變換是逐步增加的,每個實例都是相對于前一實例布局。這就是為什么這些復制體最終不會出現在同意位置上,圖6.8是代碼運行結果。
清單6.8 用CAReplicatorLayer重復圖層
@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創建一圈圖層
注意到當圖層在重復的時候,他們的顏色也在變化:這是用instanceBlueOffset和instanceGreenOffset屬性實現的。通過逐步減少藍色和綠色通道,我們逐漸將圖層顏色轉換成了紅色。這個復制效果看起來很酷,但是CAReplicatorLayer真正應用到實際程序上的場景比如:一個游戲中導彈的軌跡云,或者粒子爆炸(盡管iOS 5已經引入了CAEmitterLayer,它更適合創建任意的粒子效果)。除此之外,還有一個實際應用是:反射。
反射
使用CAReplicatorLayer并應用一個負比例變換于一個復制圖層,你就可以創建指定視圖(或整個視圖層次)內容的鏡像圖片,這樣就創建了一個實時的『反射』效果。讓我們來嘗試實現這個創意:指定一個繼承于UIView的ReflectionView,它會自動產生內容的反射效果。實現這個效果的代碼很簡單(見清單6.9),實際上用ReflectionView實現這個效果會更簡單,我們只需要把ReflectionView的實例放置于Interface Builder(見圖6.9),它就會實時生成子視圖的反射,而不需要別的代碼(見圖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自動實時產生反射效果。
開源代碼ReflectionView完成了一個自適應的漸變淡出效果(用CAGradientLayer和圖層蒙板實現),代碼見 https://github.com/nicklockwood/ReflectionView
CAScrollLayer
對于一個未轉換的圖層,它的bounds和它的frame是一樣的,frame屬性是由bounds屬性自動計算而出的,所以更改任意一個值都會更新其他值。
但是如果你只想顯示一個大圖層里面的一小部分呢。比如說,你可能有一個很大的圖片,你希望用戶能夠隨意滑動,或者是一個數據或文本的長列表。在一個典型的iOS應用中,你可能會用到UITableView或是UIScrollView,但是對于獨立的圖層來說,什么會等價于剛剛提到的UITableView和UIScrollView呢?
在第二章中,我們探索了圖層的contentsRect屬性的用法,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個非常好的解決方案,因為,這樣做的話每次你想『滑動』可視區域的時候,你就需要手工重新計算并更新所有的子圖層位置。
這個時候就需要CAScrollLayer了。CAScrollLayer有一個-scrollToPoint:方法,它自動適應bounds的原點以便圖層內容出現在滑動的地方。注意,這就是它做的所有事情。前面提到過,Core Animation并不處理用戶輸入,所以CAScrollLayer并不負責將觸摸事件轉換為滑動事件,既不渲染滾動條,也不實現任何iOS指定行為例如滑動反彈(當視圖滑動超多了它的邊界的將會反彈回正確的地方)。
讓我們來用CAScrollLayer來常見一個基本的UIScrollView替代品。我們將會用CAScrollLayer作為視圖的宿主圖層,并創建一個自定義的UIView,然后用UIPanGestureRecognizer實現觸摸事件響應。這段代碼見清單6.10. 圖6.11是運行效果:ScrollView顯示了一個大于它的frame的UIImageView。
清單6.10 用CAScrollLayer實現滑動視圖
#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創建一個湊合的滑動視圖
不同于UIScrollView,我們定制的滑動視圖類并沒有實現任何形式的邊界檢查(bounds checking)。圖層內容極有可能滑出視圖的邊界并無限滑下去。CAScrollLayer并沒有等同于UIScrollView中contentSize的屬性,所以當CAScrollLayer滑動的時候完全沒有一個全局的可滑動區域的概念,也無法自適應它的邊界原點至你指定的值。它之所以不能自適應邊界大小是因為它不需要,內容完全可以超過邊界。
那你一定會奇怪用CAScrollLayer的意義到底何在,因為你可以簡單地用一個普通的CALayer然后手動適應邊界原點啊。真相其實并不復雜,UIScrollView并沒有用CAScrollLayer,事實上,就是簡單的通過直接操作圖層邊界來實現滑動。
CAScrollLayer有一個潛在的有用特性。如果你查看CAScrollLayer的頭文件,你就會注意到有一個擴展分類實現了一些方法和屬性:
-?(void)scrollPoint:(CGPoint)p;
-?(void)scrollRectToVisible:(CGRect)r;
@property(readonly)?CGRect?visibleRect;
看到這些方法和屬性名,你也許會以為這些方法給每個CALayer實例增加了滑動功能。但是事實上他們只是放置在CAScrollLayer中的圖層的實用方法。scrollPoint:方法從圖層樹中查找并找到第一個可用的CAScrollLayer,然后滑動它使得指定點成為可視的。scrollRectToVisible:方法實現了同樣的事情只不過是作用在一個矩形上的。visibleRect屬性決定圖層(如果存在的話)的哪部分是當前的可視區域。如果你自己實現這些方法就會相對容易明白一點,但是CAScrollLayer幫你省了這些麻煩,所以當涉及到實現圖層滑動的時候就可以用上了。