鍍金池/ 教程/ iOS/ 專用圖層
圖層的樹狀結(jié)構(gòu)
視覺(jué)效果
圖像IO
寄宿圖
緩沖
隱式動(dòng)畫
圖層幾何學(xué)
圖層時(shí)間
顯式動(dòng)畫
變換
性能調(diào)優(yōu)
專用圖層
高效繪圖
圖層性能
基于定時(shí)器的動(dòng)畫

專用圖層

專用圖層

復(fù)雜的組織都是專門化的

Catharine R. Stimpson

到目前為止,我們已經(jīng)探討過(guò)CALayer類了,同時(shí)我們也了解到了一些非常有用的繪圖和動(dòng)畫功能。但是Core Animation圖層不僅僅能作用于圖片和顏色而已。本章就會(huì)學(xué)習(xí)其他的一些圖層類,進(jìn)一步擴(kuò)展使用Core Animation繪圖的能力。

CAShapeLayer

在第四章『視覺(jué)效果』我們學(xué)習(xí)到了不使用圖片的情況下用CGPath去構(gòu)造任意形狀的陰影。如果我們能用同樣的方式創(chuàng)建相同形狀的圖層就好了。

CAShapeLayer是一個(gè)通過(guò)矢量圖形而不是bitmap來(lái)繪制的圖層子類。你指定諸如顏色和線寬等屬性,用CGPath來(lái)定義想要繪制的圖形,最后CAShapeLayer就自動(dòng)渲染出來(lái)了。當(dāng)然,你也可以用Core Graphics直接向原始的CALyer的內(nèi)容中繪制一個(gè)路徑,相比直下,使用CAShapeLayer有以下一些優(yōu)點(diǎn):

  • 渲染快速。CAShapeLayer使用了硬件加速,繪制同一圖形會(huì)比用Core Graphics快很多。
  • 高效使用內(nèi)存。一個(gè)CAShapeLayer不需要像普通CALayer一樣創(chuàng)建一個(gè)寄宿圖形,所以無(wú)論有多大,都不會(huì)占用太多的內(nèi)存。
  • 不會(huì)被圖層邊界剪裁掉。一個(gè)CAShapeLayer可以在邊界之外繪制。你的圖層路徑不會(huì)像在使用Core Graphics的普通CALayer一樣被剪裁掉(如我們?cè)诘诙滤?jiàn))。
  • 不會(huì)出現(xiàn)像素化。當(dāng)你給CAShapeLayer做3D變換時(shí),它不像一個(gè)有寄宿圖的普通圖層一樣變得像素化。

創(chuàng)建一個(gè)CGPath

CAShapeLayer可以用來(lái)繪制所有能夠通過(guò)CGPath來(lái)表示的形狀。這個(gè)形狀不一定要閉合,圖層路徑也不一定要不可破,事實(shí)上你可以在一個(gè)圖層上繪制好幾個(gè)不同的形狀。你可以控制一些屬性比如lineWith(線寬,用點(diǎn)表示單位),lineCap(線條結(jié)尾的樣子),和lineJoin(線條之間的結(jié)合點(diǎn)的樣子);但是在圖層層面你只有一次機(jī)會(huì)設(shè)置這些屬性。如果你想用不同顏色或風(fēng)格來(lái)繪制多個(gè)形狀,就不得不為每個(gè)形狀準(zhǔn)備一個(gè)圖層了。

清單6.1 的代碼用一個(gè)CAShapeLayer渲染一個(gè)簡(jiǎn)單的火柴人。CAShapeLayer屬性是CGPathRef類型,但是我們用UIBezierPath幫助類創(chuàng)建了圖層路徑,這樣我們就不用考慮人工釋放CGPath了。圖6.1是代碼運(yùn)行的結(jié)果。雖然還不是很完美,但是總算知道了大意對(duì)吧!

清單6.1 用CAShapeLayer繪制一個(gè)火柴人

#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
  //create path
  UIBezierPath *path = [[UIBezierPath alloc] init];
  [path moveToPoint:CGPointMake(175, 100)];
  ?
  [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES];
  [path moveToPoint:CGPointMake(150, 125)];
  [path addLineToPoint:CGPointMake(150, 175)];
  [path addLineToPoint:CGPointMake(125, 225)];
  [path moveToPoint:CGPointMake(150, 175)];
  [path addLineToPoint:CGPointMake(175, 225)];
  [path moveToPoint:CGPointMake(100, 150)];
  [path addLineToPoint:CGPointMake(200, 150)];

  //create shape layer
  CAShapeLayer *shapeLayer = [CAShapeLayer layer];
  shapeLayer.strokeColor = [UIColor redColor].CGColor;
  shapeLayer.fillColor = [UIColor clearColor].CGColor;
  shapeLayer.lineWidth = 5;
  shapeLayer.lineJoin = kCALineJoinRound;
  shapeLayer.lineCap = kCALineCapRound;
  shapeLayer.path = path.CGPath;
  //add it to our view
  [self.containerView.layer addSublayer:shapeLayer];
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.1.png" alt="圖6.1" />

圖6.1 用CAShapeLayer繪制一個(gè)簡(jiǎn)單的火柴人

圓角

第二章里面提到了CAShapeLayer為創(chuàng)建圓角視圖提供了一個(gè)方法,就是CALayercornerRadius屬性(譯者注:其實(shí)是在第四章提到的)。雖然使用CAShapeLayer類需要更多的工作,但是它有一個(gè)優(yōu)勢(shì)就是可以單獨(dú)指定每個(gè)角。

我們創(chuàng)建圓角矩形其實(shí)就是人工繪制單獨(dú)的直線和弧度,但是事實(shí)上UIBezierPath有自動(dòng)繪制圓角矩形的構(gòu)造方法,下面這段代碼繪制了一個(gè)有三個(gè)圓角一個(gè)直角的矩形:

//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft;
//create path
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];

我們可以通過(guò)這個(gè)圖層路徑繪制一個(gè)既有直角又有圓角的視圖。如果我們想依照此圖形來(lái)剪裁視圖內(nèi)容,我們可以把CAShapeLayer作為視圖的宿主圖層,而不是添加一個(gè)子視圖(圖層蒙板的詳細(xì)解釋見(jiàn)第四章『視覺(jué)效果』)。

