【iOS】CALayer的认识

在iOS中,按钮、文本框、图片等所有能看的见的和处理事件响应的基本都是UIView。但是UIView之所以能够显示到屏幕上,完全是因为它内部的图层(Layer)。

一、CALayer介绍

在创建UIView对象时,UIView内部会自动创建一个图层,这个图层就是CALayer对象,通过UIViewlayer属性可以访问到这个属性。layer属性从来不会是空的,而且官方把UIView描述为是图层的一个代理。

1
@property(nonatomic,readonly,strong) CALayer *layer;

UIView需要显示到屏幕上时,会调用drawRect:方法进行绘图,并且会把所有内容都绘制到自己的图层上,绘图结束后,系统会把图层拷贝到屏幕上,所以我们就看到了UIView的内容。所以UIView本身不具备显示功能,是它内部的layer才有显示功能。

1.1. 基本使用

通过操作CALayer对象,可以很方便地调整UIView的一些外观属性。例如,阴影,圆角、边框、内容等。也可以给图层添加动画,很多动画都是基于图层实现的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 1.设置边框
// 1.1.设置边框颜色
self.testView.layer.borderColor = [UIColor blueColor].CGColor;
// 1.2.设置边框宽度
self.testView.layer.borderWidth = 5.0;

// 2.设置圆角
self.testView.layer.cornerRadius = self.testView.bounds.size.height/2;

// 3.设置阴影
// 3.1.设置阴影不透明度(0~1,默认0)
self.testView.layer.shadowOpacity = 1;
// 3.2.设置阴影偏移量
self.testView.layer.shadowOffset = CGSizeMake(10.0, 10.0);
// 3.3.设置阴影颜色
self.testView.layer.shadowColor = [UIColor orangeColor].CGColor;

UIImageView添加同样的属性,看下效果:

为什么没有裁剪成圆呢?这是因为UIImageView的图片是在layer里面的contents上面的。而cornerRadius是操纵layer的,无法直接操纵contents上面的内容,所以看到的效果就是没有裁剪。

如果要对图片进行裁剪,需要手动的调用一个方法,把超过视图的所有内容都裁剪掉,但是这样也会把阴影也裁掉,因为阴影本身也是在视图外的。

1
view.clipsToBounds = YES;

还有一个针对layer的裁剪方法,其实clipsToBounds本质也操作的layer

1
view.layer.masksToBounds = YES;

1.2. 离屏渲染

用上面的两种方法会造成离屏渲染,裁剪图像建议使用Quartz2D。

什么是离屏渲染?
处理图像分为CPU和GPU两部分,GPU是专门处理图像的。但是本应该交给CPU处理的事情交给了GPU处理,GPU又不擅长处理CPU的事情,所以GPU又开辟了一块新的内存专门处理CPU交代的事情。等处理完成后,GPU和CPU需要把结果合并到一起,在合并过程中比较消耗性能的,这就造成了离屏渲染。

真正造成离屏渲染的是masksToBounds。一两个视图的圆角处理可以忽略,但是如果一个tableView上大量使用masksToBounds就会造成卡顿,这时候使用Quartz2D就不会造成离屏渲染了,这也是tableView优化的一部分。

UI控件的位置和尺寸如果出现小数点可能会出现锯齿现象,这时候也会造成离屏渲染。所以要尽量避免出现小数点。

YY大神写的一篇文章非常深入透彻 -> https://blog.ibireme.com/2015/11/12/smooth_user_interfaces_for_ios/

二、CATransform3D

UIView形变属性transformCGAffineTransform类型。CALayer形变属性transformCATransform3D类型,同样也是一个结构体(CATransform3D是四维方形矩阵)。

CA代表CoreAnimation框架,CG代表CoreGraphics框架。

1
2
3
4
5
6
7
8
9
10
11
@property CATransform3D transform;

typedef struct CA_BOXABLE CATransform3D CATransform3D;

struct CATransform3D
{
CGFloat m11, m12, m13, m14;
CGFloat m21, m22, m23, m24;
CGFloat m31, m32, m33, m34;
CGFloat m41, m42, m43, m44;
};

CALayertransformUIView的用法很相似,只是参数不一样。

