>復雜的組織都是專門化的
>Catharine R. Stimpson
到目前為止腐魂,我們已經(jīng)探討過`CALayer`類了蹬屹,同時我們也了解到了一些非常有用的繪圖和動畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已空镜。本章就會學習其他的一些圖層類粹庞,進一步擴展使用Core Animation繪圖的能力咳焚。
##CAShapeLayer
在第四章『視覺效果』我們學習到了不使用圖片的情況下用`CGPath`去構造任意形狀的陰影。如果我們能用同樣的方式創(chuàng)建相同形狀的圖層就好了庞溜。
`CAShapeLayer`是一個通過矢量圖形而不是bitmap來繪制的圖層子類革半。你指定諸如顏色和線寬等屬性,用`CGPath`來定義想要繪制的圖形流码,最后`CAShapeLayer`就自動渲染出來了又官。當然,你也可以用Core Graphics直接向原始的`CALyer`的內容中繪制一個路徑漫试,相比直下赏胚,使用`CAShapeLayer`有以下一些優(yōu)點:
*渲染快速。`CAShapeLayer`使用了硬件加速商虐,繪制同一圖形會比用Core Graphics快很多觉阅。
*高效使用內存。一個`CAShapeLayer`不需要像普通`CALayer`一樣創(chuàng)建一個寄宿圖形秘车,所以無論有多大典勇,都不會占用太多的內存。
*不會被圖層邊界剪裁掉叮趴。一個`CAShapeLayer`可以在邊界之外繪制割笙。你的圖層路徑不會像在使用Core Graphics的普通`CALayer`一樣被剪裁掉(如我們在第二章所見)。
*不會出現(xiàn)像素化。當你給`CAShapeLayer`做3D變換時伤溉,它不像一個有寄宿圖的普通圖層一樣變得像素化般码。
###創(chuàng)建一個`CGPath`
`CAShapeLayer`可以用來繪制所有能夠通過`CGPath`來表示的形狀。這個形狀不一定要閉合乱顾,圖層路徑也不一定要不可破板祝,事實上你可以在一個圖層上繪制好幾個不同的形狀。你可以控制一些屬性比如`lineWith`(線寬走净,用點表示單位)券时,`lineCap`(線條結尾的樣子),和`lineJoin`(線條之間的結合點的樣子)伏伯;但是在圖層層面你只有一次機會設置這些屬性橘洞。如果你想用不同顏色或風格來繪制多個形狀,就不得不為每個形狀準備一個圖層了说搅。
清單6.1的代碼用一個`CAShapeLayer`渲染一個簡單的火柴人炸枣。`CAShapeLayer`屬性是`CGPathRef`類型,但是我們用`UIBezierPath`幫助類創(chuàng)建了圖層路徑弄唧,這樣我們就不用考慮人工釋放`CGPath`了适肠。圖6.1是代碼運行的結果。雖然還不是很完美套才,但是總算知道了大意對吧迂猴!
清單6.1用`CAShapeLayer`繪制一個火柴人
```objective-c
#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`屬性(譯者注:其實是在第四章提到的)背伴。雖然使用`CAShapeLayer`類需要更多的工作沸毁,但是它有一個優(yōu)勢就是可以單獨指定每個角。
我們創(chuàng)建圓角舉行其實就是人工繪制單獨的直線和弧度傻寂,但是事實上`UIBezierPath`有自動繪制圓角矩形的構造方法息尺,下面這段代碼繪制了一個有三個圓角一個直角的矩形:
```objective-c
//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
用戶界面是無法從一個單獨的圖片里面構建的静檬。一個設計良好的圖標能夠很好地表現(xiàn)一個按鈕或控件的意圖炭懊,不過你遲早都要需要一個不錯的老式風格的文本標簽。
如果你想在一個圖層里面顯示文字拂檩,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內容(這就是UILabel的精髓)侮腹。如果越過寄宿于圖層的視圖,直接在圖層上操作稻励,那其實相當繁瑣父阻。你要為每一個顯示文字的圖層創(chuàng)建一個能像圖層代理一樣工作的類,還要邏輯上判斷哪個圖層需要顯示哪個字符串,更別提還要記錄不同的字體加矛,顏色等一系列亂七八糟的東西履婉。
萬幸的是這些都是不必要的,Core Animation提供了一個`CALayer`的子類`CATextLayer`斟览,它以圖層的形式包含了`UILabel`幾乎所有的繪制特性毁腿,并且額外提供了一些新的特性。
同樣趣惠,`CATextLayer`也要比`UILabel`渲染得快得多狸棍。很少有人知道在iOS 6及之前的版本身害,`UILabel`其實是通過WebKit來實現(xiàn)繪制的味悄,這樣就造成了當有很多文字的時候就會有極大的性能壓力。而`CATextLayer`使用了Core text塌鸯,并且渲染得非呈躺快。
讓我們來嘗試用`CATextLayer`來顯示一些文字丙猬。清單6.2的代碼實現(xiàn)了這一功能涨颜,結果如圖6.2所示。
清單6.2用`CATextLayer`來實現(xiàn)一個`UILabel`
```objective-c
@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`來顯示一個純文本標簽
如果你自習看這個文本茧球,你會發(fā)現(xiàn)一個奇怪的地方:這些文本有一些像素化了庭瑰。這是因為并沒有以Retina的方式渲染,第二章提到了這個`contentScale`屬性抢埋,用來決定圖層內容應該以怎樣的分辨率來渲染弹灭。`contentsScale`并不關心屏幕的拉伸因素而總是默認為1.0。如果我們想以Retina的質量來顯示文字揪垄,我們就得手動地設置`CATextLayer`的`contentsScale`屬性穷吮,如下:
```objective-c
textLayer.contentsScale = [UIScreen mainScreen].scale;
```
這樣就解決了這個問題(如圖6.3)
圖6.3設置`contentsScale`來匹配屏幕
`CATextLayer`的`font`屬性不是一個`UIFont`類型,而是一個`CFTypeRef`類型饥努。這樣可以根據(jù)你的具體需要來決定字體屬性應該是用`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`就已經(jīng)支持屬性化字符串了志衣。這樣的話屯援,如果你想要支持更低版本的iOS系統(tǒng),`CATextLayer`無疑是你向界面中增加富文本的好辦法念脯,而且也不用去跟復雜的Core Text打交道狞洋,也省了用`UIWebView`的麻煩。
讓我們編輯一下示例使用到`NSAttributedString`(見清單6.3).iOS 6及以上我們可以用新的`NSTextAttributeName`實例來設置我們的字符串屬性绿店,但是練習的目的是為了演示在iOS 5及以下吉懊,所以我們用了Core Text,也就是說你需要把Core Text framework添加到你的項目中假勿。否則借嗽,編譯器是無法識別屬性常量的。
圖6.4是代碼運行結果(注意那個紅色的下劃線文本)
清單6.3用NSAttributedString實現(xiàn)一個富文本標簽转培。
```objective-c
#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實現(xiàn)一個富文本標簽恶导。
###行距和字距
有必要提一下的是,由于繪制的實現(xiàn)機制不同(Core Text和WebKit)浸须,用`CATextLayer`渲染和用`UILabel`渲染出的文本行距和字距也不是不盡相同的惨寿。
二者的差異程度(由使用的字體和字符決定)總的來說挺小,但是如果你想正確的顯示普通便簽和`CATextLayer`就一定要記住這一點羽戒。
### `UILabel`的替代品
我們已經(jīng)證實了`CATextLayer`比`UILabel`有著更好的性能表現(xiàn)缤沦,同時還有額外的布局選項并且在iOS 5上支持富文本。但是與一般的標簽比較而言會更加繁瑣一些易稠。如果我們真的在需求一個`UILabel`的可用替代品缸废,最好是能夠在Interface Builder上創(chuàng)建我們的標簽,而且盡可能地像一般的視圖一樣正常工作驶社。
我們應該繼承`UILabel`企量,然后添加一個子圖層`CATextLayer`并重寫顯示文本的方法。但是仍然會有由`UILabel`的`-drawRect:`方法創(chuàng)建的空寄宿圖亡电。而且由于`CALayer`不支持自動縮放和自動布局届巩,子視圖并不是主動跟蹤視圖邊界的大小,所以每次視圖大小被更改份乒,我們不得不手動更新子圖層的邊界恕汇。
我們真正想要的是一個用`CATextLayer`作為宿主圖層的`UILabel`子類腕唧,這樣就可以隨著視圖自動調整大小而且也沒有冗余的寄宿圖啦。
就像我們在第一章『圖層樹』討論的一樣瘾英,每一個`UIView`都是寄宿在一個`CALayer`的示例上枣接。這個圖層是由視圖自動創(chuàng)建和管理的,那我們可以用別的圖層類型替代它么缺谴?一旦被創(chuàng)建但惶,我們就無法代替這個圖層了。但是如果我們繼承了`UIView`湿蛔,那我們就可以重寫`+layerClass`方法使得在創(chuàng)建的時候能返回一個不同的圖層子類膀曾。`UIView`會在初始化的時候調用`+layerClass`方法,然后用它的返回類型來創(chuàng)建宿主圖層阳啥。
清單6.4演示了一個`UILabel`子類`LayerLabel`用`CATextLayer`繪制它的問題添谊,而不是調用一般的`UILabel`使用的較慢的`-drawRect:`方法。`LayerLabel`示例既可以用代碼實現(xiàn)苫纤,也可以在Interface Builder實現(xiàn)碉钠,只要把普通的標簽拖入視圖之中纲缓,然后設置它的類是LayerLabel就可以了卷拘。
清單6.4使用`CATextLayer`的`UILabel`子類:`LayerLabel`
```objective-c
#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
```
如果你運行代碼,你會發(fā)現(xiàn)文本并沒有像素化祝高,而我們也沒有設置`contentsScale`屬性栗弟。把`CATextLayer`作為宿主圖層的另一好處就是視圖自動設置了`contentsScale`屬性。
在這個簡單的例子中工闺,我們只是實現(xiàn)了`UILabel`的一部分風格和布局屬性乍赫,不過稍微再改進一下我們就可以創(chuàng)建一個支持`UILabel`所有功能甚至更多功能的`LayerLabel`類(你可以在一些線上的開源項目中找到)。
如果你打算支持iOS 6及以上陆蟆,基于`CATextLayer`的標簽可能就有有些局限性雷厂。但是總得來說,如果想在app里面充分利用`CALayer`子類叠殷,用`+layerClass`來創(chuàng)建基于不同圖層的視圖是一個簡單可復用的方法改鲫。
### CATransformLayer
當我們在構造復雜的3D事物的時候,如果能夠組織獨立元素就太方便了林束。比如說像棘,你想創(chuàng)造一個孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂壶冒,哪一部分是孩子的肘缕题,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等胖腾。
當然是允許獨立地移動每個區(qū)域的啦烟零。以肘為指點會移動前臂和手瘪松,而不是肩膀。Core Animation圖層很容易就可以讓你在2D環(huán)境下做出這樣的層級體系下的變換锨阿,但是3D情況下就不太可能凉逛,因為所有的圖層都把他的孩子都平面化到一個場景中(第五章『變換』有提到)。
`CATransformLayer`解決了這個問題群井,`CATransformLayer`不同于普通的`CALayer`状飞,因為它不能顯示它自己的內容。只有當存在了一個能作用域子圖層的變換它才真正存在书斜。`CATransformLayer`并不平面化它的子圖層诬辈,所以它能夠用于構造一個層級的3D結構,比如我的手臂示例荐吉。
用代碼創(chuàng)建一個手臂需要相當多的代碼焙糟,所以我就演示得更簡單一些吧:在第五章的立方體示例,我們將通過旋轉`camara`來解決圖層平面化問題而不是像立方體示例代碼中用的`sublayerTransform`样屠。這是一個非常不錯的技巧穿撮,但是只能作用域單個對象上,如果你的場景包含兩個立方體痪欲,那我們就不能用這個技巧單獨旋轉他們了悦穿。
那么,就讓我們來試一試`CATransformLayer`吧业踢,第一個問題就來了:在第五章栗柒,我們是用多個視圖來構造了我們的立方體,而不是單獨的圖層知举。我們不能在不打亂已有的視圖層次的前提下在一個本身不是有寄宿圖的圖層中放置一個寄宿圖圖層瞬沦。我們可以創(chuàng)建一個新的`UIView`子類寄宿在`CATransformLayer`(用`+layerClass`方法)之上。但是雇锡,為了簡化案例逛钻,我們僅僅重建了一個單獨的圖層,而不是使用視圖锰提。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標簽曙痘,不過我們現(xiàn)在也用不到這個特性。
清單6.5就是代碼欲账。我們以我們在第五章使用過的相同基本邏輯放置立方體屡江。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個`CATransformLayer`中創(chuàng)建一個獨立的立方體對象赛不,然后將兩個這樣的立方體放進容器中惩嘉。我們隨機地給立方面染色以將他們區(qū)分開來,這樣就不用靠標簽或是光亮來區(qū)分他們踢故。圖6.5是運行結果文黎。
清單6.5用`CATransformLayer`裝配一個3D圖層體系
```objective-c
@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).這些漸變色彩放在一個數(shù)組中桩蓉,并賦給`colors`屬性。這個數(shù)組成員接受`CGColorRef`類型的值(并不是從`NSObject`派生而來)劳闹,所以我們要用通過bridge轉換以確保編譯正常院究。
`CAGradientLayer`也有`startPoint`和`endPoint`屬性,他們決定了漸變的方向本涕。這兩個參數(shù)是以單位坐標系進行的定義业汰,所以左上角坐標是{0, 0},右下角坐標是{1, 1}菩颖。代碼運行結果如圖6.6
清單6.6簡單的兩種顏色的對角線漸變
```objective-c
@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`實現(xiàn)簡單的兩種顏色的對角線漸變
###多重漸變
如果你愿意样漆,`colors`屬性可以包含很多顏色,所以創(chuàng)建一個彩虹一樣的多重漸變也是很簡單的晦闰。默認情況下放祟,這些顏色在空間上均勻地被渲染,但是我們可以用`locations`屬性來調整空間呻右。`locations`屬性是一個浮點數(shù)值的數(shù)組(以`NSNumber`包裝)跪妥。這些浮點數(shù)定義了`colors`屬性中每個不同顏色的位置,同樣的窿冯,也是以單位坐標系進行標定骗奖。0.0代表著漸變的開始,1.0代表著結束醒串。
`locations`數(shù)組并不是強制要求的,但是如果你給它賦值了就一定要確保`locations`的數(shù)組大小和`colors`數(shù)組大小一定要相同鄙皇,否則你將會得到一個空白的漸變芜赌。
清單6.7展示了一個基于清單6.6的對角線漸變的代碼改造。現(xiàn)在變成了從紅到黃最后到綠色的漸變伴逸。`locations`數(shù)組指定了0.0缠沈,0.25和0.5三個數(shù)值,這樣這三個漸變就有點像擠在了左上角。(如圖6.7).
清單6.7在漸變上使用`locations`
```objective-c
- (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中,我們在屏幕的中間創(chuàng)建了一個小白色方塊圖層肛宋,然后用`CAReplicatorLayer`生成十個圖層組成一個圓圈州藕。`instanceCount`屬性指定了圖層需要重復多少次。`instanceTransform`指定了一個`CATransform3D`3D變換(這種情況下酝陈,下一圖層的位移和旋轉將會移動到圓圈的下一個點)床玻。
變換是逐步增加的,每個實例都是相對于前一實例布局沉帮。這就是為什么這些復制體最終不會出現(xiàn)在同意位置上锈死,圖6.8是代碼運行結果。
清單6.8用`CAReplicatorLayer`重復圖層
```objective-c
@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)建一圈圖層
注意到當圖層在重復的時候穆壕,他們的顏色也在變化:這是用`instanceBlueOffset`和`instanceGreenOffset`屬性實現(xiàn)的馅精。通過逐步減少藍色和綠色通道,我們逐漸將圖層顏色轉換成了紅色粱檀。這個復制效果看起來很酷洲敢,但是`CAReplicatorLayer`真正應用到實際程序上的場景比如:一個游戲中導彈的軌跡云,或者粒子爆炸(盡管iOS 5已經(jīng)引入了`CAEmitterLayer`茄蚯,它更適合創(chuàng)建任意的粒子效果)压彭。除此之外,還有一個實際應用是:反射渗常。
###反射
使用`CAReplicatorLayer`并應用一個負比例變換于一個復制圖層壮不,你就可以創(chuàng)建指定視圖(或整個視圖層次)內容的鏡像圖片,這樣就創(chuàng)建了一個實時的『反射』效果皱碘。讓我們來嘗試實現(xiàn)這個創(chuàng)意:指定一個繼承于`UIView`的`ReflectionView`询一,它會自動產(chǎn)生內容的反射效果。實現(xiàn)這個效果的代碼很簡單(見清單6.9)癌椿,實際上用`ReflectionView`實現(xiàn)這個效果會更簡單健蕊,我們只需要把`ReflectionView`的實例放置于Interface Builder(見圖6.9),它就會實時生成子視圖的反射踢俄,而不需要別的代碼(見圖6.10).
清單6.9用`CAReplicatorLayer`自動繪制反射
```objective-c
#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`自動實時產(chǎn)生反射效果缩功。
開源代碼`ReflectionView`完成了一個自適應的漸變淡出效果(用`CAGradientLayer`和圖層蒙板實現(xiàn)),代碼見https://github.com/nicklockwood/ReflectionView
##CAScrollLayer
對于一個未轉換的圖層都办,它的`bounds`和它的`frame`是一樣的嫡锌,`frame`屬性是由`bounds`屬性自動計算而出的,所以更改任意一個值都會更新其他值琳钉。
但是如果你只想顯示一個大圖層里面的一小部分呢势木。比如說,你可能有一個很大的圖片歌懒,你希望用戶能夠隨意滑動啦桌,或者是一個數(shù)據(jù)或文本的長列表。在一個典型的iOS應用中歼培,你可能會用到`UITableView`或是`UIScrollView`震蒋,但是對于獨立的圖層來說茸塞,什么會等價于剛剛提到的`UITableView`和`UIScrollView`呢?
在第二章中查剖,我們探索了圖層的`contentsRect`屬性的用法钾虐,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個非常好的解決方案笋庄,因為效扫,這樣做的話每次你想『滑動』可視區(qū)域的時候,你就需要手工重新計算并更新所有的子圖層位置直砂。
這個時候就需要`CAScrollLayer`了菌仁。`CAScrollLayer`有一個`-scrollToPoint:`方法,它自動適應`bounds`的原點以便圖層內容出現(xiàn)在滑動的地方静暂。注意济丘,這就是它做的所有事情。前面提到過洽蛀,Core Animation并不處理用戶輸入摹迷,所以`CAScrollLayer`并不負責將觸摸事件轉換為滑動事件,既不渲染滾動條郊供,也不實現(xiàn)任何iOS指定行為例如滑動反彈(當視圖滑動超多了它的邊界的將會反彈回正確的地方)峡碉。
讓我們來用`CAScrollLayer`來常見一個基本的`UIScrollView`替代品。我們將會用`CAScrollLayer`作為視圖的宿主圖層驮审,并創(chuàng)建一個自定義的`UIView`鲫寄,然后用`UIPanGestureRecognizer`實現(xiàn)觸摸事件響應。這段代碼見清單6.10.圖6.11是運行效果:`ScrollView`顯示了一個大于它的`frame`的`UIImageView`疯淫。
清單6.10用`CAScrollLayer`實現(xiàn)滑動視圖
```objective-c
#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`地来,我們定制的滑動視圖類并沒有實現(xiàn)任何形式的邊界檢查(bounds checking)。圖層內容極有可能滑出視圖的邊界并無限滑下去峡竣。`CAScrollLayer`并沒有等同于`UIScrollView`中`contentSize`的屬性靠抑,所以當`CAScrollLayer`滑動的時候完全沒有一個全局的可滑動區(qū)域的概念,也無法自適應它的邊界原點至你指定的值适掰。它之所以不能自適應邊界大小是因為它不需要,內容完全可以超過邊界荠列。
那你一定會奇怪用`CAScrollLayer`的意義到底何在类浪,因為你可以簡單地用一個普通的`CALayer`然后手動適應邊界原點啊。真相其實并不復雜肌似,`UIScrollView`并沒有用`CAScrollLayer`费就,事實上,就是簡單的通過直接操作圖層邊界來實現(xiàn)滑動川队。
`CAScrollLayer`有一個潛在的有用特性力细。如果你查看`CAScrollLayer`的頭文件睬澡,你就會注意到有一個擴展分類實現(xiàn)了一些方法和屬性:
```objective-c
- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;
```
看到這些方法和屬性名,你也許會以為這些方法給每個`CALayer`實例增加了滑動功能眠蚂。但是事實上他們只是放置在`CAScrollLayer`中的圖層的實用方法煞聪。`scrollPoint:`方法從圖層樹中查找并找到第一個可用的`CAScrollLayer`,然后滑動它使得指定點成為可視的逝慧。`scrollRectToVisible:`方法實現(xiàn)了同樣的事情只不過是作用在一個矩形上的昔脯。`visibleRect`屬性決定圖層(如果存在的話)的哪部分是當前的可視區(qū)域。如果你自己實現(xiàn)這些方法就會相對容易明白一點笛臣,但是`CAScrollLayer`幫你省了這些麻煩云稚,所以當涉及到實現(xiàn)圖層滑動的時候就可以用上了。
##CATiledLayer
有些時候你可能需要繪制一個很大的圖片沈堡,常見的例子就是一個高像素的照片或者是地球表面的詳細地圖静陈。iOS應用通暢運行在內存受限的設備上,所以讀取整個圖片到內存中是不明智的诞丽。載入大圖可能會相當?shù)芈ㄓ担切δ憧瓷先ケ容^方便的做法(在主線程調用`UIImage`的`-imageNamed:`方法或者`-imageWithContentsOfFile:`方法)將會阻塞你的用戶界面,至少會引起動畫卡頓現(xiàn)象率拒。
能高效繪制在iOS上的圖片也有一個大小限制崩泡。所有顯示在屏幕上的圖片最終都會被轉化為OpenGL紋理,同時OpenGL有一個最大的紋理尺寸(通常是2048\*2048猬膨,或4096\*4096角撞,這個取決于設備型號)。如果你想在單個紋理中顯示一個比這大的圖勃痴,即便圖片已經(jīng)存在于內存中了谒所,你仍然會遇到很大的性能問題,因為Core Animation強制用CPU處理圖片而不是更快的GPU(見第12章『速度的曲調』沛申,和第13章『高效繪圖』劣领,它更加詳細地解釋了軟件繪制和硬件繪制)。
`CATiledLayer`為載入大圖造成的性能問題提供了一個解決方案:將大圖分解成小片然后將他們單獨按需載入铁材。讓我們用實驗來證明一下尖淘。
###小片裁剪
這個示例中,我們將會從一個2048*2048分辨率的雪人圖片入手著觉。為了能夠從`CATiledLayer`中獲益村生,我們需要把這個圖片裁切成許多小一些的圖片。你可以通過代碼來完成這件事情饼丘,但是如果你在運行時讀入整個圖片并裁切趁桃,那`CATiledLayer`這些所有的性能優(yōu)點就損失殆盡了。理想情況下來說,最好能夠逐個步驟來實現(xiàn)卫病。
清單6.11演示了一個簡單的Mac OS命令行程序油啤,它用`CATiledLayer`將一個圖片裁剪成小圖并存儲到不同的文件中。
清單6.11裁剪圖片成小圖的終端程序
```objective-c
#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;
}
```
這個程序將2048\*2048分辨率的雪人圖案裁剪成了64個不同的256\*256的小圖蟀苛。(256*256是`CATiledLayer`的默認小圖大小益咬,默認大小可以通過`tileSize`屬性更改)。程序接受一個圖片路徑作為命令行的第一個參數(shù)屹逛。我們可以在編譯的scheme將路徑參數(shù)硬編碼然后就可以在Xcode中運行了础废,但是以后作用在另一個圖片上就不方便了。所以罕模,我們編譯了這個程序并把它保存到敏感的地方评腺,然后從終端調用,如下面所示:
```objective-c
> 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:
這個程序相當基礎淑掌,但是能夠輕易地擴展支持額外的參數(shù)比如小圖大小蒿讥,或者導出格式等等。運行結果是64個新圖的序列抛腕,如下面命名:
```
Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg
```
既然我們有了裁切后的小圖芋绸,我們就要讓iOS程序用到他們。`CATiledLayer`很好地和`UIScrollView`集成在一起担敌。除了設置圖層和滑動視圖邊界以適配整個圖片大小摔敛,我們真正要做的就是實現(xiàn)`-drawLayer:inContext:`方法,當需要載入新的小圖時全封,`CATiledLayer`就會調用到這個方法马昙。
清單6.12演示了代碼。圖6.12是代碼運行結果刹悴。
清單6.12一個簡單的滾動`CATiledLayer`實現(xiàn)
```objective-c
#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
```
圖6.12用`UIScrollView`滾動`CATiledLayer`
當你滑動這個圖片行楞,你會發(fā)現(xiàn)當`CATiledLayer`載入小圖的時候,他們會淡入到界面中土匀。這是`CATiledLayer`的默認行為子房。(你可能已經(jīng)在iOS 6之前的蘋果地圖程序中見過這個效果)你可以用`fadeDuration`屬性改變淡入時長或直接禁用掉。`CATiledLayer`(不同于大部分的`UIKit`和Core Animation方法)支持多線程繪制就轧,`-drawLayer:inContext:`方法可以在多個線程中同時地并發(fā)調用证杭,所以請小心謹慎地確保你在這個方法中實現(xiàn)的繪制代碼是線程安全的。
###Retina小圖
你也許已經(jīng)注意到了這些小圖并不是以Retina的分辨率顯示的妒御。為了以屏幕的原生分辨率來渲染`CATiledLayer`躯砰,我們需要設置圖層的`contentsScale`來匹配`UIScreen`的`scale`屬性:
```objective-c
tileLayer.contentsScale = [UIScreen mainScreen].scale;
```
有趣的是,`tileSize`是以像素為單位携丁,而不是點,所以增大了`contentsScale`就自動有了默認的小圖尺寸(現(xiàn)在它是128\*128的點而不是256\*256).所以,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個不同的小圖梦鉴。我們需要做的是適應小圖渲染代碼以對應安排`scale`的變化李茫,然而:
```objective-c
//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設備上(總尺寸是1024\*1024,而不是2048\*2048)肥橙。這個通常都不會影響到用`CATiledLayer`正常顯示的圖片類型(比如照片和地圖魄宏,他們在設計上就是要支持放大縮小,能夠在不同的縮放條件下顯示)存筏,但是也需要在心里明白宠互。
##CAEmitterLayer
在iOS 5中,蘋果引入了一個新的`CALayer`子類叫做`CAEmitterLayer`椭坚。`CAEmitterLayer`是一個高性能的粒子引擎予跌,被用來創(chuàng)建實時例子動畫如:煙霧,火善茎,雨等等這些效果券册。
`CAEmitterLayer`看上去像是許多`CAEmitterCell`的容器,這些`CAEmitierCell`定義了一個例子效果垂涯。你將會為不同的例子效果定義一個或多個`CAEmitterCell`作為模版烁焙,同時`CAEmitterLayer`負責基于這些模版實例化一個粒子流。一個`CAEmitterCell`類似于一個`CALayer`:它有一個`contents`屬性可以定義為一個`CGImage`耕赘,另外還有一些可設置屬性控制著表現(xiàn)和行為骄蝇。我們不會對這些屬性逐一進行詳細的描述,你們可以在`CAEmitterCell`類的頭文件中找到操骡。
我們來舉個例子九火。我們將利用在一圓中發(fā)射不同速度和透明度的粒子創(chuàng)建一個火爆炸的效果。清單6.13包含了生成爆炸的代碼当娱。圖6.13是運行結果
清單6.13用`CAEmitterLayer`創(chuàng)建爆炸效果
```objetive-c
#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`屬性指定了一個可以混合圖片內容顏色的混合色跨细。在示例中鹦倚,我們將它設置為桔色。
*例子某一屬性的變化范圍冀惭。比如`emissionRange`屬性的值是2π震叙,這意味著例子可以從360度任意位置反射出來。如果指定一個小一些的值散休,就可以創(chuàng)造出一個圓錐形
*指定值在時間線上的變化媒楼。比如,在示例中戚丸,我們將`alphaSpeed`設置為-0.4划址,就是說例子的透明度每過一秒就是減少0.4扔嵌,這樣就有發(fā)射出去之后逐漸小時的效果。
`CAEmitterLayer`的屬性它自己控制著整個例子系統(tǒng)的位置和形狀夺颤。一些屬性比如`birthRate`痢缎,`lifetime`和`celocity`,這些屬性在`CAEmitterCell`中也有世澜。這些屬性會以相乘的方式作用在一起独旷,這樣你就可以用一個值來加速或者擴大整個例子系統(tǒng)。其他值得提到的屬性有以下這些:
* `preservesDepth`寥裂,是否將3D例子系統(tǒng)平面化到一個圖層(默認值)或者可以在3D空間中混合其他的圖層
* `renderMode`嵌洼,控制著在視覺上粒子圖片是如何混合的。你可能已經(jīng)注意到了示例中我們把它設置為`kCAEmitterLayerAdditive`封恰,它實現(xiàn)了這樣一個效果:合并例子重疊部分的亮度使得看上去更亮麻养。如果我們把它設置為默認的`kCAEmitterLayerUnordered`,效果就沒那么好看了(見圖6.14).
圖6.14禁止混色之后的火焰粒子
##CAEAGLLayer
當iOS要處理高性能圖形繪制俭驮,必要時就是OpenGL回溺。應該說它應該是最后的殺手锏,至少對于非游戲的應用來說是的混萝。因為相比Core Animation和UIkit框架遗遵,它不可思議地復雜。
OpenGL提供了Core Animation的基礎逸嘀,它是底層的C接口车要,直接和iPhone,iPad的硬件通信崭倘,極少地抽象出來的方法翼岁。OpenGL沒有對象或是圖層的繼承概念。它只是簡單地處理三角形司光。OpenGL中所有東西都是3D空間中有顏色和紋理的三角形琅坡。用起來非常復雜和強大,但是用OpenGL繪制iOS用戶界面就需要很多很多的工作了残家。
為了能夠以高性能使用Core Animation榆俺,你需要判斷你需要繪制哪種內容(矢量圖形,例子坞淮,文本茴晋,等等),但后選擇合適的圖層去呈現(xiàn)這些內容回窘,Core Animation中只有一些類型的內容是被高度優(yōu)化的诺擅;所以如果你想繪制的東西并不能找到標準的圖層類,想要得到高性能就比較費事情了啡直。
因為OpenGL根本不會對你的內容進行假設烁涌,它能夠繪制得相當快苍碟。利用OpenGL,你可以繪制任何你知道必要的集合信息和形狀邏輯的內容烹玉。所以很多游戲都喜歡用OpenGL(這些情況下驰怎,Core Animation的限制就明顯了:它優(yōu)化過的內容類型并不一定能滿足需求),但是這樣依賴二打,方便的高度抽象接口就沒了。
在iOS 5中掂榔,蘋果引入了一個新的框架叫做GLKit继效,它去掉了一些設置OpenGL的復雜性,提供了一個叫做`CLKView`的`UIView`的子類装获,幫你處理大部分的設置和繪制工作瑞信。前提是各種各樣的OpenGL繪圖緩沖的底層可配置項仍然需要你用`CAEAGLLayer`完成,它是`CALayer`的一個子類穴豫,用來顯示任意的OpenGL圖形凡简。
大部分情況下你都不需要手動設置`CAEAGLLayer`(假設用GLKView),過去的日子就不要再提了精肃。特別的秤涩,我們將設置一個OpenGL ES 2.0的上下文,它是現(xiàn)代的iOS設備的標準做法司抱。
盡管不需要GLKit也可以做到這一切筐眷,但是GLKit囊括了很多額外的工作,比如設置頂點和片段著色器习柠,這些都以類C語言叫做GLSL自包含在程序中匀谣,同時在運行時載入到圖形硬件中。編寫GLSL代碼和設置`EAGLayer`沒有什么關系资溃,所以我們將用`GLKBaseEffect`類將著色邏輯抽象出來武翎。其他的事情,我們還是會有以往的方式溶锭。
在開始之前宝恶,你需要將GLKit和OpenGLES框架加入到你的項目中,然后就可以實現(xiàn)清單6.14中的代碼暖途,里面是設置一個`GAEAGLLayer`的最少工作卑惜,它使用了OpenGL ES 2.0的繪圖上下文,并渲染了一個有色三角(見圖6.15).
清單6.14用`CAEAGLLayer`繪制一個三角形
```objective-c
#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
```
圖6.15用OpenGL渲染的`CAEAGLLayer`圖層
在一個真正的OpenGL應用中驻售,我們可能會用`NSTimer`或`CADisplayLink`周期性地每秒鐘調用`-drawRrame`方法60次露久,同時會將幾何圖形生成和繪制分開以便不會每次都重新生成三角形的頂點(這樣也可以讓我們繪制其他的一些東西而不是一個三角形而已),不過上面這個例子已經(jīng)足夠演示了繪圖原則了欺栗。
##AVPlayerLayer
最后一個圖層類型是`AVPlayerLayer`毫痕。盡管它不是Core Animation框架的一部分(AV前綴看上去像)征峦,`AVPlayerLayer`是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結合在一起消请,提供了一個`CALayer`子類來顯示自定義的內容類型栏笆。
`AVPlayerLayer`是用來在iOS上播放視頻的。他是高級接口例如`MPMoivePlayer`的底層實現(xiàn)臊泰,提供了顯示視頻的底層控制蛉加。`AVPlayerLayer`的使用相當簡單:你可以用`+playerLayerWithPlayer:`方法創(chuàng)建一個已經(jīng)綁定了視頻播放器的圖層,或者你可以先創(chuàng)建一個圖層缸逃,然后用`player`屬性綁定一個`AVPlayer`實例针饥。
在我們開始之前,我們需要添加AVFoundation到我們的項目中需频。然后丁眼,清單6.15創(chuàng)建了一個簡單的電影播放器,圖6.16是代碼運行結果昭殉。
清單6.15用`AVPlayerLayer`播放視頻
```objective-c
#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
```
圖6.16用`AVPlayerLayer`圖層播放視頻的截圖
我們用代碼創(chuàng)建了一個`AVPlayerLayer`苞七,但是我們仍然把它添加到了一個容器視圖中,而不是直接在controller中的主視圖上添加挪丢。這樣其實是為了可以使用自動布局限制使得圖層在最中間蹂风;否則,一旦設備被旋轉了我們就要手動重新放置位置吃靠,因為Core Animation并不支持自動大小和自動布局(見第三章『圖層幾何學』)硫眨。
當然,因為`AVPlayerLayer`是`CALayer`的子類巢块,它繼承了父類的所有特性礁阁。我們并不會受限于要在一個矩形中播放視頻;清單6.16演示了在3D族奢,圓角姥闭,有色邊框,蒙板越走,陰影等效果(見圖6.17).
清單6.16給視頻增加變換棚品,邊框和圓角
```objective-c
- (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`
##總結
這一章我們簡要概述了一些專用圖層以及用他們實現(xiàn)的一些效果,我們只是了解到這些圖層的皮毛廊敌,像`CATiledLayer`和`CAEMitterLayer`這些類可以單獨寫一章的铜跑。但是,重點是記住`CALayer`是用處很大的骡澈,而且它并沒有為所有可能的場景進行優(yōu)化锅纺。為了獲得Core Animation最好的性能,你需要為你的工作選對正確的工具肋殴,希望你能夠挖掘這些不同的`CALayer`子類的功能囤锉。
這一章我們通過`CAEmitterLayer`和`AVPlayerLayer`類簡單地接觸到了一些動畫坦弟,在第二章,我們將繼續(xù)深入研究動畫官地,就從隱式動畫開始酿傍。