iOS-Core-Animation-Advanced-Techniques(三)

專用圖層

複雜的組織都是專門化的--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繪製一個火柴人

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#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.png

圖6.1 用CAShapeLayer繪製一個簡單的火柴人

圓角

第二章裏面提到了CAShapeLayer爲創建圓角視圖提供了一個方法,就是CALayer的cornerRadius屬性(譯者注:其實是在第四章提到的)。雖然使用CAShapeLayer類需要更多的工作,但是它有一個優勢就是可以單獨指定每個角。

我們創建圓角舉行其實就是人工繪製單獨的直線和弧度,但是事實上UIBezierPath有自動繪製圓角矩形的構造方法,下面這段代碼繪製了一個有三個圓角一個直角的矩形:

1
2
3
4
5
6
//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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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屬性,如下:

1
textLayer.contentsScale = [UIScreen mainScreen].scale;

這樣就解決了這個問題(如圖6.3)

6.3.png

圖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實現一個富文本標籤。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
#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.png

圖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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
#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圖層體系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
@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.png

圖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 簡單的兩種顏色的對角線漸變

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@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.png

圖6.6 用CAGradientLayer實現簡單的兩種顏色的對角線漸變

多重漸變

如果你願意,colors屬性可以包含很多顏色,所以創建一個彩虹一樣的多重漸變也是很簡單的。默認情況下,這些顏色在空間上均勻地被渲染,但是我們可以用locations屬性來調整空間。locations屬性是一個浮點數值的數組(以NSNumber包裝)。這些浮點數定義了colors屬性中每個不同顏色的位置,同樣的,也是以單位座標系進行標定。0.0代表着漸變的開始,1.0代表着結束。

locations數組並不是強制要求的,但是如果你給它賦值了就一定要確保locations的數組大小和colors數組大小一定要相同,否則你將會得到一個空白的漸變。

清單6.7展示了一個基於清單6.6的對角線漸變的代碼改造。現在變成了從紅到黃最後到綠色的漸變。locations數組指定了0.0,0.25和0.5三個數值,這樣這三個漸變就有點像擠在了左上角。(如圖6.7).

清單6.7 在漸變上使用locations

1
2
3
4
5
6
7
8
9
10
11
12
13
14
- (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.png

圖6.7 用locations構造偏移至左上角的三色漸變

CAReplicatorLayer

CAReplicatorLayer的目的是爲了高效生成許多相似的圖層。它會繪製一個或多個圖層的子圖層,並在每個複製體上應用不同的變換。看上去演示能夠更加解釋這些,我們來寫個例子吧。

重複圖層(Repeating Layers)

清單6.8中,我們在屏幕的中間創建了一個小白色方塊圖層,然後用CAReplicatorLayer生成十個圖層組成一個圓圈。instanceCount屬性指定了圖層需要重複多少次。instanceTransform指定了一個CATransform3D3D變換(這種情況下,下一圖層的位移和旋轉將會移動到圓圈的下一個點)。

變換是逐步增加的,每個實例都是相對於前一實例佈局。這就是爲什麼這些複製體最終不會出現在同意位置上,圖6.8是代碼運行結果。

清單6.8 用CAReplicatorLayer重複圖層

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@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.png

圖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自動繪製反射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#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.jpg

圖6.9 在Interface Builder中使用ReflectionView

6.10.png

圖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實現滑動視圖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#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的頭文件,你就會注意到有一個擴展分類實現了一些方法和屬性:

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

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

CATiledLayer

有些時候你可能需要繪製一個很大的圖片,常見的例子就是一個高像素的照片或者是地球表面的詳細地圖。iOS應用通暢運行在內存受限的設備上,所以讀取整個圖片到內存中是不明智的。載入大圖可能會相當地慢,那些對你看上去比較方便的做法(在主線程調用UIImage的-imageNamed:方法或者-imageWithContentsOfFile:方法)將會阻塞你的用戶界面,至少會引起動畫卡頓現象。

能高效繪製在iOS上的圖片也有一個大小限制。所有顯示在屏幕上的圖片最終都會被轉化爲OpenGL紋理,同時OpenGL有一個最大的紋理尺寸(通常是2048*2048,或4096*4096,這個取決於設備型號)。如果你想在單個紋理中顯示一個比這大的圖,即便圖片已經存在於內存中了,你仍然會遇到很大的性能問題,因爲Core Animation強制用CPU處理圖片而不是更快的GPU(見第12章『速度的曲調』,和第13章『高效繪圖』,它更加詳細地解釋了軟件繪製和硬件繪製)。

CATiledLayer爲載入大圖造成的性能問題提供了一個解決方案:將大圖分解成小片然後將他們單獨按需載入。讓我們用實驗來證明一下。

小片裁剪

這個示例中,我們將會從一個2048*2048分辨率的雪人圖片入手。爲了能夠從CATiledLayer中獲益,我們需要把這個圖片裁切成許多小一些的圖片。你可以通過代碼來完成這件事情,但是如果你在運行時讀入整個圖片並裁切,那CATiledLayer這些所有的性能優點就損失殆盡了。理想情況下來說,最好能夠逐個步驟來實現。

清單6.11 演示了一個簡單的Mac OS命令行程序,它用CATiledLayer將一個圖片裁剪成小圖並存儲到不同的文件中。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#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: @"_i_i.jpg", x, y];
            [data writeToFile:path atomically:NO];
            }
        }
    }
    return 0;
}

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