3D即代表是个三维空间,坐标系有x、y、z轴。

2.1. 旋转、平移、缩放

2.1.1. 旋转

angle是旋转角度,x、y、z分别代表围绕哪个坐标轴旋转。

绕哪个轴旋转就把对应坐标轴设为1,其他为0。

  • 绕x轴旋转:(angle, 1, 0, 0)
  • 绕y轴旋转:(angle, 0, 1, 0)
  • 绕z轴旋转:(angle, 0, 0, 1)

UIViewtransform旋转就是对根Layer进行z轴旋转。

注意:旋转操作会自动根据最短路径旋转(例如,围绕z轴旋转270°,会逆时针旋转90°;旋转180°时也是逆时针旋转的;旋转45°是顺时针旋转)

1
2
3
CATransform3D CATransform3DMakeRotation (CGFloat angle, CGFloat x, CGFloat y, CGFloat z);

CATransform3D CATransform3DRotate (CATransform3D t, CGFloat angle, CGFloat x, CGFloat y, CGFloat z);

2.1.2. 平移

x、y分表代表二维偏移量,z代表层级,层级高会遮盖层级低的视图。

1
2
3
CATransform3D CATransform3DMakeTranslation (CGFloat tx, CGFloat ty, CGFloat tz);

CATransform3D CATransform3DTranslate (CATransform3D t, CGFloat tx, CGFloat ty, CGFloat tz);

2.1.3. 缩放

缩放操作的是x、y,z轴一般默认1。

1
2
3
CATransform3D CATransform3DMakeScale (CGFloat sx, CGFloat sy, CGFloat sz);

CATransform3D CATransform3DScale (CATransform3D t, CGFloat sx, CGFloat sy, CGFloat sz)

2.1.4. KVC

以上操作还可以通过KVC进行设置:

1
2
NSValue *value = [NSValue valueWithCATransform3D:CATransform3DMakeRotation(M_PI_4, 0, 0, 1)];
[self.testImgView.layer setValue:value forKeyPath:@"transform"];

使用KVC场景:主要用来做快速形变操作,即只有一个值的操作。
例:
只做x轴的平移操作:

1
[self.testImgView.layer setValue:@(100) forKeyPath:@"transform.translation.x"];

只做缩放操作:

1
[self.testImgView.layer setValue:@(1.2) forKeyPath:@"transform.scale"];

只做绕z轴旋转操作:

1
[self.testImgView.layer setValue:@(M_PI) forKeyPath:@"transform.rotation.z"];

二、自定义CALayer

CALayer的创建形式和UIView类似。

2.1. 创建CALayer

1
2
3
4
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor redColor].CGColor;
layer.frame = CGRectMake(20.0, 20.0, 100.0, 100.0);
[self.view.layer addSublayer:layer];

2.2. CALayer的内容

CALayer的内容填充最具代表性的是图片,CALayercontents属性就是对内容进行填充,默认为空。

图片会自动填充layer,不需要设置大小。但是需要CGImageRef类型,因此需要对UIImage进行转换。

1
2
3
4
5
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor redColor].CGColor;
layer.frame = CGRectMake(20.0, 20.0, 100.0, 100.0);
layer.contents =(id)[UIImage imageNamed:@"avatar"].CGImage;
[self.view.layer addSublayer:layer];

2.3. 扩展

为什么CALayer的背景色和内容需要转换为CoreGraphics支持的类型。

  • CALayer是定义在QuartzCore框架中的;
  • CGImageRefCGColorRef是定义在CoreGraphics框架中的;
  • UIColorUIImage定义在UIKit框架中。

QuartzCoreCoreGraphics两个框架是跨平台的(MaxOS和iOS),但是UIKit只能在iOS中使用。为了保证可移植性,QuartzCore只能使用CGImageRefCGColorRef

三、CALayer和UIView的区别

通过CALayer能够做出和UIImageView一样的界面效果。既然CALayerUIView都能实现实现相同的效果,那开发中选择谁更好呢?

CALayer继承自NSObject,而UIView继承自UIResponder。所以UIViewCALayer多了一个事件处理的能力,也就是说CALayer不能处理用户的触摸事件。