CATextLayer

用戶界面是無(wú)法從一個(gè)單獨(dú)的圖片里面構(gòu)建的。一個(gè)設(shè)計(jì)良好的圖標(biāo)能夠很好地表現(xiàn)一個(gè)按鈕或控件的意圖,不過(guò)你遲早都要需要一個(gè)不錯(cuò)的老式風(fēng)格的文本標(biāo)簽。

如果你想在一個(gè)圖層里面顯示文字,完全可以借助圖層代理直接將字符串使用Core Graphics寫入圖層的內(nèi)容(這就是UILabel的精髓)。如果越過(guò)寄宿于圖層的視圖,直接在圖層上操作,那其實(shí)相當(dāng)繁瑣。你要為每一個(gè)顯示文字的圖層創(chuàng)建一個(gè)能像圖層代理一樣工作的類,還要邏輯上判斷哪個(gè)圖層需要顯示哪個(gè)字符串,更別提還要記錄不同的字體,顏色等一系列亂七八糟的東西。

萬(wàn)幸的是這些都是不必要的,Core Animation提供了一個(gè)CALayer的子類CATextLayer,它以圖層的形式包含了UILabel幾乎所有的繪制特性,并且額外提供了一些新的特性。

同樣,CATextLayer也要比UILabel渲染得快得多。很少有人知道在iOS 6及之前的版本,UILabel其實(shí)是通過(guò)WebKit來(lái)實(shí)現(xiàn)繪制的,這樣就造成了當(dāng)有很多文字的時(shí)候就會(huì)有極大的性能壓力。而CATextLayer使用了Core text,并且渲染得非??臁?/p>

讓我們來(lái)嘗試用CATextLayer來(lái)顯示一些文字。清單6.2的代碼實(shí)現(xiàn)了這一功能,結(jié)果如圖6.2所示。

清單6.2 用CATextLayer來(lái)實(shí)現(xiàn)一個(gè)UILabel

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *labelView;

@end

@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];

  //create a text layer
  CATextLayer *textLayer = [CATextLayer layer];
  textLayer.frame = self.labelView.bounds;
  [self.labelView.layer addSublayer:textLayer];

  //set text attributes
  textLayer.foregroundColor = [UIColor blackColor].CGColor;
  textLayer.alignmentMode = kCAAlignmentJustified;
  textLayer.wrapped = YES;

  //choose a font
  UIFont *font = [UIFont systemFontOfSize:15];

  //set layer font
  CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  CGFontRef fontRef = CGFontCreateWithFontName(fontName);
  textLayer.font = fontRef;
  textLayer.fontSize = font.pointSize;
  CGFontRelease(fontRef);

  //choose some text
  NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";

  //set layer text
  textLayer.string = text;
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.2.png" alt="圖6.2" />

圖6.2 用CATextLayer來(lái)顯示一個(gè)純文本標(biāo)簽

如果你仔細(xì)看這個(gè)文本,你會(huì)發(fā)現(xiàn)一個(gè)奇怪的地方:這些文本有一些像素化了。這是因?yàn)椴](méi)有以Retina的方式渲染,第二章提到了這個(gè)contentScale屬性,用來(lái)決定圖層內(nèi)容應(yīng)該以怎樣的分辨率來(lái)渲染。contentsScale并不關(guān)心屏幕的拉伸因素而總是默認(rèn)為1.0。如果我們想以Retina的質(zhì)量來(lái)顯示文字,我們就得手動(dòng)地設(shè)置CATextLayercontentsScale屬性,如下:

textLayer.contentsScale = [UIScreen mainScreen].scale;

這樣就解決了這個(gè)問(wèn)題(如圖6.3)

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.3.png" alt="圖6.3" />

圖6.3 設(shè)置contentsScale來(lái)匹配屏幕

CATextLayerfont屬性不是一個(gè)UIFont類型,而是一個(gè)CFTypeRef類型。這樣可以根據(jù)你的具體需要來(lái)決定字體屬性應(yīng)該是用CGFontRef類型還是CTFontRef類型(Core Text字體)。同時(shí)字體大小也是用fontSize屬性單獨(dú)設(shè)置的,因?yàn)?code>CTFontRef和CGFontRef并不像UIFont一樣包含點(diǎn)大小。這個(gè)例子會(huì)告訴你如何將UIFont轉(zhuǎn)換成CGFontRef。

另外,CATextLayerstring屬性并不是你想象的NSString類型,而是id類型。這樣你既可以用NSString也可以用NSAttributedString來(lái)指定文本了(注意,NSAttributedString并不是NSString的子類)。屬性化字符串是iOS用來(lái)渲染字體風(fēng)格的機(jī)制,它以特定的方式來(lái)決定指定范圍內(nèi)的字符串的原始信息,比如字體,顏色,字重,斜體等。

富文本

iOS 6中,Apple給UILabel和其他UIKit文本視圖添加了直接的屬性化字符串的支持,應(yīng)該說(shuō)這是一個(gè)很方便的特性。不過(guò)事實(shí)上從iOS3.2開始CATextLayer就已經(jīng)支持屬性化字符串了。這樣的話,如果你想要支持更低版本的iOS系統(tǒng),CATextLayer無(wú)疑是你向界面中增加富文本的好辦法,而且也不用去跟復(fù)雜的Core Text打交道,也省了用UIWebView的麻煩。

讓我們編輯一下示例使用到NSAttributedString(見(jiàn)清單6.3).iOS 6及以上我們可以用新的NSTextAttributeName實(shí)例來(lái)設(shè)置我們的字符串屬性,但是練習(xí)的目的是為了演示在iOS 5及以下,所以我們用了Core Text,也就是說(shuō)你需要把Core Text framework添加到你的項(xiàng)目中。否則,編譯器是無(wú)法識(shí)別屬性常量的。

圖6.4是代碼運(yùn)行結(jié)果(注意那個(gè)紅色的下劃線文本)

清單6.3 用NSAttributedString實(shí)現(xiàn)一個(gè)富文本標(biāo)簽。

#import "DrawingView.h"
#import <QuartzCore/QuartzCore.h>
#import <CoreText/CoreText.h>

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *labelView;