1
> 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:

這個程序相當基礎,但是能夠輕易地擴展支持額外的參數比如小圖大小,或者導出格式等等。運行結果是64個新圖的序列,如下面命名:

1
2
3
4
5
Snowman_00_00.jpg
Snowman_00_01.jpg
Snowman_00_02.jpg
...
Snowman_07_07.jpg

既然我們有了裁切後的小圖,我們就要讓iOS程序用到他們。CATiledLayer很好地和UIScrollView集成在一起。除了設置圖層和滑動視圖邊界以適配整個圖片大小,我們真正要做的就是實現-drawLayer:inContext:方法,當需要載入新的小圖時,CATiledLayer就會調用到這個方法。

清單6.12演示了代碼。圖6.12是代碼運行結果。

清單6.12 一個簡單的滾動CATiledLayer實現

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#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_i_i, 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.png

圖6.12 用UIScrollView滾動CATiledLayer

當你滑動這個圖片,你會發現當CATiledLayer載入小圖的時候,他們會淡入到界面中。這是CATiledLayer的默認行爲。(你可能已經在iOS 6之前的蘋果地圖程序中見過這個效果)你可以用fadeDuration屬性改變淡入時長或直接禁用掉。CATiledLayer(不同於大部分的UIKit和Core Animation方法)支持多線程繪製,-drawLayer:inContext:方法可以在多個線程中同時地併發調用,所以請小心謹慎地確保你在這個方法中實現的繪製代碼是線程安全的。

Retina小圖

你也許已經注意到了這些小圖並不是以Retina的分辨率顯示的。爲了以屏幕的原生分辨率來渲染CATiledLayer,我們需要設置圖層的contentsScale來匹配UIScreen的scale屬性:

1
tileLayer.contentsScale = [UIScreen mainScreen].scale;

有趣的是,tileSize是以像素爲單位,而不是點,所以增大了contentsScale就自動有了默認的小圖尺寸(現在它是128*128的點而不是256*256).所以,我們不需要手工更新小圖的尺寸或是在Retina分辨率下指定一個不同的小圖。我們需要做的是適應小圖渲染代碼以對應安排scale的變化,然而:

1
2
3
4
5
//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是一個高性能的粒子引擎,被用來創建實時例子動畫如:煙霧,火,雨等等這些效果。

