iOS Core Animation Advanced Techniques(三):专用图层

到目前为止,咱们已经探讨过CALayer类了,同时咱们也了解到了一些很是有用的绘图和动画功能。可是Core Animation图层不只仅能做用于图片和颜色而已。本章就会学习其余的一些图层类,进一步扩展使用Core Animation绘图的能力。git

CAShapeLayergithub

在第四章『视觉效果』咱们学习到了不使用图片的状况下用CGPath去构造任意形状的阴影。若是咱们能用一样的方式建立相同形状的图层就行了。api

CAShapeLayer是一个经过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath来定义想要绘制的图形,最后CAShapeLayer就自动渲染出来了。固然,你也能够用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有如下一些优势:数组

  • 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快不少。安全

  • 高效使用内存。一个CAShapeLayer不须要像普通CALayer同样建立一个寄宿图形,因此不管有多大,都不会占用太多的内存。多线程

  • 不会被图层边界剪裁掉。一个CAShapeLayer能够在边界以外绘制。你的图层路径不会像在使用Core Graphics的普通CALayer同样被剪裁掉(如咱们在第二章所见)。并发

  • 不会出现像素化。当你给CAShapeLayer作3D变换时,它不像一个有寄宿图的普通图层同样变得像素化。app

建立一个CGPath框架

CAShapeLayer能够用来绘制全部可以经过CGPath来表示的形状。这个形状不必定要闭合,图层路径也不必定要不可破,事实上你能够在一个图层上绘制好几个不一样的形状。你能够控制一些属性好比lineWith(线宽,用点表示单位),lineCap(线条结尾的样子),和lineJoin(线条之间的结合点的样子);可是在图层层面你只有一次机会设置这些属性。若是你想用不一样颜色或风格来绘制多个形状,就不得不为每一个形状准备一个图层了。dom

清单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类简单地接触到了一些动画,在第二章,咱们将继续深刻研究动画,就从隐式动画开始。

相关文章
相关标签/搜索