@end

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];

  //create a text layer
  CATextLayer *textLayer = [CATextLayer layer];
  textLayer.frame = self.labelView.bounds;
  textLayer.contentsScale = [UIScreen mainScreen].scale;
  [self.labelView.layer addSublayer:textLayer];

  //set text attributes
  textLayer.alignmentMode = kCAAlignmentJustified;
  textLayer.wrapped = YES;

  //choose a font
  UIFont *font = [UIFont systemFontOfSize:15];

  //choose some text
  NSString *text = @"Lorem ipsum dolor sit amet, consectetur adipiscing \ elit. Quisque massa arcu, eleifend vel varius in, facilisis pulvinar \ leo. Nunc quis nunc at mauris pharetra condimentum ut ac neque. Nunc \ elementum, libero ut porttitor dictum, diam odio congue lacus, vel \ fringilla sapien diam at purus. Etiam suscipit pretium nunc sit amet \ lobortis";
  ?
  //create attributed string
  NSMutableAttributedString *string = nil;
  string = [[NSMutableAttributedString alloc] initWithString:text];

  //convert UIFont to a CTFont
  CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  CGFloat fontSize = font.pointSize;
  CTFontRef fontRef = CTFontCreateWithName(fontName, fontSize, NULL);

  //set text attributes
  NSDictionary *attribs = @{
    (__bridge id)kCTForegroundColorAttributeName:(__bridge id)[UIColor blackColor].CGColor,
    (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
  };

  [string setAttributes:attribs range:NSMakeRange(0, [text length])];
  attribs = @{
    (__bridge id)kCTForegroundColorAttributeName: (__bridge id)[UIColor redColor].CGColor,
    (__bridge id)kCTUnderlineStyleAttributeName: @(kCTUnderlineStyleSingle),
    (__bridge id)kCTFontAttributeName: (__bridge id)fontRef
  };
  [string setAttributes:attribs range:NSMakeRange(6, 5)];

  //release the CTFont we created earlier
  CFRelease(fontRef);

  //set layer text
  textLayer.string = string;
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.4.png" alt="圖6.4" />

圖6.4 用CATextLayer實(shí)現(xiàn)一個(gè)富文本標(biāo)簽。

行距和字距

有必要提一下的是,由于繪制的實(shí)現(xiàn)機(jī)制不同(Core Text和WebKit),用CATextLayer渲染和用UILabel渲染出的文本行距和字距也不是不盡相同的。

二者的差異程度(由使用的字體和字符決定)總的來(lái)說(shuō)挺小,但是如果你想正確的顯示普通便簽和CATextLayer就一定要記住這一點(diǎn)。

UILabel的替代品

我們已經(jīng)證實(shí)了CATextLayerUILabel有著更好的性能表現(xiàn),同時(shí)還有額外的布局選項(xiàng)并且在iOS 5上支持富文本。但是與一般的標(biāo)簽比較而言會(huì)更加繁瑣一些。如果我們真的在需求一個(gè)UILabel的可用替代品,最好是能夠在Interface Builder上創(chuàng)建我們的標(biāo)簽,而且盡可能地像一般的視圖一樣正常工作。

我們應(yīng)該繼承UILabel,然后添加一個(gè)子圖層CATextLayer并重寫顯示文本的方法。但是仍然會(huì)有由UILabel-drawRect:方法創(chuàng)建的空寄宿圖。而且由于CALayer不支持自動(dòng)縮放和自動(dòng)布局,子視圖并不是主動(dòng)跟蹤視圖邊界的大小,所以每次視圖大小被更改,我們不得不手動(dòng)更新子圖層的邊界。

我們真正想要的是一個(gè)用CATextLayer作為宿主圖層的UILabel子類,這樣就可以隨著視圖自動(dòng)調(diào)整大小而且也沒(méi)有冗余的寄宿圖啦。

就像我們?cè)诘谝徽隆簣D層樹』討論的一樣,每一個(gè)UIView都是寄宿在一個(gè)CALayer的示例上。這個(gè)圖層是由視圖自動(dòng)創(chuàng)建和管理的,那我們可以用別的圖層類型替代它么?一旦被創(chuàng)建,我們就無(wú)法代替這個(gè)圖層了。但是如果我們繼承了UIView,那我們就可以重寫+layerClass方法使得在創(chuàng)建的時(shí)候能返回一個(gè)不同的圖層子類。UIView會(huì)在初始化的時(shí)候調(diào)用+layerClass方法,然后用它的返回類型來(lái)創(chuàng)建宿主圖層。

清單6.4 演示了一個(gè)UILabel子類LayerLabelCATextLayer繪制它的問(wèn)題,而不是調(diào)用一般的UILabel使用的較慢的-drawRect:方法。LayerLabel示例既可以用代碼實(shí)現(xiàn),也可以在Interface Builder實(shí)現(xiàn),只要把普通的標(biāo)簽拖入視圖之中,然后設(shè)置它的類是LayerLabel就可以了。

清單6.4 使用CATextLayerUILabel子類:LayerLabel

#import "LayerLabel.h"
#import <QuartzCore/QuartzCore.h>

@implementation LayerLabel
+ (Class)layerClass
{
  //this makes our label create a CATextLayer //instead of a regular CALayer for its backing layer
  return [CATextLayer class];
}

- (CATextLayer *)textLayer
{
  return (CATextLayer *)self.layer;
}

- (void)setUp
{
  //set defaults from UILabel settings
  self.text = self.text;
  self.textColor = self.textColor;
  self.font = self.font;

  //we should really derive these from the UILabel settings too
  //but that's complicated, so for now we'll just hard-code them
  [self textLayer].alignmentMode = kCAAlignmentJustified;
  ?
  [self textLayer].wrapped = YES;
  [self.layer display];
}

- (id)initWithFrame:(CGRect)frame
{
  //called when creating label programmatically
  if (self = [super initWithFrame:frame]) {
    [self setUp];
  }
  return self;
}

- (void)awakeFromNib
{
  //called when creating label using Interface Builder
  [self setUp];
}

- (void)setText:(NSString *)text
{
  super.text = text;
  //set layer text
  [self textLayer].string = text;
}

