UIView有一个搭档叫CALayer,UIView实际上不直接绘制在屏幕上,它绘制在它的layer上面,layer再绘制在屏幕上面。view不会每次都重新绘制,它会缓存绘制 (存储的是bitmap),它缓存的实际上就是layer。view的graphic context实际上是layer的graphic context。layer有如下功能:
-
layer有属性会影响绘制
-
多个layers可以和单个view组合
-
layer是动画的基础
View 和layer
一个UIView有一个CALayer,可以使用view的layer属性访问。layer没有view属性,但是view是layer的 delegate,这个layer被称为view的”优先layer(underlying layer)”。
layer的delegate属性可以被设置,但是永远不要设置underlying layer的delegate属性
当你子类化UIView,想要子类化的layer成为view的underlying layer,你可以重写UIView的layerClass
方法:
+ (Class) layerClass {
return [CompassLayer class];
}
layer扮演着view所有绘制的功能,如果view绘制,layer会同样绘制。view是layer的delegate,访问view 的属性通常仅仅是访问layer的属性。例如,你设置view的backgroundColor,实际上你设置的是layer的 backgroundColor,如果你直接设置layer的backgroundColor,view的backgroundColor也会同样被设置。
view在它的layer中绘制,layer缓存这些绘制为一个图片。layer接下来可以被操作,在view不被重新绘制的前提下, 可以改变view的显示。
layers 和 sublayers
一个layer可以有多个sublayers,view的子view的underlying layer是view的underlying layer的子layer。
CALayer* lay1 = [CALayer new];
lay1.frame = CGRectMake(113, 111, 132, 194);
lay1.backgroundColor =
[[UIColor colorWithRed:1 green:.4 blue:1 alpha:1] CGColor];
[mainview.layer addSublayer:lay1];
CALayer* lay2 = [CALayer new];
lay2.backgroundColor =
[[UIColor colorWithRed:.5 green:1 blue:0 alpha:1] CGColor];
lay2.frame = CGRectMake(41, 56, 132, 194);
[lay1 addSublayer:lay2];
UIImageView* iv =
[[UIImageView alloc] initWithImage:[UIImage imageNamed:@"smiley"]];
CGRect r = iv.frame;
r.origin = CGPointMake(180,180);
iv.frame = r;
[mainview addSubview:iv];
CALayer* lay3 = [CALayer new];
lay3.backgroundColor =
[[UIColor colorWithRed:1 green:0 blue:0 alpha:1] CGColor];
lay3.frame = CGRectMake(43, 197, 160, 230);
[mainview.layer addSublayer:lay3];
layer的子layer显示是否超过父layer的边界,由layer的masksToBounds属性决定,这个属性同view的 clipsToBounds属性作用一样。
管理layer的层次结构
layer同view一样也有superlayer和sublayers属性,可以通过下面的方法管理layer的层次结构:
- addSublayer:
- insertSublayer:atIndex:
- insertSublayer:below:
- insertSublayer:above:
- replaceSublayer:with:
- removeFromSuperlayer
不像view的subviews属性,layer的sublayers属性是可写的,你可以一次性删除和改写它的所有sublayers。
layer还有一个属性zPosition
来决定layer的顺序,zPostion默认为0。
layer同样提供了如下方法,用于转换在同一层次结构中两个layer的坐标系
- convertPoint:fromLayer:,convertPoint:toLayer:
- convertRect:fromLayer:,convertRect:toLayer:
定位sublayer
layer的内部坐标系同view一样,使用bounds表示。但是layer在其superlayer的位置,并不像view那样 使用center表示,它取决于两个属性,position和anchorPoint。想象layer是钉在其superlayer上面, 那么需要同时知道图钉在layer和superlayer的位置。
-
position 在superlayer坐标系中的位置
-
anchorPoint position相对于layer自己的bounds的位置。它表示当前位置和自己宽度和高度的比值,如{0,0}表示在 layer的左上角,{1,1}表示在layer的右下角。
如果anchorPoint等于{0.5,0.5}(默认),那么position相当于view的center。layer的position和 anchorPoint是相互独立的,改变一个并不会改变另一个。
layer的frame仅仅是一个计算出来的数据;当你读取frame时,layer通过计算bounds、position和anchorPoint 得出相应的frame;当你设置frame时,同时会设置bounds和position。
在代码中创建layer时,它的frame和bounds都是{0,0}、{0,0}。你必须设置正确的非零值,这个layer 才会显示出来。
CAScrollLayer
当你想要移动一个layer的bounds原点后,它的sublayers也跟着一起移动时,你可以使用CALayer的子类 CAScrollLayer(不像它的名字,CAScrollLayer不提供scroll界面)。默认情况下,CAScrollLayer的 masksToBounds属性为YES,因此你不能看见所有超出bounds的界面。(你也可以设置masksToBounds为 NO,但是会有奇怪的事情发生,可能不能满足你的要求)。
你可以移动CAScrollLayer的bounds,也可以移动其任意深度的sublayer
移动CAScrollLayer:
-
scrollToPoint: 改变CAScrollLayer的原点到指定点
-
scrollToRect: 最小改变CAScrollLayer的原点,使指定的范围可见
移动sublayers:
-
scrollPoint: 改变CAScrollLayer的原点,使sublayer的指定点在CAScrollLayer的左上角
-
scrollRectToVisible: 改变CAScrollLayer的原点,使sublayer的bounds的指定范围在CAScrollLayer的bounds范围内。 你可以访问sublayer的visibleRect属性,现在它在CAScrollLayer的bounds范围内。
sublayers的布局
view的层次结构实际上是layer的层次结构,view在superview中的位置,实际上是layer在其superlayer 中的位置。view可以通过autoresizingMask或者constraint实现自动布局,因此,如果layer是view的 underlying layer那么它也可以。否则,不是underlying layer就不能实现自动布局,只能在代码中 手动调整位置。
当一个layer需要布局时,不管是它bounds发生变化还是你调用setNeedsLayout
,你可以通过下列方法
中的任意一种实现:
- 重写CALayer子类中的
layoutSublayers
方法 - 在layer的delegate中实现
layoutSublayersOfLayer:
方法
当手动布局时,你可能需要标识出每个sublayers,以方便后面引用。在Layer中没有viewWithTag:
类似
的方法,你可以使用key-value编程方法,这将后面会介绍。
对于view的underlying layer,layoutSublayers
和layoutSublayersOfLayer
在view的
layoutSubviews
后调用。如果使用自动布局,你必须调用super,而且这些方法可能调用多次,如果你
想要只调用一次的方法,请使用layoutSubviews
。
在layer中绘制
如果drawRect:
实现,那么view在它的underlying layer中绘制。下面介绍怎么在其他layer中绘制。
最简单的方法,是设置layer的contents属性,它类似于UIImageView的image属性。contents接受一个 CGImageRef或nil,contents的类型是id,而CGImageRef是一个对象,因此,在赋值时你必须强制转换。
CALayer* lay4 = [CALayer new];
UIImage* im = [UIImage imageNamed:@"smiley"];
CGRect r = lay4.frame;
r.origin = CGPointMake(180,180);
r.size = im.size;
lay4.frame = r;
lay4.contents = (id)im.CGImage;
[mainview.layer addSublayer:lay4];
设置layer的contents为UIImage,而不是CGImageRef,会导致图形不显示,但是不会提示任何错误
layer有4个方法,用于绘制它的contents,在下列情况下会调用它们:
-
如果layer的
needsDisplayOnBoundsChange
为NO(默认),除非你调用setNeedsDisplay
或setNeedsDisplayInRect:
,layer才会重新绘制。 -
如果layer的
needsDisplayOnBoundsChange
为YES,那么当layer的bounds变化时,layer会重新 绘制(相当于UIView的UIViewContentModeRedraw)。
下面有4个方法用于绘制,选择一个来实现(不要实现多个,会导致冲突)
- subclass中的
display
CALayer的子类可以重写display
方法,这儿没有graphic context,因此这个方法仅仅用于设置contents属性。
- delegate中
displayLayer:
你可以设置CALayer的delegate属性,然后在其delegate中实现displayLayer:
,这儿也没有graphic context,
因此你仅能设置contents属性。
- subclass中
drawInContext:
CALayer的子类可以重写drawInContext:
方法,提供的graphic context你可以直接用于绘制,但不会
使其为当前context。
- delegate中的
drawLayer:inContext:
你可以设置CALayer的delegate属性,然后在其delegate中实现drawLayer:inContext:
,提供的
graphic context你可以直接用于绘制,但不会使其为当前context。
layer有一个contentScale
属性,如果layer被CoCoa管理,且它有contents,系统将会设置正确的
contentSacle
。例如,UIView实现了drawRect:
方法,在一个double-resolution设备中,它的
underlying layer的contentScale
为2。你自己创建的layer中,没有自动为你设置好contentScale
,
你必须正确它的值,否则会导致显示出错。你可以通过[UIScreen mainScreen].scale
获得正确的值。
下面的属性对layer的显示影响很大,初学者很容易混淆它们。
-
backgroundColor
:把它和layer的绘制分开考虑,它相当layer的绘制的背景。它等于view的backgroundColor
(如果它是view的underlying layer,那么它就是viewbackgroundColor
), 设置它会立即生效。 -
opaque
:决定layer的graphic context是否为不透明。一个不透明的graphic context是黑色的, 你可以在黑色上面绘制,但是黑色任然在那儿。一个透明的graphic context是完全透明的。改变layer的 opaque只有在重新绘制layer时才会生效。view的underlying layer的opaque
属性和view的opaque
属性是完全独立的,它们是完全不同的。 -
opacity
:影响所有的layer和其sublayers的透明度。它等于view的alpha
(如果他是view的 underlying layer,那么它就是view的alpha
)。它分别影响着background color和layer的contents 的透明度,改变它立即生效。
view的underlying layer自动重新绘制
一个layer不会自动重新绘制,除非needsDisplayOnBoundsChange
为YES且bounds发生改变,但是view
会。例如,当你发送setNeedsDisplay
时,view会重新绘制,但是如果你的view没有实现drawRect:
,那么
它不会给underlying layer发送setNeedsDisplay
。因此,当你直接在view的underlying layer中
执行所有绘制时,你想要underlying layer自动重新绘制,你应该实现darwRect:
方法,即使这个方法
什么都不做。(这不会影响到underlying layer的sublayers)。
contents的大小和位置
layer的context被缓存为一个bitmap,就像image一样对待。
-
如果contents来自于直接设置layer的属性contents,那么缓存的就是那张图片,大小就是图片的大小。
-
如果contents来自于graphic context的绘制(drawInContext:, drawLayer:inContext:),那么 缓存的就是整个graphic context,大小是layer在绘制时的大小。
下面的这些layer属性影响着缓存的contents怎么显示:
-
contentsGravity: 这个相当于UIView的
contentMode
属性。 -
contentsRect: 决定哪些部分能被显示,默认是{{0,0},{1,1}}表示整个都可以被显示。也可以用于按比例缩小contents,设置 一个更大的区域如{{-0.5,-0.5},{1.5,1.5}},但是任何contents的边界会被扩充到layer的边界(为了保护 这些区域,应所有超过contents边界的区域都为空)。
-
contentsCenter: 一个CGRect,和resizable image的缩放行为相同。将一个contentsRect分为9个区域,中间的contentsCenter 区域向两边缩放,4边的区域向一个方向缩放,4角的区域不缩放。
如果layer的contents来自于直接在graphic context绘制,那么layer的contentsGravity没有影响, 因为graphic context的大小刚好是layer的大小。但是如果layer的contentsRect不等于{{0,0},{1,1}}, 那么contentsGravity会有影响。
再次重申,当layer的contents来自于直接在graphic context绘制,layer大小改变时,如果layer被
要求重新显示,layer会被重新执行绘制以保证contents的大小刚好为layer的大小。但如果layer的
needsDisplayOnBoundsChange
为NO,那么当layer的大小改变时,它不会被重新绘制,它将使用缓存
的contents,这时contentsGravity就会起作用。
layer的maskToBounds
属性对它的sublayers有同样的影响。如果为NO,即使contents超过layer
(取决于layer的contentsGravity
和contentsRect
)也会显示整个contents。如果为YES,只有
在layer的bounds范围内的才会显示。
layer的内置类型
- CATextLayer
CATextLayer有一个string
属性,它会绘制其string
属性。默认情况下,文本的颜色foregroundColor
是白色,这可能不是你想要的颜色。string
属性和layer的contents
属性不相容,它会绘制其中的一个,但
不会两个都绘制,所以通常不应该设置CATextLayer的contents
属性。
- CAShapeLayer
CAShapeLayer有一个类型为CGPath的path
属性,它会使用fillColor
填充或使用strokeColor
画出
这个path。CAShapeLayer也可以使用contents
属性,图形会显示在contents图形上面,但是没有方法指定
它们的混合模式。
- CAGradientLayer
CAGradientLayer将背景(background)转换为一个简单的线性渐变。就像Core Graphic中渐变一样,你需要
指定使用数组(NSArray)指定渐变位置和颜色(需要的颜色是CGColor,因此你需要强制转换为id类型才能加入数组)。
CAGradientLayer的contents
不会被显示。
Transform
最简单的变换是二维变换,你可以使用affineTransform
进行二维变换,它是一个CGAffineTransform类型,同
view的transform相同,它围绕着anchorPoint
变换。
完整的变换是三维变换,包含x、y、z轴。layer并不能给你逼真的三维透视图,你可以使用OpenGL达到这个目的。
三维变换扩展了anchorPoint
,加入anchorPointZ
属性以用于支持z轴变换。三维变换在apple中被称为
CATransform3D,它和CAAffineTransform相似,只是在其基础上加入了z轴。
Depth
阴影,边框,面罩
CALayer有很多属性影响着绘制,因为这些属性也可以在view的underlying layer中应用,所以它们也影响着view。
CALayer可以有阴影效果,通过shadowColor
,shadowOpacity
,shadowRadius
,shadowOffset
属性
配置。阴影会添加到图形非透明的地方,你可以自己定义图形然后将这个图片赋值给CGPath的shadowPath
属性
以提高性能。
CALayer可以通过borderWidth
和borderColor
定义边框。通过设置cornerRadius
可以让layer有圆角矩形
效果。
layer 效率
layer和key-value编程
layer的所有属性都可以通过key-value编程来访问。因此设置layer的mask:
layer.mask = mask;
也可以这样书写:
[layer setValue:mask forKey:@"mask"]
而且,CATransform3D和CGAffineTransform的值可以通过key-value编程得到扩充访问。如:
self.rotationLayer.transform = CGTransform3DMakeRotation(M_PI/4.0, 0, 1, 0);
可以写成:
[self.rotationLayer setValue:@(M_PI/4.0) forKeyPath:@"transform.rotation.y"]
此外,你可以将CALayer想象为一个NSDictionary,你可以设置它的任意key和value。比如,当你手动布局layer 时,你需要分别每个sublayers。你可以这样使用:
[myLayer1 setValue:@"layer1" forKey:@"name"];
[myLayer2 setValue:@"layer2" forKey:@"name"];
layer并没有name
属性,但是你这样使用,用于辨识每个sublayers。