如果显示的内容需要和用户进行交互,使用UIView;如果不需要交互,两者都可以,但建议选择CALayer,因为 CALayer的性能更好,更加轻量级

反转,反转,实际开发中即使不需要事件处理,也建议使用UIView,不是自相矛盾么?因为 UIView可扩展性更好,开发效率会更高

三、position和anchorPoint的作用

positionanchorPointCALayer的两个非常重要的属性。

position用来设置CALayer在父层中的位置。以父层左上角为原点坐标(0,0)

1
@property CGPoint position;

anchorPoint是锚点(定位点),决定着CALayer的哪个点会在position属性所指的位置。锚点的坐标系是自己本身,以自己左上角为原点坐标(0,0),取值范围是[0, 1],默认值是(0.5, 0.5)

1
@property CGPoint anchorPoint;

positionanchorPoint是始终重合的。

示例

场景:绘制一个红色layer,查看positionanchorPoint坐标点。

步骤一:初始化位置

1
2
3
4
5
6
CALayer *layer = [CALayer layer];
layer.backgroundColor = [UIColor redColor].CGColor;
layer.bounds = CGRectMake(0.0, 0.0, 100.0, 100.0);
[self.view.layer addSublayer:layer];
NSLog(@"%@-%@", NSStringFromCGPoint(layer.position), NSStringFromCGPoint(layer.anchorPoint));
// 输出:{0, 0}-{0.5, 0.5}

步骤二:修改position

1
2
3
4
5
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_layer.position = CGPointMake(50.0, 50.0);
NSLog(@"%@-%@", NSStringFromCGPoint(_layer.position), NSStringFromCGPoint(_layer.anchorPoint));
// 输出:{50, 50}-{0.5, 0.5}
}

步骤三:修改anchorPoint

1
2
3
4
5
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
_layer.anchorPoint = CGPointMake(0.0, 0.0);
NSLog(@"%@-%@", NSStringFromCGPoint(_layer.position), NSStringFromCGPoint(_layer.anchorPoint));
// 输出:{50, 50}-{0.5, 0.5}
}

通过输出发现:positionanchorPoint始终在一个点上,无论修改哪个属性值都会让layer的位置发生改变。默认情况下,layer相对父layer的左上角坐标点是(layer.position.x-layer.width/2, layer.position.y-layer.height/2)UIViewcenter就是内部layer的positionanchorPoint的可移动范围就是layer的自身大小,

四、隐式动画

每一个UIView内部都默认关联着一个CALayer,我们称这个layer为RootLayer(根层)。所有非RootLayer(手动创建的CALayer对象)都存在隐式动画。

通过上面的案例发现,修改自定义layer的部分属性时会有一个动画效果,这个效果是系统自己添加的,这个动画就是隐式动画。属性是否携带动画,苹果在文档注释中都有描述,携带Animatable的就是会产生隐式动画。常见的会产生隐式动画的属性有boundsbackgroundColorposition

取消隐式动画
隐式动画的本质是封装的一个事务,如果要取消隐式动画只需要把事务取消就可以了。

1
[CATransaction setDisableActions:NO];

把需要取消隐式动画的属性操作放在上面代码的后面就可以了。

包装隐式动画
如果需要部分属性执行隐式动画,只需要使用事务把这部分属性上下包住就行了。

1
2
3
4
5
6
7
8
// 1.开启事务
[CATransaction begin];
// 2.事务有效
[CATransaction setDisableActions:NO];
// 3.动画执行时长
[CATransaction setAnimationDuration:5.0];
// 4.提交事务
[CATransaction commit];

例:部分属性添加隐式动画

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 隐式动画时长
[CATransaction setAnimationDuration:3.0];
// layer不需要隐式动画
[CATransaction setDisableActions:YES];

// 为背景色添加隐式动画,并修改动画时长
[CATransaction begin];
[CATransaction setAnimationDuration:5.0];
[CATransaction setDisableActions:NO];
_layer.backgroundColor = [UIColor blueColor].CGColor;
[CATransaction commit];

// 为圆角添加隐式动画
[CATransaction begin];
[CATransaction setDisableActions:NO];
_layer.cornerRadius = 50.0;
[CATransaction commit];

// 平移就没有隐式动画
_layer.position = CGPointMake(150.0, 150.0);