- (void)setTextColor:(UIColor *)textColor
{
  super.textColor = textColor;
  //set layer text color
  [self textLayer].foregroundColor = textColor.CGColor;
}

- (void)setFont:(UIFont *)font
{
  super.font = font;
  //set layer font
  CFStringRef fontName = (__bridge CFStringRef)font.fontName;
  CGFontRef fontRef = CGFontCreateWithFontName(fontName);
  [self textLayer].font = fontRef;
  [self textLayer].fontSize = font.pointSize;
  ?
  CGFontRelease(fontRef);
}
@end

如果你運(yùn)行代碼,你會(huì)發(fā)現(xiàn)文本并沒(méi)有像素化,而我們也沒(méi)有設(shè)置contentsScale屬性。把CATextLayer作為宿主圖層的另一好處就是視圖自動(dòng)設(shè)置了contentsScale屬性。

在這個(gè)簡(jiǎn)單的例子中,我們只是實(shí)現(xiàn)了UILabel的一部分風(fēng)格和布局屬性,不過(guò)稍微再改進(jìn)一下我們就可以創(chuàng)建一個(gè)支持UILabel所有功能甚至更多功能的LayerLabel類(你可以在一些線上的開源項(xiàng)目中找到)。

如果你打算支持iOS 6及以上,基于CATextLayer的標(biāo)簽可能就有有些局限性。但是總得來(lái)說(shuō),如果想在app里面充分利用CALayer子類,用+layerClass來(lái)創(chuàng)建基于不同圖層的視圖是一個(gè)簡(jiǎn)單可復(fù)用的方法。

CATransformLayer

當(dāng)我們?cè)跇?gòu)造復(fù)雜的3D事物的時(shí)候,如果能夠組織獨(dú)立元素就太方便了。比如說(shuō),你想創(chuàng)造一個(gè)孩子的手臂:你就需要確定哪一部分是孩子的手腕,哪一部分是孩子的前臂,哪一部分是孩子的肘,哪一部分是孩子的上臂,哪一部分是孩子的肩膀等等。

當(dāng)然是允許獨(dú)立地移動(dòng)每個(gè)區(qū)域的啦。以肘為指點(diǎn)會(huì)移動(dòng)前臂和手,而不是肩膀。Core Animation圖層很容易就可以讓你在2D環(huán)境下做出這樣的層級(jí)體系下的變換,但是3D情況下就不太可能,因?yàn)樗械膱D層都把他的孩子都平面化到一個(gè)場(chǎng)景中(第五章『變換』有提到)。

CATransformLayer解決了這個(gè)問(wèn)題,CATransformLayer不同于普通的CALayer,因?yàn)樗荒茱@示它自己的內(nèi)容。只有當(dāng)存在了一個(gè)能作用域子圖層的變換它才真正存在。CATransformLayer并不平面化它的子圖層,所以它能夠用于構(gòu)造一個(gè)層級(jí)的3D結(jié)構(gòu),比如我的手臂示例。

用代碼創(chuàng)建一個(gè)手臂需要相當(dāng)多的代碼,所以我就演示得更簡(jiǎn)單一些吧:在第五章的立方體示例,我們將通過(guò)旋轉(zhuǎn)camara來(lái)解決圖層平面化問(wèn)題而不是像立方體示例代碼中用的sublayerTransform。這是一個(gè)非常不錯(cuò)的技巧,但是只能作用域單個(gè)對(duì)象上,如果你的場(chǎng)景包含兩個(gè)立方體,那我們就不能用這個(gè)技巧單獨(dú)旋轉(zhuǎn)他們了。

那么,就讓我們來(lái)試一試CATransformLayer吧,第一個(gè)問(wèn)題就來(lái)了:在第五章,我們是用多個(gè)視圖來(lái)構(gòu)造了我們的立方體,而不是單獨(dú)的圖層。我們不能在不打亂已有的視圖層次的前提下在一個(gè)本身不是有寄宿圖的圖層中放置一個(gè)寄宿圖圖層。我們可以創(chuàng)建一個(gè)新的UIView子類寄宿在CATransformLayer(用+layerClass方法)之上。但是,為了簡(jiǎn)化案例,我們僅僅重建了一個(gè)單獨(dú)的圖層,而不是使用視圖。這意味著我們不能像第五章一樣在立方體表面顯示按鈕和標(biāo)簽,不過(guò)我們現(xiàn)在也用不到這個(gè)特性。

清單6.5就是代碼。我們以我們?cè)诘谖逭率褂眠^(guò)的相同基本邏輯放置立方體。但是并不像以前那樣直接將立方面添加到容器視圖的宿主圖層,我們將他們放置到一個(gè)CATransformLayer中創(chuàng)建一個(gè)獨(dú)立的立方體對(duì)象,然后將兩個(gè)這樣的立方體放進(jìn)容器中。我們隨機(jī)地給立方面染色以將他們區(qū)分開來(lái),這樣就不用靠標(biāo)簽或是光亮來(lái)區(qū)分他們。圖6.5是運(yùn)行結(jié)果。

清單6.5 用CATransformLayer裝配一個(gè)3D圖層體系

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (CALayer *)faceWithTransform:(CATransform3D)transform
{
  //create cube face layer
  CALayer *face = [CALayer layer];
  face.frame = CGRectMake(-50, -50, 100, 100);

  //apply a random color
  CGFloat red = (rand() / (double)INT_MAX);
  CGFloat green = (rand() / (double)INT_MAX);
  CGFloat blue = (rand() / (double)INT_MAX);
  face.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;

  ?//apply the transform and return
  face.transform = transform;
  return face;
}

