转载:http://www.jianshu.com/p/18c306333080
CALayer在概念上和UIView类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。并且它们有一些方法和属性用来做动画和变换,但是CALayer不能响应事件。
UIView是基于CALayer的一种封装,每一个UIView都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作。UIView的尺寸样式以及内容都由CALayer提供。
大多数情况下我们都使用UIView而不使用CALayer的原因是,首先是CALayer不能响应事件,其次是CALayer不方便自动布局,最重要的是苹果已经基于CALayer封装好了UIView,我们在使用UIView高级接口的同时也能使用CALayer的底层功能。
既然这里提到了层级树,顺便提一下事件响应链:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{
UIResponder* next = [selfnextResponder];
NSMutableString* prefix =@"-".mutableCopy;
while(next !=nil) {
NSLog(@"%@%@", prefix, [nextclass]);
[prefix appendString:@"--"];
next = [next nextResponder]; }
}
2. CALayer内容相关的属性
contents
可以给CALayer的contents赋值
self.layerView.layer.contents= (__bridgeid)image.CGImage;
contentGravity
可以类似设置UIView的contentModel一样,设置CALayer的contentGravity
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
contentGravity可以赋值的常量值如下
CA_EXTERNNSString*constkCAGravityCenterCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityTopCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityBottomCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityLeftCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityRightCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityTopLeftCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityTopRightCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityBottomLeftCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityBottomRightCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityResizeCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityResizeAspectCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);CA_EXTERNNSString*constkCAGravityResizeAspectFillCA_AVAILABLE_STARTING(10.5, 2.0, 9.0, 2.0);
contentsScale
CALayer的contentsScale属性可以用以适配retina屏幕,该属性值默认为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。
UIImage*image = [UIImageimageNamed:@"doll"];UIView*view = [[UIViewalloc] initWithFrame:self.view.bounds];view.layer.contents = (__bridgeid_Nullable)(image.CGImage);view.layer.contentsGravity = kCAGravityCenter;[self.view addSubview:view];
运行结果:
6CC1E4E8-6490-4586-BFA6-B146EC440776.png
可以看到显示出来的图片有像素颗粒感,在上面代码的基础上再加上一句
view.layer.contentsScale = [UIScreen mainScreen].scale;
运行结果:
B01FE8E0-7E06-4BA3-A95D-99D1F5BFC1CD.png
maskToBounds
类似UIView的clipsToBounds,CALayer有一个属性叫maskToBounds
self.layerView.layer.masksToBounds =YES;
contentsRect
CALayer的contentsRect属性可以用于图片拼合
举个栗子,有如下图片:
doll@2x.png
我们可以利用contentsRect属性来分别单独显示三个娃娃:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIView*leftTopImage;@property(weak,nonatomic)IBOutletUIView*rightTopImage;@property(weak,nonatomic)IBOutletUIView*leftBottomImage;@end@implementationViewController-(void)viewDidLoad { [superviewDidLoad];UIImage*image = [UIImageimageNamed:@"doll"];self.leftTopImage.layer.contents = (__bridgeid_Nullable)(image.CGImage);self.leftTopImage.layer.contentsRect =CGRectMake(0.0f,0.0f,0.5f,0.5f);self.rightTopImage.layer.contents = (__bridgeid_Nullable)(image.CGImage);self.rightTopImage.layer.contentsRect =CGRectMake(0.5f,0.0f,0.5f,0.5f);self.leftBottomImage.layer.contents = (__bridgeid_Nullable)(image.CGImage);self.leftBottomImage.layer.contentsRect =CGRectMake(0.0f,0.5f,0.5f,0.5f);}@end
运行结果:
0EE9992A-027F-43C4-84DB-F2127EC2BCEE.png
拼合不仅给app提供了一个整洁的载入方式,还有效地提高了载入性能(单张大图比多张小图载入地更快),但是如果有手动安排的话,他们还是有一些不方便的,如果你需要在一个已经创建好的品和图上做一些尺寸上的修改或者其他变动,无疑是比较麻烦的。
Mac上有一些商业软件可以为你自动拼合图片,这些工具自动生成一个包含拼合后的坐标的XML或者plist文件,拼合图片的使用大大简化。这个文件可以和图片一同载入,并给每个拼合的图层设置contentsRect,这样开发者就不用手动写代码来摆放位置了。
contentsCenter
CALayer的另一个属性contentsCenter,有点类似于UIImage里的-resizableImageWithCapInsets: 方法,楼主浅薄,实在不懂如何描述,这里有篇文章讲得很清楚:iOS: 关于CALayer的contentsCenter属性
3. CALayer位置坐标相关的属性
对应于UIView的frame、bounds、center,CALayer有frame、bounds、position,访问UIView的这三个属性,返回的其实就是CALayer对应的这三个值。但是CALayer另外还有一个比较难于理解的属性anchorPoint。
anchorPoint
anchorPoint决定了CALayer的那个点处于position的位置,其作用效果如下图:
3.3.jpeg
坐标转换
CALayer也有用于在不同坐标系中转换坐标的函数:
-(CGPoint)convertPoint:(CGPoint)p fromLayer:(nullableCALayer*)l;-(CGPoint)convertPoint:(CGPoint)p toLayer:(nullableCALayer*)l;-(CGRect)convertRect:(CGRect)r fromLayer:(nullableCALayer*)l;-(CGRect)convertRect:(CGRect)r toLayer:(nullableCALayer*)l;
zPosition
CALayer的zPosition属性可以控制图层的显示顺序,Interface Build布局如下:
Paste_Image.png
修改zPosition:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIView*blueView;@property(weak,nonatomic)IBOutletUIView*orangeView;@end@implementationViewController-(void)viewDidLoad { [superviewDidLoad];self.blueView.layer.zPosition =1.0f;}@end
运行结果:
Paste_Image.png
zPosition属性虽然可以改变图层的显示顺序,但是不能改变图层在图层树中的顺序
CALayer事件处理
CALayer并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:和-hitTest:。
-containsPoint:接受一个在本图层坐标系下的CGPoint,如果这个点在图层frame范围内就返回YES。举个栗子:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIView*whiteView;@property(weak,nonatomic)IBOutletUIView*orangeView;@end@implementationViewController-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{UITouch*touch = touches.anyObject;CGPointoriginPoint = [touch locationInView:self.view];CGPointthePoint = [self.whiteView.layer convertPoint:originPoint fromLayer:self.view.layer];BOOLcontainsResult = [self.whiteView.layer containsPoint:thePoint];if(containsResult) { thePoint = [self.orangeView.layer convertPoint:thePoint fromLayer:self.whiteView.layer]; containsResult = [self.orangeView.layer containsPoint:thePoint];UIAlertController*alert;if(containsResult) { alert = [UIAlertControlleralertControllerWithTitle:nilmessage:@"inside orange layer"preferredStyle:UIAlertControllerStyleAlert]; }else{ alert = [UIAlertControlleralertControllerWithTitle:nilmessage:@"inside white layer"preferredStyle:UIAlertControllerStyleAlert]; } [alert addAction:[UIAlertActionactionWithTitle:@"OK"style:UIAlertActionStyleCancelhandler:nil]]; [selfpresentViewController:alert animated:YEScompletion:nil]; }}@end
运行结果:
Paste.gif
-hitTest:方法同样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层本身,或者包含这个坐标点的叶子节点图层,如果这个点在最外面图层的范围之外,则返回nil。
下面这段代码实现了上图一样的效果:
-(void)touchesBegan:(NSSet *)touches withEvent:(UIEvent*)event{UITouch*touch = touches.anyObject;CGPointoriginPoint = [touch locationInView:self.view];CALayer*layer = [self.whiteView.layer hitTest:originPoint];UIAlertController*alert;if(layer ==self.orangeView.layer) { alert = [UIAlertControlleralertControllerWithTitle:nilmessage:@"inside orange layer"preferredStyle:UIAlertControllerStyleAlert]; }else{ alert = [UIAlertControlleralertControllerWithTitle:nilmessage:@"inside white layer"preferredStyle:UIAlertControllerStyleAlert]; } [alert addAction:[UIAlertActionactionWithTitle:@"OK"style:UIAlertActionStyleCancelhandler:nil]]; [selfpresentViewController:alert animated:YEScompletion:nil];}
4. CALayer视觉相关的属性
conrnerRadius
CALayer有一个叫做conrnerRadius的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把masksToBounds设置成YES的话,图层里面的所有东西都会被截取。
Interface Build布局如下:
Paste_Image.png
添加如下代码:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIView*topWhiteView;@property(weak,nonatomic)IBOutletUIView*bottomWhiteView;@end@implementationViewController-(void)viewDidLoad { [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.self.topWhiteView.layer.cornerRadius =20.0f;self.bottomWhiteView.layer.cornerRadius =20.0f;self.bottomWhiteView.layer.masksToBounds =YES;}@end
运行效果:
Paste_Image.png
borderWidth和borderColor
CALayer另外两个非常有用属性就是borderWidth和borderColor。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds绘制,同时也包含图层的角。在上面代码的基础上再加上几句:
self.topWhiteView.layer.borderWidth=5.0f;self.bottomWhiteView.layer.borderWidth=5.0f;self.topWhiteView.layer.borderColor= [UIColor yellowColor].CGColor;self.bottomWhiteView.layer.borderColor= [UIColor yellowColor].CGColor;
运行效果:
Paste_Image.png
阴影效果
CALayer还有一系列属性用来绘制阴影:
/** Shadow properties. **//* The color of the shadow. Defaults to opaque black. Colors created
* from patterns are currently NOT supported. Animatable. */@property(nullable) CGColorRef shadowColor;/* The opacity of the shadow. Defaults to 0. Specifying a value outside the
* [0,1] range will give undefined results. Animatable. */@propertyfloat shadowOpacity;/* The shadow offset. Defaults to (0, -3). Animatable. */@propertyCGSize shadowOffset;/* The blur radius used to create the shadow. Defaults to 3. Animatable. */@propertyCGFloat shadowRadius;/* When non-null this path defines the outline used to construct the
* layer's shadow instead of using the layer's composited alpha
* channel. The path is rendered using the non-zero winding rule.
* Specifying the path explicitly using this property will usually
* improve rendering performance, as will sharing the same path
* reference across multiple layers. Upon assignment the path is copied.
* Defaults to null. Animatable. */@property(nullable) CGPathRef shadowPath;
在前面代码的基础上再加上几句:
self.topWhiteView.layer.shadowColor= [UIColorblackColor].CGColor;self.bottomWhiteView.layer.shadowColor= [UIColorblackColor].CGColor;self.topWhiteView.layer.shadowOpacity=0.5f;self.bottomWhiteView.layer.shadowOpacity=0.5f;
运行效果:
Paste_Image.png
发现两个问题:
1、topWhiteView有阴影,但是阴影在视图的上方
2、而bottomWhiteView干脆根本没有阴影
第一个问题的原因在于这个属性:
/* The shadowoffset. Defaultsto(0,-3). Animatable. */@propertyCGSize shadowOffset;
CALayer的阴影偏移量默认为(0, -3),所以阴影出现在了视图的上方
第二个问题的原因在于这句代码:
self.bottomWhiteView.layer.masksToBounds= YES;
如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被才剪掉,一个简单的解决办法就是在bottomWhiteView的下面再插入一个view来充当阴影视图。
修改之前的代码,最终代码如下:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIView*topWhiteView;@property(weak,nonatomic)IBOutletUIView*bottomWhiteView;@property(weak,nonatomic)IBOutletUIView*bottomBackView;@end@implementationViewController-(void)viewDidLoad { [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.self.bottomWhiteView.layer.masksToBounds =YES; [selfconfigLayer:self.topWhiteView.layer]; [selfconfigLayer:self.bottomWhiteView.layer]; [selfconfigLayer:self.bottomBackView.layer];}-(void)configLayer:(CALayer*)aLayer{ aLayer.cornerRadius =20.0f;//圆角aLayer.borderWidth =5.0f;//边框线宽aLayer.borderColor = [UIColoryellowColor].CGColor;//边框颜色aLayer.shadowColor = [UIColorblackColor].CGColor;//阴影颜色aLayer.shadowOpacity =0.5f;//阴影透明度aLayer.shadowOffset =CGSizeMake(5.0f,5.0f);//阴影偏移量}@end
运行效果:
Paste_Image.png
shadowRadius
shadowRadius属性可以控制CALayer阴影的模糊度,在viewDidLoad方法的最后加上一句:
self.topWhiteView.layer.shadowRadius=10.0f;
运行结果:
Paste_Image.png
shadowPath
shadowPath可以为CALayer指定特定的阴影路径。
举个栗子:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIView*orangeView;@property(weak,nonatomic)IBOutletUIView*cyanView;@end@implementationViewController-(void)viewDidLoad { [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.[selfconfigLayer:self.orangeView.layer]; [selfconfigLayer:self.cyanView.layer];//create a square shadowCGMutablePathRefsquarePath =CGPathCreateMutable();CGPathAddRect(squarePath,NULL,self.cyanView.bounds);self.cyanView.layer.shadowPath = squarePath;CGPathRelease(squarePath);//create a circular shadowCGMutablePathRefcirclePath =CGPathCreateMutable();CGPathAddEllipseInRect(circlePath,NULL,self.orangeView.bounds);self.orangeView.layer.shadowPath = circlePath;CGPathRelease(circlePath);}-(void)configLayer:(CALayer*)aLayer{ aLayer.shadowOpacity =0.5f; aLayer.cornerRadius = aLayer.bounds.size.width /2.0f; aLayer.shadowOffset =CGSizeMake(0.0f,0.0f); aLayer.shadowRadius =5.0f;}@end
运行效果:
Paste_Image.png
mask
mask图层定义了父图层的部分可见区域,mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。
举个栗子:
Interface Build布局如下:
Paste_Image.png
使用下面这个小雪人来充当图层蒙版:
3.png
代码如下:
#import"ViewController.h"@interfaceViewController()@property(weak,nonatomic)IBOutletUIImageView*imageView;@end@implementationViewController-(void)viewDidLoad { [superviewDidLoad];// Do any additional setup after loading the view, typically from a nib.//create mask layerCALayer*maskLayer = [CALayerlayer]; maskLayer.frame =self.imageView.bounds;UIImage*maskImage = [UIImageimageNamed:@"3"]; maskLayer.contents = (__bridgeid)maskImage.CGImage; maskLayer.contentsGravity = kCAGravityCenter; maskLayer.contentsScale = [UIScreenmainScreen].scale;//apply mask to image layerself.imageView.layer.mask = maskLayer;}@end
运行效果:
Paste_Image.png
每一个UIView内部都默认关联着一个CALayer,我们可用称这个Layer为Root Layer(根层)
所有的非Root Layer,也就是手动创建的CALayer对象,都存在着隐式动画
什么是隐式动画?
当对非Root Layer的部分属性进行修改时,默认会自动产生一些动画效果
而这些属性称为Animatable Properties(可动画属性)
列举几个常见的Animatable Properties:
bounds:用于设置CALayer的宽度和高度。修改这个属性会产生缩放动画
backgroundColor:用于设置CALayer的背景色。修改这个属性会产生背景色的渐变动画
position:用于设置CALayer的位置。修改这个属性会产生平移动画
......
1.UIView可以通过subviews属性访问所有的子视图,类似地,CALayer也可以通过sublayers属性访问所有的子层
2.UIView可以通过superview属性访问父视图,类似地,CALayer也可以通过superlayer属性访问父层
3.特别注意:如果一个控件是另外一个控件的子控件,那么这个控件的layer也是另一个控件的子layer。
查看苹果的官方文档,下面的属性都可以通过KVC进行设置。
//
// YYMylayer.m
// 05-自定义layer(1)
//
// Created by apple on 14-6-21.
// Copyright (c) 2014年 itcase. All rights reserved.
//
#import "YYMylayer.h"
@implementation YYMylayer
//重写该方法,在该方法内绘制图形
-(void)drawInContext:(CGContextRef)ctx
{
//1.绘制图形
//画一个圆
CGContextAddEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
//设置属性(颜色)
// [[UIColor yellowColor]set];
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
//2.渲染
CGContextFillPath(ctx);
}
@en
//
// YYViewController.m
// 05-自定义layer(1)
//
// Created by apple on 14-6-21.
// Copyright (c) 2014年 itcase. All rights reserved.
//
#import "YYViewController.h"
#import "YYMylayer.h"
@interface YYViewController ()
@end
@implementation YYViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//1.创建自定义的layer
YYMylayer *layer=[YYMylayer layer];
//2.设置layer的属性
layer.backgroundColor=[UIColor brownColor].CGColor;
layer.bounds=CGRectMake(0, 0, 200, 150);
layer.anchorPoint=CGPointZero;
layer.position=CGPointMake(100, 100);
layer.cornerRadius=20;
layer.shadowColor=[UIColor blackColor].CGColor;
layer.shadowOffset=CGSizeMake(10, 20);
layer.shadowOpacity=0.6;
[layer setNeedsDisplay];
//3.添加layer
[self.view.layer addSublayer:layer];
}
@end
注意点:
(1)默认为无色,不会显示。要想让绘制的图形显示出来,还需要设置图形的颜色。注意不能直接使用UI框架中的类
(2)在自定义layer中的-(void)drawInContext:方法不会自己调用,只能自己通过setNeedDisplay方法调用,在view中画东西DrawRect:方法在view第一次显示的时候会自动调用。
实现效果:
#import "YYVIEW.h"
@implementation YYVIEW
- (void)drawRect:(CGRect)rect
{
//1.获取上下文
CGContextRef ctx=UIGraphicsGetCurrentContext();
//2.绘制图形
CGContextAddEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
//设置属性(颜色)
// [[UIColor yellowColor]set];
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
//3.渲染
CGContextFillPath(ctx);
//在执行渲染操作的时候,本质上它的内部相当于调用了下面的方法
[self.layer drawInContext:ctx];
}
说明:在UIView中绘制图形,获取的上下文就是这个view对应的layer的上下文。在渲染的时候,就是把图形渲染到对应的layer上。
在执行渲染操作的时候,本质上它的内部相当于执行了 [self.layer drawInContext:ctx];
二、第二种方式
方法描述:设置CALayer的delegate,然后让delegate实现drawLayer:inContext:方法,当CALayer需要绘图时,会调用delegate的drawLayer:inContext:方法进行绘图。
代码示例:
//
// YYViewController.m
// 06-自定义layer(2)
//
// Created by apple on 14-6-21.
// Copyright (c) 2014年 itcase. All rights reserved.
#import "YYViewController.h"
@interface YYViewController ()
@end
@implementation YYViewController
- (void)viewDidLoad
{
[super viewDidLoad];
//1.创建自定义的layer
CALayer *layer=[CALayer layer];
//2.设置layer的属性
layer.backgroundColor=[UIColor brownColor].CGColor;
layer.bounds=CGRectMake(0, 0, 200, 150);
layer.anchorPoint=CGPointZero;
layer.position=CGPointMake(100, 100);
layer.cornerRadius=20;
layer.shadowColor=[UIColor blackColor].CGColor;
layer.shadowOffset=CGSizeMake(10, 20);
layer.shadowOpacity=0.6;
//设置代理
layer.delegate=self;
[layer setNeedsDisplay];
//3.添加layer
[self.view.layer addSublayer:layer];
}
-(void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx
{
//1.绘制图形
//画一个圆
CGContextAddEllipseInRect(ctx, CGRectMake(50, 50, 100, 100));
//设置属性(颜色)
// [[UIColor yellowColor]set];
CGContextSetRGBFillColor(ctx, 0, 0, 1, 1);
//2.渲染
CGContextFillPath(ctx);
}
@end
注意点:不能再将某个UIView设置为CALayer的delegate,因为UIView对象已经是它内部根层的delegate,再次设置为其他层的delegate就会出问题。
在设置代理的时候,它并不要求我们遵守协议,说明这个方法是nsobject中的,就不需要再额外的显示遵守协议了。
提示:以后如果要设置某个类的代理,但是这个代理没要求我们遵守什么特定的协议,那么可以认为这个协议方法是NSObject里边的。
三、补充说明
(1)无论采取哪种方法来自定义层,都必须调用CALayer的setNeedsDisplay方法才能正常绘图。
(2)详细现实过程:
当UIView需要显示时,它内部的层会准备好一个CGContextRef(图形上下文),然后调用delegate(这里就是UIView)的drawLayer:inContext:方法,并且传入已经准备好的CGContextRef对象。而UIView在drawLayer:inContext:方法中又会调用自己的drawRect:方法。平时在drawRect:中通过UIGraphicsGetCurrentContext()获取的就是由层传入的CGContextRef对象,在drawRect:中完成的所有绘图都会填入层的CGContextRef中,然后被拷贝至屏幕。