Core Animation
图层不仅仅能作用于图片和颜色而已。本章就会学习其他的一些图层类,进一步扩展使用Core Animation
绘图的能力。
CAShapeLayer
在前面『视觉效果』我们学习到了不使用图片的情况下用 CGPath
去构造任意形状的阴影。如果我们能用同样的方式创建相同形状的图层就好了。
CAShapeLayer
是一个通过矢量图形而不是bitmap
来绘制的图层子类。你指定诸如颜色和线宽等属性,用 CGPath
来定义想要绘制的图形,最后CAShapeLayer
就自动渲染出来了。当然,你也可以用Core Graphics
直接向原始的CALayer
的内容中绘制一个路径,相比直下,使用 CAShapeLayer
有以下一 些优点:
- 渲染快速。
CAShapeLayer
使用了硬件加速,绘制同一图形会比用Core Graphics
快很多。 - 高效使用内存。一个
CAShapeLayer
不需要像普通CALayer
一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。 - 不会被图层边界剪裁掉。一个
CAShapeLayer
可以在边界之外绘制。你的图层路径不会像在使用Core Graphics
的普通CALayer
一样被剪裁掉。 - 不会出现像素化。当你给
CAShapeLayer
做3D
变换时,它不像一个有寄宿图的普通图层一样变得像素化。
创建一个 CGPath
CAShapeLayer
可以用来绘制所有能够通过 CGPath
来表示的形状。这个形状不 一定要闭合,图层路径也不一定要不可破,事实上你可以在一个图层上绘制好几个不同的形状。你可以控制一些属性比如lineWidth
(线宽,用点表示单位), lineCap
(线条结尾的样子),和lineJoin
(线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。
Demo
使用代码用一个CAShapeLayer
渲染一个简单的火柴人。CAShapeLayer
属性是CGPathRef
类型,但是我们用UIBezierPath
帮助类创建了图层路径,这样我们就不用考虑人工释放CGpath
了。
- (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:M_PI * 2 clockwise:true];
[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];
}
圆角
前面提到了CAShapeLayer
为创建圆角视图提供了一个方法,就是CALayer
的cornerRadius
属性。虽然使用CAShapeLayer
类需要更多的工作,但是它有一个优势就是可以单独指定每个角。
我们创建圆角矩形其实就是人工绘制单独的直线和弧度,但是事实上 UIBezierPath
有自动绘制圆角矩形的构造方法,下面这段代码绘制了一个有 三个圆角一个直角的矩形:
//define path parameters
CGRect rect = CGRectMake(50, 50, 100, 100);
CGSize radii = CGSizeMake(20, 20);
//create path
UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomLeft | UIRectCornerTopRight;
UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rectbyRoundingCorners:corners cornerRadii:radii];
我们可以通过这个图层路径绘制一个既有直角又有圆角的视图。如果我们想依照此 图形来剪裁视图内容,我们可以把 CAShapeLayer
作为视图的宿主图层,而不是添加一个子视图。
CATextLayer
如果你想在一个图层里面显示文字,完全可以借助图层代理直接将字符串使用Core Graphics
写入图层的内容(这就是UILabel
的精髓)。如果越过寄宿于图层的视图,直接在图层上操作,那其实相当繁琐。你要为每一个显示文字的图层创建一个 能像图层代理一样工作的类,还要逻辑上判断哪个图层需要显示哪个字符串,更别 提还要记录不同的字体,颜色等一系列乱七八糟的东西。
万幸的是这些都是不必要的,Core Animation
提供了一个CALayer
的子类 CATextLayer
,它以图层的形式包含了UILabel
几乎所有的绘制特性,并且额外提供了一些新的特性。
同样,CATextLayer
也要比 UILabel
渲染得快得多。很少有人知道在iOS 6
及之 前的版本,UILabel
其实是通过WebKit
来实现绘制的,这样就造成了当有很多文 字的时候就会有极大的性能压力。而 CATextLayer
使用了Core text
,并且渲染得非常快。
让我们来尝试用 CATextLayer 来显示一些文字即用 CATextLayer 来实现一个 UILabel
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *labelView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//create a text layer
CATextLayer *textLayer = [CATextLayer layer];
textLayer.frame = self.labelView.bounds;
[self.labelView.layer addSublayer:textLayer];
//set text attributes
textLayer.foregroundColor = [UIColor blackColor].CGColor;
textLayer.alignmentMode = kCAAlignmentJustified;
textLayer.wrapped = YES;
//choose a font
UIFont *font = [UIFont systemFontOfSize:15];
//set layer font
CFStringRef fontName = (__bridge CFStringRef)font.fontName;
CGFontRef fontRef = CGFontCreateWithFontName(fontName);
textLayer.font = fontRef;
textLayer.fontSize = font.pointSize;
CGFontRelease(fontRef);
NSString *text = @"街坊邻居撒砥砺奋进阿卡丽是点击分类卡萨丁";
//set layer text
textLayer.string = text;
}
@end
如果你仔细看这个文本,你会发现一个奇怪的地方:这些文本有一些像素化了。这是因为并没有以Retina
的方式渲染,前面提到了这个contentScale
属性,用来决定图层内容应该以怎样的分辨率来渲染。 contentScale
并不关心屏幕的拉伸因素而总是默认为1.0。如果我们想以Retina
的质量来显示文字,我们就得手动地设 置 CATextLayer
的contentsScale
属性,如下:
textLayer.contentsScale = [UIScreen mainScreen].scale;
CATextLayer
的 font
属性不是一个 UIFont
类型,而是一个CFTypeRef
类型。这样可以根据你的具体需要来决定字体属性应该是用CGFontRef
类 类型还是 CTFontRef
类型(Core Text
字体)。同时字体大小也是用fontSize
属性单独设置的,因为CTFontRef
和CGFontRef
并不像UIFont
一样包含点大小。这个例子会告诉你如何将UIFont
转换成CGFontRef
。
另外, CATextLayer
的string
属性并不是你想象的NSString
类型,而是id
类型。这样你既可以用 NNString
也可以用NSAttributedString
来指定文本了(注意, NSAttributedString
并不是NSString
的子类)。属性化字符串是iOS
用来渲染字体风格的机制,它以特定的方式来决定指定范围内的字符串的原始信息,比如字体,颜色,字重,斜体等。
富文本
iOS 6
中,Apple
给 UILabel
和其他UIKit
文本视图添加了直接的属性化字符串的支持,应该说这是一个很方便的特性。不过事实上从iOS3.2
开始 CATextLayer
就已经支持属性化字符串了。这样的话,如果你想要支持更低版本的iOS
系统, CATextLayer
无疑是你向界面中增加富文本的好办法,而且也不用去跟复杂 的Core Text
打交道,也省了用UIWebView
的麻烦。
iOS 6
及以上我们可以用新的NSTextAttributeName
实例来设置我们的字符串属性,但是练习的 目的是为了演示在iOS 5
及以下,所以我们用了Core Text
,也就是说你需要把Core Text framework
添加到你的项目中。否则,编译器是无法识别属性常量的。
行距和字距
有必要提一下的是,由于绘制的实现机制不同(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
方法,然后用它的返回类型来创建宿主图层。
一个UILabel
子类LayerLabel
用CATextLayer
绘制它的问题,而不是调用一般的 UILabel
使用的较慢的- drawRect:
方法。LayerLabel
示例既可以用代码实现,也可以在Interface Builder
实现,只要把普通的标签拖入视图之中,然后设置它的类是LayerLabel
就可以了。
#import "LayerLabel.h"
@implementation LayerLabel
+ (Class)layerClass
{
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
方法)之上。但是,为了简化案例, 我们仅仅重建了一个单独的图层,而不是使用视图。这意味着我们不能像第五章一 样在立方体表面显示按钮和标签,不过我们现在也用不到这个特性。
#import "ViewController.h"
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (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];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
- (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 * 0.5, containerSize.width * 0.5);
//apply the transform and return
cube.transform = transform;
return cube;
}
@end
CAGradientLayer
CAGradientLayer
是用来生成两种或更多颜色平滑渐变的。用Core Graphics
复制一个CAGradientLayer
并将内容绘制到一个普通图层的寄宿图也是有可能的,
但是CAGradientLayer
的真正好处在于绘制使用了硬件加速。
基础渐变
我们将从一个简单的红变蓝的对角线渐变开始.这些渐变色彩放在一 个数组中,并赋给colors
属性。这个数组成员接受 CGColorRef
类型的值(并不是从 NSObject
派生而来),所以我们要用通过bridge
转换以确保编译正常。
CAGradientLayer
也有 startPoint
和 endPoint
属性,他们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0}
,右下角坐标 是{1, 1}
。
- (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 = [UIColor redColor].CGColor;
//set gradient start and end points
gradientLayer.startPoint = CGPointMake(0, 0);
gradientLayer.endPoint = CGPointMake(1, 1);
}
多重渐变
如果你愿意, colors
属性可以包含很多颜色,所以创建一个彩虹一样的多重渐变 也是很简单的。默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用locations
属性来调整空间。 locations
属性是一个浮点数值的数组(以NSNumber
包装)。这些浮点数定义了 colors
属性中每个不同颜色的位 置,同样的,也是以单位坐标系进行标定。0.0
代表着渐变的开始,1.0
代表着结束。
locations
数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations
的数组大小和 colors
数组大小一定要相同,否则你将会得到一个 空白的渐变。
Demo
基于上面代码对角线渐变的代码改造。现在变成了从红到黄最后到绿色的渐变。 locations
数组指定了0.0
,0.25
和0.5
三个数值,这样这三个渐变就有点像挤在了左上角。
- (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 = [UIColor redColor].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);
}
CAReplicatorLayer
CAReplicatorLayer
的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。看上去演示能够更加解释这 些,我们来写个例子吧。
重复图层(Repeating Layers)
我们在屏幕的中间创建了一个小白色方块图层,然后用 CAReplicatorLayer
生成十个图层组成一个圆圈。 instanceCount
属性指定了图层需要重复多少次。instanceTransform
指定了一个 CATransform3D 3D
变换(这种情况下,下一图层的位移和旋转将会移动到圆圈的下一个点)。
变换是逐步增加的,每个实例都是相对于前一实例布局。这就是为什么这些复制体 最终不会出现在统一位置上。
- (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];
}
注意到当图层在重复的时候,他们的颜色也在变化:这是用 instanceBlueOffset
和 instanceGreenOffset
属性实现的。通过逐步减少 蓝色和绿色通道,我们逐渐将图层颜色转换成了红色。这个复制效果看起来很酷, 但是CAReplicatorLayer
真正应用到实际程序上的场景比如:一个游戏中导弹的 轨迹云,或者粒子爆炸(尽管iOS 5
已经引入了 CAEmitterLayer
,它更适合创建 任意的粒子效果)。除此之外,还有一个实际应用是:反射。
反射
使用 CAReplicatorLayer
并应用一个负比例变换于一个复制图层,你就可以创建指定视图(或整个视图层次)内容的镜像图片,这样就创建了一个实时的『反射』 效果。让我们来尝试实现这个创意:指定一个继承于 UIView
的 ReflectionView
,它会自动产生内容的反射效果。实现这个效果的代码很简单,实际上用ReflectionView
实现这个效果会更简单,我们只需要把 ReflectionView
的实例放置于Interface Builder
(见代码一), 它就会实时生成子视图的反射,而不需要别的代码.
代码一
#import "ReflectionView.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
CAScrollLayer
对于一个未转换的图层,它的 bounds
和它的 frame
是一样的, frame
属性是由 bounds
属性自动计算而出的,所以更改任意一个值都会更新其他值。
但是如果你只想显示一个大图层里面的一小部分呢。比如说,你可能有一个很大的 图片,你希望用户能够随意滑动,或者是一个数据或文本的长列表。在一个典型的 iOS
应用中,你可能会用到UITableView
或是UIScrollView
,但是对于独立的图层来说,什么会等价于刚刚提到的 UITableView
和UIScrollView
呢?
在前面,我们探索了图层的 contentsRect
属性的用法,它的确是能够解决 在图层中小地方显示大图片的解决方法。但是如果你的图层包含子图层那它就不是 一个非常好的解决方案,因为,这样做的话每次你想『滑动』可视区域的时候,你 就需要手工重新计算并更新所有的子图层位置。
这个时候就需要 CAScrollLayer
了。CAScrollLayer
有一个- scrollToPoint:
方法,它自动适应bounds
的原点以便图层内容出现在滑动的地方。注意,这就是它做的所有事情。前面提到过,Core Animation
并不处理用户输 入,所以 CAScrollLayer
并不负责将触摸事件转换为滑动事件,既不渲染滚动 条,也不实现任何iOS
指定行为例如滑动反弹(当视图滑动超多了它的边界的将会 反弹回正确的地方)。
让我们来用CAScrollLayer
来常见一个基本的UIScrollView
替代品。我们将会用CAScrollLayer
作为视图的宿主图层,并创建一个自定义的 UIView
,然后用UIPanGestureRecognizer
实现触摸事件响应。
#import "ScrollView.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
不同于 UIScrollView
,我们定制的滑动视图类并没有实现任何形式的边界检查 (bounds checking
)。图层内容极有可能滑出视图的边界并无限滑下去。CAScrollLayer
并没有等同于 UIScrollView
中contentSize
的属性,所以当 CAScrollLayer
滑动的时候完全没有一个全局的可滑动区域的概念,也无法 自适应它的边界原点至你指定的值。它之所以不能自适应边界大小是因为它不需 要,内容完全可以超过边界。
那你一定会奇怪用 CAScrollLayer
的意义到底何在,因为你可以简单地用一个普 通的CALayer
然后手动适应边界原点啊。真相其实并不复杂,UIScrollView
并 没有用CAScrollLayer
,事实上,就是简单的通过直接操作图层边界来实现滑 动。
CAScrollLayer
有一个潜在的有用特性。如果你查看 CAScrollLayer
的头文 件,你就会注意到有一个扩展分类实现了一些方法和属性:
- (void)scrollPoint:(CGPoint)p;
- (void)scrollRectToVisible:(CGRect)r;
@property(readonly) CGRect visibleRect;
看到这些方法和属性名,你也许会以为这些方法给每个 CALayer
实例增加了滑动功能。但是事实上他们只是放置在 CAScrollLayer
中的图层的实用方法。scrollPoint:
方法从图层树中查找并找到第一个可用
的CAScrollLayer
,然后滑动它使得指定点成为可视的。 scrollRectToVisible:
方法实现了同样的事情只不过是作用在一个矩形上 的。 visibleRect
属性决定图层(如果存在的话)的哪部分是当前的可视区域。 如果你自己实现这些方法就会相对容易明白一点,但是 CAScrollLayer
帮你省了 这些麻烦,所以当涉及到实现图层滑动的时候就可以用上了。
CATiledLayer
有些时候你可能需要绘制一个很大的图片,常见的例子就是一个高像素的照片或者 是地球表面的详细地图。 iOS
应用通常运行在内存受限的设备上,所以读取整个图 片到内存中是不明智的。载入大图可能会相当地慢,那些对你看上去比较方便的做 法(在主线程调用UIImage
的 -imageNamed:
方法或者 - imageWithContentsOfFile:
方法)将会阻塞你的用户界面,至少会引起动画卡顿现象。
能高效绘制在iOS
上的图片也有一个大小限制。所有显示在屏幕上的图片最终都会 被转化为OpenGL
纹理,同时OpenGL
有一个最大的纹理尺寸(通常是2048*2048
, 或4096*4096
,这个取决于设备型号)。如果你想在单个纹理中显示一个比这大的 图,即便图片已经存在于内存中了,你仍然会遇到很大的性能问题,因为Core Animation
强制用CPU
处理图片而不是更快的GPU
(见第12章『速度的曲调』,和 第13章『高效绘图』,它更加详细地解释了软件绘制和硬件绘制)。
CATiledLayer
为载入大图造成的性能问题提供了一个解决方案:将大图分解成 小片然后将他们单独按需载入。让我们用实验来证明一下。
小片裁剪
这个示例中,我们将会从一个2048*2048
分辨率的雪人图片入手。为了能够从 CATiledLayer
中获益,我们需要把这个图片裁切成许多小一些的图片。你可以通过代码来完成这件事情,但是如果你在运行时读入整个图片并裁切,那 CATiledLayer
这些所有的性能优点就损失殆尽了。理想情况下来说,最好能 够逐个步骤来实现。
Demo演示了一个简单的Mac OS
命令行程序,它用 CATiledLayer
将一个图片 裁剪成小图并存储到不同的文件中。
Retina小图
你也许已经注意到了这些小图并不是以Retina
的分辨率显示的。为了以屏幕的原生分辨率来渲染 ,我们需要设置图层的 contentsScale
来匹配UIScreen
的 scale
属性:
tileLayer.contentsScale = [UIScreen mainScreen].scale;
有趣的是, titleSize
是以像素为单位,而不是点,所以增大了contentScale
就自动有了默认的小图尺寸(现在它是128*128
的点而不是256*256
).所以,我们不需要手工更新小图的尺寸或是在Retina
分辨率下指定一个 不同的小图。我们需要做的是适应小图渲染代码以对应安排scale
的变化,然 而:
//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
的容器,这些 CAEmitterCell
定义了一个粒子效果. 你将会为不同的粒子效果定义一个或多个CAEmitterCell
作为模板,同时CAEmitterLayer
负责基于这些模板实例化一个粒子流.一个CAEmitterCell
类似一个CALayer
:它有一个 contents
属性可以定义为一个 CGImage
,另外还有一些可设置属性控制着表 现和行为。我们不会对这些属性逐一进行详细的描述,你们可以在 CAEmitterCell
类的头文件中找到。
我们来举个例子。我们将利用在一圆中发射不同速度和透明度的粒子创建一个火爆 炸的效果。
#import "ViewController.h"
@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,emitter.frame.size.height);
//create a particle template
CAEmitterCell *cell = [[CAEmitterCell alloc] init];
cell.contents = (__bridge id)[UIImage imageNamed:@"Spark.png"];
cell.birthRate = 150;
cell.lifetime = 5.0;
cell.color = [UIColor colorWithRed:1 green:0.5 blue:0.1 alpha:3.0];
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
CAEMitterCell
的属性基本上可以分为三种:
- 这种粒子的某一属性的初始值。比如,
color
属性指定了一个可以混合图片 内容颜色的混合色。在示例中,我们将它设置为桔色。 - 例子某一属性的变化范围。比如
emissionRange
属性的值是2π
,这意味着例 子可以从360
度任意位置反射出来。如果指定一个小一些的值,就可以创造出 一个圆锥形 - 指定值在时间线上的变化。比如,在示例中,我们将
alphaSpeed
设置 为-0.4
,就是说例子的透明度每过一秒就是减少0.4
,这样就有发射出去之后逐渐小时的效果。
CAEmitterLayer
的属性它自己控制着整个例子系统的位置和形状。一些属性比如 birthRate
,lifetime
和 celocity
,这些属性在CAEmitterCell
中也有。这些属性会以相乘的方式作用在一起,这样你就可以用一个值来加速或者扩大整个例子系统。其他值得提到的属性有以下这些:
-
preservesDepth
,是否将3D
例子系统平面化到一个图层(默认值)或者可 以在3D
空间中混合其他的图层 -
renderMode
,控制着在视觉上粒子图片是如何混合的。你可能已经注意到 了示例中我们把它设置为kCAEmitterLayerAdditive
,它实现了这样一个效果:合并例子重叠部分的亮度使得看上去更亮。如果我们把它设置为默认的kCAEmitterLayerUnordered
,效果就没那么好看了
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
类将着色逻辑抽象出来。其他的事情,我们还是会有以往的方 式。
在一个真正的OpenGL
应用中,我们可能会用 NSTimer
或 CADisplayLink
周期性地每秒钟调用-drawRrame
方法60
次,同时会将几何图形生成和绘制分开以便不会每次都重新生成三角形的顶点(这样也可以让我们绘制其他的一些东西而不是 一个三角形而已),不过上面这个例子已经足够演示了绘图原则了。
AVPlayerLayer
最后一个图层类型是 AVPlayerLayer
。尽管它不是Core Animation
框架的一部分 (AV
前缀看上去像), AVPlayerLayer
是有别的框架(AVFoundation
)提供 的,它和Core Animation
紧密地结合在一起,提供了一个 CALayer
子类来显示自定义的内容类型。
AVPlayerLayer
是用来在iOS
上播放视频的。他是高级接口例如MPMoviePlayer
的底层实现,提供了显示视频的底层控制。AVPlayerLayer
的使用相当简单:你可以用 +playerLayerWithPlayer:
方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后
用player
属性绑定一个 AVPlayer
实例。
@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *containerView;
@end
@implementation ViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//get video URL
NSURL *URL = [[NSBundle mainBundle]URLForResource:@"" withExtension:@""];
//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];
}
我们用代码创建了一个 AVPlayerLayer
,但是我们仍然把它添加到了一个容器视 图中,而不是直接在controller
中的主视图上添加。这样其实是为了可以使用自动布局限制使得图层在最中间;否则,一旦设备被旋转了我们就要手动重新放置位置, 因为Core Animation
并不支持自动大小和自动布局。
当然,因为 AVPlayerLayer
是CALayer
的子类,它继承了父类的所有特性。我们并不会受限于要在一个矩形中播放视频;下面代码演示了在3D
,圆角,有色边框,蒙板,阴影等效果.
- (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];
}
总结
这一章我们简要概述了一些专用图层以及用他们实现的一些效果,我们只是了解到 这些图层的皮毛,像CATiledLayer
和 CAEMitterLayer
这些类可以单独写一章 的。但是,重点是记住 CALayer
是用处很大的,而且它并没有为所有可能的场景 进行优化。为了获得Core Animation
最好的性能,你需要为你的工作选对正确的工 具,希望你能够挖掘这些不同的 CALayer
子类的功能。 这一章我们通过 CAEmitterLayer
和AVPlayerLayer
类简单地接触到了一些动画,在下面,我们将继续深入研究动画,就从隐式动画开始。
iOS核心动画高级技巧--目录