- (CALayer *)cubeWithTransform:(CATransform3D)transform
{
  //create cube layer
  CATransformLayer *cube = [CATransformLayer layer];

  //add cube face 1
  CATransform3D ct = CATransform3DMakeTranslation(0, 0, 50);
  [cube addSublayer:[self faceWithTransform:ct]];

  //add cube face 2
  ct = CATransform3DMakeTranslation(50, 0, 0);
  ct = CATransform3DRotate(ct, M_PI_2, 0, 1, 0);
  [cube addSublayer:[self faceWithTransform:ct]];

  //add cube face 3
  ct = CATransform3DMakeTranslation(0, -50, 0);
  ct = CATransform3DRotate(ct, M_PI_2, 1, 0, 0);
  [cube addSublayer:[self faceWithTransform:ct]];

  //add cube face 4
  ct = CATransform3DMakeTranslation(0, 50, 0);
  ct = CATransform3DRotate(ct, -M_PI_2, 1, 0, 0);
  [cube addSublayer:[self faceWithTransform:ct]];

  //add cube face 5
  ct = CATransform3DMakeTranslation(-50, 0, 0);
  ct = CATransform3DRotate(ct, -M_PI_2, 0, 1, 0);
  [cube addSublayer:[self faceWithTransform:ct]];

  //add cube face 6
  ct = CATransform3DMakeTranslation(0, 0, -50);
  ct = CATransform3DRotate(ct, M_PI, 0, 1, 0);
  [cube addSublayer:[self faceWithTransform:ct]];

  //center the cube layer within the container
  CGSize containerSize = self.containerView.bounds.size;
  cube.position = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0);

  //apply the transform and return
  cube.transform = transform;
  return cube;
}

- (void)viewDidLoad
{?
  [super viewDidLoad];

  //set up the perspective transform
  CATransform3D pt = CATransform3DIdentity;
  pt.m34 = -1.0 / 500.0;
  self.containerView.layer.sublayerTransform = pt;

  //set up the transform for cube 1 and add it
  CATransform3D c1t = CATransform3DIdentity;
  c1t = CATransform3DTranslate(c1t, -100, 0, 0);
  CALayer *cube1 = [self cubeWithTransform:c1t];
  [self.containerView.layer addSublayer:cube1];

  //set up the transform for cube 2 and add it
  CATransform3D c2t = CATransform3DIdentity;
  c2t = CATransform3DTranslate(c2t, 100, 0, 0);
  c2t = CATransform3DRotate(c2t, -M_PI_4, 1, 0, 0);
  c2t = CATransform3DRotate(c2t, -M_PI_4, 0, 1, 0);
  CALayer *cube2 = [self cubeWithTransform:c2t];
  [self.containerView.layer addSublayer:cube2];
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.5.png" alt="圖6.5" />

圖6.5 同一視角下的倆不同變換的立方體

CAGradientLayer

CAGradientLayer是用來(lái)生成兩種或更多顏色平滑漸變的。用Core Graphics復(fù)制一個(gè)CAGradientLayer并將內(nèi)容繪制到一個(gè)普通圖層的寄宿圖也是有可能的,但是CAGradientLayer的真正好處在于繪制使用了硬件加速。

基礎(chǔ)漸變

我們將從一個(gè)簡(jiǎn)單的紅變藍(lán)的對(duì)角線漸變開始(見(jiàn)清單6.6).這些漸變色彩放在一個(gè)數(shù)組中,并賦給colors屬性。這個(gè)數(shù)組成員接受CGColorRef類型的值(并不是從NSObject派生而來(lái)),所以我們要用通過(guò)bridge轉(zhuǎn)換以確保編譯正常。

CAGradientLayer也有startPointendPoint屬性,他們決定了漸變的方向。這兩個(gè)參數(shù)是以單位坐標(biāo)系進(jìn)行的定義,所以左上角坐標(biāo)是{0, 0},右下角坐標(biāo)是{1, 1}。代碼運(yùn)行結(jié)果如圖6.6

清單6.6 簡(jiǎn)單的兩種顏色的對(duì)角線漸變

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController

- (void)viewDidLoad
{
  [super viewDidLoad];
  //create gradient layer and add it to our container view
  CAGradientLayer *gradientLayer = [CAGradientLayer layer];
  gradientLayer.frame = self.containerView.bounds;
  [self.containerView.layer addSublayer:gradientLayer];

  //set gradient colors
  gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor];

  //set gradient start and end points
  gradientLayer.startPoint = CGPointMake(0, 0);
  gradientLayer.endPoint = CGPointMake(1, 1);
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.6.png" alt="圖6.6" />

圖6.6 用CAGradientLayer實(shí)現(xiàn)簡(jiǎn)單的兩種顏色的對(duì)角線漸變

多重漸變

如果你愿意,colors屬性可以包含很多顏色,所以創(chuàng)建一個(gè)彩虹一樣的多重漸變也是很簡(jiǎn)單的。默認(rèn)情況下,這些顏色在空間上均勻地被渲染,但是我們可以用locations屬性來(lái)調(diào)整空間。locations屬性是一個(gè)浮點(diǎn)數(shù)值的數(shù)組(以NSNumber包裝)。這些浮點(diǎn)數(shù)定義了colors屬性中每個(gè)不同顏色的位置,同樣的,也是以單位坐標(biāo)系進(jìn)行標(biāo)定。0.0代表著漸變的開始,1.0代表著結(jié)束。

locations數(shù)組并不是強(qiáng)制要求的,但是如果你給它賦值了就一定要確保locations的數(shù)組大小和colors數(shù)組大小一定要相同,否則你將會(huì)得到一個(gè)空白的漸變。

清單6.7展示了一個(gè)基于清單6.6的對(duì)角線漸變的代碼改造?,F(xiàn)在變成了從紅到黃最后到綠色的漸變。locations數(shù)組指定了0.0,0.25和0.5三個(gè)數(shù)值,這樣這三個(gè)漸變就有點(diǎn)像擠在了左上角。(如圖6.7).

清單6.7 在漸變上使用locations

- (void)viewDidLoad {
    [super viewDidLoad];

    //create gradient layer and add it to our container view
    CAGradientLayer *gradientLayer = [CAGradientLayer layer];
    gradientLayer.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:gradientLayer];

    //set gradient colors
    gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id) [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor];

    //set locations
    gradientLayer.locations = @[@0.0, @0.25, @0.5];

    //set gradient start and end points
    gradientLayer.startPoint = CGPointMake(0, 0);
    gradientLayer.endPoint = CGPointMake(1, 1);
}

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.7.png" alt="圖6.7" />

圖6.7 用locations構(gòu)造偏移至左上角的三色漸變

CAReplicatorLayer

CAReplicatorLayer的目的是為了高效生成許多相似的圖層。它會(huì)繪制一個(gè)或多個(gè)圖層的子圖層,并在每個(gè)復(fù)制體上應(yīng)用不同的變換??瓷先パ菔灸軌蚋咏忉屵@些,我們來(lái)寫個(gè)例子吧。

