iOS Layers

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,layoutSublayerslayoutSublayersOfLayer在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(默认),除非你调用setNeedsDisplaysetNeedsDisplayInRect:,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的contentsGravitycontentsRect)也会显示整个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可以有阴影效果,通过shadowColorshadowOpacityshadowRadiusshadowOffset属性 配置。阴影会添加到图形非透明的地方,你可以自己定义图形然后将这个图片赋值给CGPath的shadowPath属性 以提高性能。

CALayer可以通过borderWidthborderColor定义边框。通过设置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。