CAEmitterLayer看上去像是許多CAEmitterCell的容器,這些CAEmitierCell定義了一個例子效果。你將會爲不同的例子效果定義一個或多個CAEmitterCell作爲模版,同時CAEmitterLayer負責基於這些模版實例化一個粒子流。一個CAEmitterCell類似於一個CALayer:它有一個contents屬性可以定義爲一個CGImage,另外還有一些可設置屬性控制着表現和行爲。我們不會對這些屬性逐一進行詳細的描述,你們可以在CAEmitterCell類的頭文件中找到。

我們來舉個例子。我們將利用在一圓中發射不同速度和透明度的粒子創建一個火爆炸的效果。清單6.13包含了生成爆炸的代碼。圖6.13是運行結果

清單6.13 用CAEmitterLayer創建爆炸效果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#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度任意位置反射出來。如果指定一個小一些的值,就可以創造出一個圓錐形

  • 指定值在時間線上的變化。比如,在示例中,我們將alphaSpeed設置爲-0.4,就是說例子的透明度每過一秒就是減少0.4,這樣就有發射出去之後逐漸小時的效果。

CAEmitterLayer的屬性它自己控制着整個例子系統的位置和形狀。一些屬性比如birthRate,lifetime和celocity,這些屬性在CAEmitterCell中也有。這些屬性會以相乘的方式作用在一起,這樣你就可以用一個值來加速或者擴大整個例子系統。其他值得提到的屬性有以下這些:

  • preservesDepth,是否將3D例子系統平面化到一個圖層(默認值)或者可以在3D空間中混合其他的圖層

  • renderMode,控制着在視覺上粒子圖片是如何混合的。你可能已經注意到了示例中我們把它設置爲kCAEmitterLayerAdditive,它實現了這樣一個效果:合併例子重疊部分的亮度使得看上去更亮。如果我們把它設置爲默認的kCAEmitterLayerUnordered,效果就沒那麼好看了(見圖6.14).

6.14.png

圖6.14 禁止混色之後的火焰粒子

CAEAGLLayer

當iOS要處理高性能圖形繪製,必要時就是OpenGL。應該說它應該是最後的殺手鐗,至少對於非遊戲的應用來說是的。因爲相比Core Animation和UIkit框架,它不可思議地複雜。

OpenGL提供了Core Animation的基礎,它是底層的C接口,直接和iPhone,iPad的硬件通信,極少地抽象出來的方法。OpenGL沒有對象或是圖層的繼承概念。它只是簡單地處理三角形。OpenGL中所有東西都是3D空間中有顏色和紋理的三角形。用起來非常複雜和強大,但是用OpenGL繪製iOS用戶界面就需要很多很多的工作了。

爲了能夠以高性能使用Core Animation,你需要判斷你需要繪製哪種內容(矢量圖形,例子,文本,等等),但後選擇合適的圖層去呈現這些內容,Core Animation中只有一些類型的內容是被高度優化的;所以如果你想繪製的東西並不能找到標準的圖層類,想要得到高性能就比較費事情了。

因爲OpenGL根本不會對你的內容進行假設,它能夠繪製得相當快。利用OpenGL,你可以繪製任何你知道必要的集合信息和形狀邏輯的內容。所以很多遊戲都喜歡用OpenGL(這些情況下,Core Animation的限制就明顯了:它優化過的內容類型並不一定能滿足需求),但是這樣依賴,方便的高度抽象接口就沒了。

在iOS 5中,蘋果引入了一個新的框架叫做GLKit,它去掉了一些設置OpenGL的複雜性,提供了一個叫做CLKView的UIView的子類,幫你處理大部分的設置和繪製工作。前提是各種各樣的OpenGL繪圖緩衝的底層可配置項仍然需要你用CAEAGLLayer完成,它是CALayer的一個子類,用來顯示任意的OpenGL圖形。