重復(fù)圖層(Repeating Layers)

清單6.8中,我們?cè)谄聊坏闹虚g創(chuàng)建了一個(gè)小白色方塊圖層,然后用CAReplicatorLayer生成十個(gè)圖層組成一個(gè)圓圈。instanceCount屬性指定了圖層需要重復(fù)多少次。instanceTransform指定了一個(gè)CATransform3D3D變換(這種情況下,下一圖層的位移和旋轉(zhuǎn)將會(huì)移動(dòng)到圓圈的下一個(gè)點(diǎn))。

變換是逐步增加的,每個(gè)實(shí)例都是相對(duì)于前一實(shí)例布局。這就是為什么這些復(fù)制體最終不會(huì)出現(xiàn)在同意位置上,圖6.8是代碼運(yùn)行結(jié)果。

清單6.8 用CAReplicatorLayer重復(fù)圖層

@interface ViewController ()

@property (nonatomic, weak) IBOutlet UIView *containerView;

@end

@implementation ViewController
- (void)viewDidLoad
{
    [super viewDidLoad];
    //create a replicator layer and add it to our view
    CAReplicatorLayer *replicator = [CAReplicatorLayer layer];
    replicator.frame = self.containerView.bounds;
    [self.containerView.layer addSublayer:replicator];

    //configure the replicator
    replicator.instanceCount = 10;

    //apply a transform for each instance
    CATransform3D transform = CATransform3DIdentity;
    transform = CATransform3DTranslate(transform, 0, 200, 0);
    transform = CATransform3DRotate(transform, M_PI / 5.0, 0, 0, 1);
    transform = CATransform3DTranslate(transform, 0, -200, 0);
    replicator.instanceTransform = transform;

    //apply a color shift for each instance
    replicator.instanceBlueOffset = -0.1;
    replicator.instanceGreenOffset = -0.1;

    //create a sublayer and place it inside the replicator
    CALayer *layer = [CALayer layer];
    layer.frame = CGRectMake(100.0f, 100.0f, 100.0f, 100.0f);
    layer.backgroundColor = [UIColor whiteColor].CGColor;
    [replicator addSublayer:layer];
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.8.png" alt="圖6.8" />

圖6.8 用CAReplicatorLayer創(chuàng)建一圈圖層

注意到當(dāng)圖層在重復(fù)的時(shí)候,他們的顏色也在變化:這是用instanceBlueOffsetinstanceGreenOffset屬性實(shí)現(xiàn)的。通過(guò)逐步減少藍(lán)色和綠色通道,我們逐漸將圖層顏色轉(zhuǎn)換成了紅色。這個(gè)復(fù)制效果看起來(lái)很酷,但是CAReplicatorLayer真正應(yīng)用到實(shí)際程序上的場(chǎng)景比如:一個(gè)游戲中導(dǎo)彈的軌跡云,或者粒子爆炸(盡管iOS 5已經(jīng)引入了CAEmitterLayer,它更適合創(chuàng)建任意的粒子效果)。除此之外,還有一個(gè)實(shí)際應(yīng)用是:反射。

反射

使用CAReplicatorLayer并應(yīng)用一個(gè)負(fù)比例變換于一個(gè)復(fù)制圖層,你就可以創(chuàng)建指定視圖(或整個(gè)視圖層次)內(nèi)容的鏡像圖片,這樣就創(chuàng)建了一個(gè)實(shí)時(shí)的『反射』效果。讓我們來(lái)嘗試實(shí)現(xiàn)這個(gè)創(chuàng)意:指定一個(gè)繼承于UIViewReflectionView,它會(huì)自動(dòng)產(chǎn)生內(nèi)容的反射效果。實(shí)現(xiàn)這個(gè)效果的代碼很簡(jiǎn)單(見(jiàn)清單6.9),實(shí)際上用ReflectionView實(shí)現(xiàn)這個(gè)效果會(huì)更簡(jiǎn)單,我們只需要把ReflectionView的實(shí)例放置于Interface Builder(見(jiàn)圖6.9),它就會(huì)實(shí)時(shí)生成子視圖的反射,而不需要?jiǎng)e的代碼(見(jiàn)圖6.10).

清單6.9 用CAReplicatorLayer自動(dòng)繪制反射

#import "ReflectionView.h"
#import <QuartzCore/QuartzCore.h>

@implementation ReflectionView

+ (Class)layerClass
{
    return [CAReplicatorLayer class];
}

- (void)setUp
{
    //configure replicator
    CAReplicatorLayer *layer = (CAReplicatorLayer *)self.layer;
    layer.instanceCount = 2;

    //move reflection instance below original and flip vertically
    CATransform3D transform = CATransform3DIdentity;
    CGFloat verticalOffset = self.bounds.size.height + 2;
    transform = CATransform3DTranslate(transform, 0, verticalOffset, 0);
    transform = CATransform3DScale(transform, 1, -1, 0);
    layer.instanceTransform = transform;

    //reduce alpha of reflection layer
    layer.instanceAlphaOffset = -0.6;
}
?
- (id)initWithFrame:(CGRect)frame
{
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}

- (void)awakeFromNib
{
    //this is called when view is created from a nib
    [self setUp];
}
@end

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.9.png" alt="圖6.9" />

圖6.9 在Interface Builder中使用ReflectionView

http://wiki.jikexueyuan.com/project/ios-core-animation/images/6.10.png" alt="圖6.10" />

圖6.10 ReflectionView自動(dòng)實(shí)時(shí)產(chǎn)生反射效果。

開源代碼ReflectionView完成了一個(gè)自適應(yīng)的漸變淡出效果(用CAGradientLayer和圖層蒙板實(shí)現(xiàn)),代碼見(jiàn) https://github.com/nicklockwood/ReflectionView

CAScrollLayer

對(duì)于一個(gè)未轉(zhuǎn)換的圖層,它的bounds和它的frame是一樣的,frame屬性是由bounds屬性自動(dòng)計(jì)算而出的,所以更改任意一個(gè)值都會(huì)更新其他值。

但是如果你只想顯示一個(gè)大圖層里面的一小部分呢。比如說(shuō),你可能有一個(gè)很大的圖片,你希望用戶能夠隨意滑動(dòng),或者是一個(gè)數(shù)據(jù)或文本的長(zhǎng)列表。在一個(gè)典型的iOS應(yīng)用中,你可能會(huì)用到UITableView或是UIScrollView,但是對(duì)于獨(dú)立的圖層來(lái)說(shuō),什么會(huì)等價(jià)于剛剛提到的UITableViewUIScrollView呢?

在第二章中,我們探索了圖層的contentsRect屬性的用法,它的確是能夠解決在圖層中小地方顯示大圖片的解決方法。但是如果你的圖層包含子圖層那它就不是一個(gè)非常好的解決方案,因?yàn)?,這樣做的話每次你想『滑動(dòng)』可視區(qū)域的時(shí)候,你就需要手工重新計(jì)算并更新所有的子圖層位置。

這個(gè)時(shí)候就需要CAScrollLayer了。CAScrollLayer有一個(gè)-scrollToPoint:方法,它自動(dòng)適應(yīng)bounds的原點(diǎn)以便圖層內(nèi)容出現(xiàn)在滑動(dòng)的地方。注意,這就是它做的所有事情。前面提到過(guò),Core Animation并不處理用戶輸入,所以CAScrollLayer并不負(fù)責(zé)將觸摸事件轉(zhuǎn)換為滑動(dòng)事件,既不渲染滾動(dòng)條,也不實(shí)現(xiàn)任何iOS指定行為例如滑動(dòng)反彈(當(dāng)視圖滑動(dòng)超多了它的邊界的將會(huì)反彈回正確的地方)。

讓我們來(lái)用CAScrollLayer來(lái)常見(jiàn)一個(gè)基本的UIScrollView替代品。我們將會(huì)用CAScrollLayer作為視圖的宿主圖層,并創(chuàng)建一個(gè)自定義的UIView,然后用UIPanGestureRecognizer實(shí)現(xiàn)觸摸事件響應(yīng)。這段代碼見(jiàn)清單6.10. 圖6.11是運(yùn)行效果:ScrollView顯示了一個(gè)大于它的frameUIImageView。

清單6.10 用CAScrollLayer實(shí)現(xiàn)滑動(dòng)視圖

#import "ScrollView.h"
#import <QuartzCore/QuartzCore.h> @implementation ScrollView
+ (Class)layerClass
{
    return [CAScrollLayer class];
}

- (void)setUp
{
    //enable clipping
    self.layer.masksToBounds = YES;

    //attach pan gesture recognizer
    UIPanGestureRecognizer *recognizer = nil;
    recognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(pan:)];
    [self addGestureRecognizer:recognizer];
}

- (id)initWithFrame:(CGRect)frame
{
    //this is called when view is created in code
    if ((self = [super initWithFrame:frame])) {
        [self setUp];
    }
    return self;
}

- (void)awakeFromNib {
    //this is called when view is created from a nib
    [self setUp];
}

- (void)pan:(UIPanGestureRecognizer *)recognizer
{
    //get the offset by subtracting the pan gesture
    //translation from the current bounds origin
    CGPoint offset = self.bounds.origin;
    offset.x -= [recognizer translationInView:self].x;
    offset.y -= [recognizer translationInView:self].y;

    //scroll the layer
    [(CAScrollLayer *)self.layer scrollToPoint:offset];

    //reset the pan gesture translation
    [recognizer setTranslation:CGPointZero inView:self];
}
@end

圖6.11 用UIScrollView創(chuàng)建一個(gè)湊合的滑動(dòng)視圖

不同于UIScrollView,我們定制的滑動(dòng)視圖類并沒(méi)有實(shí)現(xiàn)任何形式的邊界檢查(bounds checking)。圖層內(nèi)容極有可能滑出視圖的邊界并無(wú)限滑下去。CAScrollLayer并沒(méi)有等同于UIScrollViewcontentSize的屬性,所以當(dāng)CAScrollLayer滑動(dòng)的時(shí)候完全沒(méi)有一個(gè)全局的可滑動(dòng)區(qū)域的概念,也無(wú)法自適應(yīng)它的邊界原點(diǎn)至你指定的值。它之所以不能自適應(yīng)邊界大小是因?yàn)樗恍枰?,?nèi)容完全可以超過(guò)邊界。

那你一定會(huì)奇怪用CAScrollLayer的意義到底何在,因?yàn)槟憧梢院?jiǎn)單地用一個(gè)普通的CALayer然后手動(dòng)適應(yīng)邊界原點(diǎn)啊。真相其實(shí)并不復(fù)雜,UIScrollView并沒(méi)有用CAScrollLayer,事實(shí)上,就是簡(jiǎn)單的通過(guò)直接操作圖層邊界來(lái)實(shí)現(xiàn)滑動(dòng)。

CAScrollLayer有一個(gè)潛在的有用特性。如果你查看CAScrollLayer的頭文件,你就會(huì)注意到有一個(gè)擴(kuò)展分類實(shí)現(xiàn)了一些方法和屬性:

- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;

看到這些方法和屬性名,你也許會(huì)以為這些方法給每個(gè)CALayer實(shí)例增加了滑動(dòng)功能。但是事實(shí)上他們只是放置在CAScrollLayer中的圖層的實(shí)用方法。scrollPoint:方法從圖層樹中查找并找到第一個(gè)可用的CAScrollLayer,然后滑動(dòng)它使得指定點(diǎn)成為可視的。scrollRectToVisible:方法實(shí)現(xiàn)了同樣的事情只不過(guò)是作用在一個(gè)矩形上的。visibleRect屬性決定圖層(如果存在的話)的哪部分是當(dāng)前的可視區(qū)域。如果你自己實(shí)現(xiàn)這些方法就會(huì)相對(duì)容易明白一點(diǎn),但是CAScrollLayer幫你省了這些麻煩,所以當(dāng)涉及到實(shí)現(xiàn)圖層滑動(dòng)的時(shí)候就可以用上了。

CATiledLayer