大部分情況下你都不需要手動設置CAEAGLLayer(假設用GLKView),過去的日子就不要再提了。特別的,我們將設置一個OpenGL ES 2.0的上下文,它是現代的iOS設備的標準做法。

儘管不需要GLKit也可以做到這一切,但是GLKit囊括了很多額外的工作,比如設置頂點和片段着色器,這些都以類C語言叫做GLSL自包含在程序中,同時在運行時載入到圖形硬件中。編寫GLSL代碼和設置EAGLayer沒有什麼關係,所以我們將用GLKBaseEffect類將着色邏輯抽象出來。其他的事情,我們還是會有以往的方式。

在開始之前,你需要將GLKit和OpenGLES框架加入到你的項目中,然後就可以實現清單6.14中的代碼,裏面是設置一個GAEAGLLayer的最少工作,它使用了OpenGL ES 2.0 的繪圖上下文,並渲染了一個有色三角(見圖6.15).

清單6.14 用CAEAGLLayer繪製一個三角形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
#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.png

圖6.15 用OpenGL渲染的CAEAGLLayer圖層

在一個真正的OpenGL應用中,我們可能會用NSTimer或CADisplayLink週期性地每秒鐘調用-drawRrame方法60次,同時會將幾何圖形生成和繪製分開以便不會每次都重新生成三角形的頂點(這樣也可以讓我們繪製其他的一些東西而不是一個三角形而已),不過上面這個例子已經足夠演示了繪圖原則了。

AVPlayerLayer

最後一個圖層類型是AVPlayerLayer。儘管它不是Core Animation框架的一部分(AV前綴看上去像),AVPlayerLayer是有別的框架(AVFoundation)提供的,它和Core Animation緊密地結合在一起,提供了一個CALayer子類來顯示自定義的內容類型。

AVPlayerLayer是用來在iOS上播放視頻的。他是高級接口例如MPMoivePlayer的底層實現,提供了顯示視頻的底層控制。AVPlayerLayer的使用相當簡單:你可以用+playerLayerWithPlayer:方法創建一個已經綁定了視頻播放器的圖層,或者你可以先創建一個圖層,然後用player屬性綁定一個AVPlayer實例。

在我們開始之前,我們需要添加AVFoundation到我們的項目中。然後,清單6.15創建了一個簡單的電影播放器,圖6.16是代碼運行結果。

清單6.15 用AVPlayerLayer播放視頻

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#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.png

圖6.16 用AVPlayerLayer圖層播放視頻的截圖

我們用代碼創建了一個AVPlayerLayer,但是我們仍然把它添加到了一個容器視圖中,而不是直接在controller中的主視圖上添加。這樣其實是爲了可以使用自動佈局限制使得圖層在最中間;否則,一旦設備被旋轉了我們就要手動重新放置位置,因爲Core Animation並不支持自動大小和自動佈局(見第三章『圖層幾何學』)。

當然,因爲AVPlayerLayer是CALayer的子類,它繼承了父類的所有特性。我們並不會受限於要在一個矩形中播放視頻;清單6.16演示了在3D,圓角,有色邊框,蒙板,陰影等效果(見圖6.17).

清單6.16 給視頻增加變換,邊框和圓角

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
- (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.png

圖6.17 3D視角下的邊框和圓角AVPlayerLayer

總結

這一章我們簡要概述了一些專用圖層以及用他們實現的一些效果,我們只是瞭解到這些圖層的皮毛,像CATiledLayer和CAEMitterLayer這些類可以單獨寫一章的。但是,重點是記住CALayer是用處很大的,而且它並沒有爲所有可能的場景進行優化。爲了獲得Core Animation最好的性能,你需要爲你的工作選對正確的工具,希望你能夠挖掘這些不同的CALayer子類的功能。 這一章我們通過CAEmitterLayer和AVPlayerLayer類簡單地接觸到了一些動畫,在第二章,我們將繼續深入研究動畫,就從隱式動畫開始。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章