有些時(shí)候你可能需要繪制一個(gè)很大的圖片,常見(jiàn)的例子就是一個(gè)高像素的照片或者是地球表面的詳細(xì)地圖。iOS應(yīng)用通暢運(yùn)行在內(nèi)存受限的設(shè)備上,所以讀取整個(gè)圖片到內(nèi)存中是不明智的。載入大圖可能會(huì)相當(dāng)?shù)芈?,那些?duì)你看上去比較方便的做法(在主線程調(diào)用UIImage-imageNamed:方法或者-imageWithContentsOfFile:方法)將會(huì)阻塞你的用戶界面,至少會(huì)引起動(dòng)畫卡頓現(xiàn)象。

能高效繪制在iOS上的圖片也有一個(gè)大小限制。所有顯示在屏幕上的圖片最終都會(huì)被轉(zhuǎn)化為OpenGL紋理,同時(shí)OpenGL有一個(gè)最大的紋理尺寸(通常是2048*2048,或4096*4096,這個(gè)取決于設(shè)備型號(hào))。如果你想在單個(gè)紋理中顯示一個(gè)比這大的圖,即便圖片已經(jīng)存在于內(nèi)存中了,你仍然會(huì)遇到很大的性能問(wèn)題,因?yàn)镃ore Animation強(qiáng)制用CPU處理圖片而不是更快的GPU(見(jiàn)第12章『速度的曲調(diào)』,和第13章『高效繪圖』,它更加詳細(xì)地解釋了軟件繪制和硬件繪制)。

CATiledLayer為載入大圖造成的性能問(wèn)題提供了一個(gè)解決方案:將大圖分解成小片然后將他們單獨(dú)按需載入。讓我們用實(shí)驗(yàn)來(lái)證明一下。

小片裁剪

這個(gè)示例中,我們將會(huì)從一個(gè)2048*2048分辨率的雪人圖片入手。為了能夠從CATiledLayer中獲益,我們需要把這個(gè)圖片裁切成許多小一些的圖片。你可以通過(guò)代碼來(lái)完成這件事情,但是如果你在運(yùn)行時(shí)讀入整個(gè)圖片并裁切,那CATiledLayer這些所有的性能優(yōu)點(diǎn)就損失殆盡了。理想情況下來(lái)說(shuō),最好能夠逐個(gè)步驟來(lái)實(shí)現(xiàn)。

清單6.11 演示了一個(gè)簡(jiǎn)單的Mac OS命令行程序,它用CATiledLayer將一個(gè)圖片裁剪成小圖并存儲(chǔ)到不同的文件中。

清單6.11 裁剪圖片成小圖的終端程序

#import <AppKit/AppKit.h>

int main(int argc, const char * argv[])
{
    @autoreleasepool{
        ?//handle incorrect arguments
        if (argc < 2) {
            NSLog(@"TileCutter arguments: inputfile");
            return 0;
        }

        //input file
        NSString *inputFile = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];

        //tile size
        CGFloat tileSize = 256; //output path
        NSString *outputPath = [inputFile stringByDeletingPathExtension];

        //load image
        NSImage *image = [[NSImage alloc] initWithContentsOfFile:inputFile];
        NSSize size = [image size];
        NSArray *representations = [image representations];
        if ([representations count]){
            NSBitmapImageRep *representation = representations[0];
            size.width = [representation pixelsWide];
            size.height = [representation pixelsHigh];
        }
        NSRect rect = NSMakeRect(0.0, 0.0, size.width, size.height);
        CGImageRef imageRef = [image CGImageForProposedRect:&rect context:NULL hints:nil];

        //calculate rows and columns
        NSInteger rows = ceil(size.height / tileSize);
        NSInteger cols = ceil(size.width / tileSize);

        //generate tiles
        for (int y = 0; y < rows; ++y) {
            for (int x = 0; x < cols; ++x) {
            //extract tile image
            CGRect tileRect = CGRectMake(x*tileSize, y*tileSize, tileSize, tileSize);
            CGImageRef tileImage = CGImageCreateWithImageInRect(imageRef, tileRect);

            //convert to jpeg data
            NSBitmapImageRep *imageRep = [[NSBitmapImageRep alloc] initWithCGImage:tileImage];
            NSData *data = [imageRep representationUsingType: NSJPEGFileType properties:nil];
            CGImageRelease(tileImage);

            //save file
            NSString *path = [outputPath stringByAppendingFormat: @"_%02i_%02i.jpg", x, y];
            [data writeToFile:path atomically:NO];
            }
        }
    }
    return 0;
}

這個(gè)程序?qū)?048*2048分辨率的雪人圖案裁剪成了64個(gè)不同的256*256的小圖。(256*256是CATiledLayer的默認(rèn)小圖大小,默認(rèn)大小可以通過(guò)tileSize屬性更改)。程序接受一個(gè)圖片路徑作為命令行的第一個(gè)參數(shù)。我們可以在編譯的scheme將路徑參數(shù)硬編碼然后就可以在Xcode中運(yùn)行了,但是以后作用在另一個(gè)圖片上就不方便了。所以,我們編譯了這個(gè)程序并把它保存到敏感的地方,然后從終端調(diào)用,如下面所示:

> path/to/TileCutterApp path/to/Snowman.jpg

The app is very basic, but could easily be extended to support additional arguments such as tile size, or to export images in formats other than JPEG. The result of running it is a sequence of 64 new images, named as follows:

這個(gè)程序相當(dāng)基礎(chǔ),但是能夠輕易地?cái)U(kuò)展支持額外的參數(shù)比如小圖大小,或者導(dǎo)出格式等等。運(yùn)行結(jié)果是64個(gè)新圖的序列,如下面命名:

Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg

既然我們有了裁切后的小圖,我們就要讓iOS程序用到他們。CATiledLayer很好地和UIScrollView集成在一起。除了設(shè)置圖層和滑動(dòng)視圖邊界以適配整個(gè)圖片大小,我們真正要做的就是實(shí)現(xiàn)-drawLayer:inContext:方法,當(dāng)需要載入新的小圖時(shí),CATiledLayer就會(huì)調(diào)用到這個(gè)方法。

清單6.12演示了代碼。圖6.12是代碼運(yùn)行結(jié)果。

清單6.12 一個(gè)簡(jiǎn)單的滾動(dòng)CATiledLayer實(shí)現(xiàn)

#import "ViewController.h"
#import <