第一篇:CALayer基础知识
参考书籍:iOS核心动画
1.png我们打开QuartzCore框架
的QuartzCore.h
文件会发现其中只包含了<QuartzCore/CoreAnimation.h>
一个头文件,也就是说QuartzCore
和CoreAnimation
可以看做是一对同义词,iOS设备的所有视觉反馈都是由这个框架来支持的。
当然Core Animation翻译过来是核心动画,这个名字听起来多少让人有点误解,会以为它仅仅是用来做动画的,但其实动画仅仅是Core Animation的一部分特性,CALayer及CALayer的呈现能力也属于Core Animation的内容。
所以这次将从Core Animation的呈现特性(CALayer及CALayer的呈现能力,前五篇)和Core Animation的动画特性(后四篇)两个方面来学习下Core Animation。
捎带提一下:
- CoreGraphics框架也称为Quartz2D框架,是UIKit下的用来绘图的框架,它提供的都是C语言的函数接口,在iOS和macOS是通用的。UIBezierPath是CoreGraphics的封装,使用它可以完成大部分的绘图操作,不过更底层的CoreGraphics更加强大。
- 看到后面我们经常会发现在使用Core Animation框架里的东西时,后面经常需要使用
xxx.CGxxx
,是因为Core Animation是同时支持iOS和macOS的,为了跨平台的特性,它的某些属性就不能使用UIKit里的类型了,因为UIKit只能使用于iOS,而CoreGraphics框架是跨平台的,所以CALayer类的某些属性就使用了CGxxx类型。
目录
一、iOS中的坐标系统
1、坐标系与单位
2、CALayer和UIView的三大布局属性
3、CALayer和UIView的坐标转换
二、CALayer和UIView的联系和区别
1、CALayer和UIView的联系
2、CALayer和UIView的区别
三、CALayer的寄宿图属性
1、contents和contentsScale属性
2、contentsGravity和masksToBounds属性
3、contentsRect属性
4、contentsCenter属性
一、iOS中的坐标系统
1、坐标系与单位
iOS中的坐标系是以左上角作为(0,0)点的,常用的单位有点(point)、像素(pixel)和单位坐标(0~1的一个值),下面将具体介绍这三个单位。
点(point):就是我们常说的开发分辨率,我们开发的时候用的就是这个单位。
像素(pixel):就是我们常说的物理分辨率,就是机子的分辨率,UI给我们切图的时候用的是这个单位。
单位坐标(0~1的一个值):单位坐标不再是一个具体的数值,而是一个0~1之间的数,代表的是一个比例,因此这种方式在某些情况下会很方便。
在iPhone4之前,开发分辨率和物理分辨率是一样的,所以1个点就是1个像素,我们只需要用1x的图片就可以了。但是iPhone4之后采用了Retina显示屏,1个点不再是1个像素,而是2x2个像素,所以要用2x的图片来保证和普通屏幕上的显示效果一样。iPhone-Plus和iPhoneX上的一个点则是3x3个像素,所以要用3x的图片。以下列出了各机型的开发分辨率和物理分辨率:
机型 | 开发分辨率(pt) | 物理分辨率(px) |
---|---|---|
iPhone3G | 320x480 | 320x480 |
iPhone4 | 320x480 | 640x960 |
iPhone5 | 320x568 | 640x1136 |
iPhone6 | 375x667 | 750x1334 |
iPhone-Plus | 414x736 | 1242x2208 |
iPhoneX | 375x812 | 1125x2436 |
- 此外,这里指出一点,UIView是处于二维坐标系下的,而CALayer是处于三维坐标系下的,CALayer有一个zPosition的属性来表明它在Z轴方向上的位置。大多数情况下,我们并不常用这个属性,一般就是用它来做一下Z轴方向上的变换或者调整一下图层的显示顺序。zPosition的默认值为0,值越大,图层显示越靠前。UIView的bringSubViewToFront或者sendViewToBack也能达到改变视图显示顺序的效果,但是这两个方法会改变(父视图.subViews)这个数组里元素的顺序,而采用改变zPosition值的方法,只会改变视图或者图层的显示顺序,并不会改变(父视图.subViews)或者(父图层.subLayers)数组内元素的顺序。
2、CALayer和UIView的三大布局属性
UIView | CALayer |
---|---|
frame(相对于父视图的坐标) | frame(相对于父图层的坐标) |
bounds(相对于视图本身的坐标,因此buonds的x和y值总是0) | bounds(相对于图层本身的坐标,因此buonds的x和y值总是0) |
center(视图的中心点) | position(图层的中心点) |
-- | anchorPoint(CALayer其实比UIView多出来一个anchorPoint属性来改变它的布局,比如做时钟的指针时就得通过改变layer的anchorPoint才能达到想要的结果) |
从大的方面来说:UIView的frame、bounds和center仅仅是CALayer的frame、bounds和position的存取方法,当我们操作UIView的frame、bounds和center时,本质其实是在操作该view所关联的layer的frame、bounds和position。
从小的方面来说:UIView和CALayer的frame其实是一个虚拟属性,它其实是根据frame、bounds、center/position、transform属性等计算得来的。因此修改其它几个值frame就会跟着改变,同理修改frame其它几个值也会作出相应地改变。例如(以UIView举例,CALayer同理):
// 变换前
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(100, 100, 100, 100);
[self.view addSubview:redView];
// 变换后
UIView *redView = [[UIView alloc] init];
redView.backgroundColor = [UIColor redColor];
redView.frame = CGRectMake(100, 100, 100, 100);
redView.transform = CGAffineTransformMakeRotation(M_PI_4);
[self.view addSubview:redView];
变换前 | 变换后 |
---|---|
frame = {100,100,100,100} | frame = {79.3,79.3,141.4,141.4} |
bounds = {0,0,100,100} | bounds = {0,0,100,100} |
center = {150,150} | center={150,150} |
由此这里的旋转变换我们可以看出:frame其实不是视图本身,而是视图旋转之后覆盖了的矩形区域,也就是说frame的宽和高并不总是和bounds的宽和高相等的。
3、CALayer和UIView的坐标转换
我们知道子视图或子图层都是依据其父视图或父图层来布局的,如果移动了父视图或父图层,那么子视图或子图层也会跟着做相应的变化,因此我们可以利用这个特性将一些子视图或者子图层添加到一个父视图或者父图层上来整体做变化。
因此,我们默认获取到的视图或图层的坐标都是相对于其父视图或父图层的,但有时我们并不总是想要获得这样的坐标,而是希望获得某个视图或图层相对与某个指定的视图或图层的坐标。这种情况下,我们就用到了坐标转换技术(以UIView为例,CALayer同理):
// 方式1:
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
greenView.backgroundColor = [UIColor greenColor];
[redView addSubview:greenView];
CGRect newGreenRect = [redView convertRect:greenView.frame toView:self.view];
NSLog(@"%@===%@", NSStringFromCGRect(greenView.frame), NSStringFromCGRect(newGreenRect));
// 方式2:
UIView *redView = [[UIView alloc] initWithFrame:CGRectMake(100, 100, 100, 100)];
redView.backgroundColor = [UIColor redColor];
[self.view addSubview:redView];
UIView *greenView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 50, 50)];
greenView.backgroundColor = [UIColor greenColor];
[redView addSubview:greenView];
CGRect newGreenRect = [self.view convertRect:greenView.frame fromView:redView];
NSLog(@"%@===%@", NSStringFromCGRect(greenView.frame), NSStringFromCGRect(newGreenRect));
两种方式的输出都是:{{0, 0}, {50, 50}}==={{100, 100}, {50, 50}。可见我们完成了坐标的转换。下面列出较完整的坐标转换的方法:
UIView | CALayer |
---|---|
[view1 convertRect:rect toView:view2];(view1把自己坐标系下的某个rect转换到view2坐标系下,并返回转换后的rect) | [layer1 convertRect:rect toLayer:layer2]; |
[view1 convertRect:rect fromView:view2];(view1把view2坐标系下的某个rect转换到自己坐标系下,并返回转换后的rect) | [layer1 convertRect:rect fromLayer:layer2]; |
[view1 convertPoint:point toView:view2];(view1把自己坐标系下的某个point转换到view2坐标系下,并返回转换后的point) | [layer1 convertPoint:point toLayer:layer2]; |
[view1 convertPoint:point fromView:view2];(view1把view2坐标系下的某个point转换到自己坐标系下,并返回转换后的point) | [layer1 convertPoint:point fromLayer:layer2]; |
二、CALayer和UIView的联系和区别
UIView:我们在开发中显示在屏幕上的东西一般都是UIView或其子类,我们可以在view上显示一些背景色、文本或者图片等内容,还可以做动画。UIView可以拦截我们的用户操作并作出响应。iOS系统为我们提供了一个视图树来维护所有存在于屏幕上的视图。
CALayer:同样地,CALayer的概念类似于UIView,我们同样可以在layer上显示一些背景色、文本或者图片等内容,也可以做动画。但是CALayer并不关心响应者链。iOS系统同样为我们提供了一个图层数来维护所有存在的图层。
1、CALayer和UIView的联系
- 我们开发中创建的要显示在屏幕上的东西一般都是UIView及其子类,但其实真正负责显示在屏幕上和做动画的是CALayer,因为每个UIView都默认关联了一个CALayer,并且view都是它所关联的layer的代理;
- 换句话说,UIView其实仅仅是对CALayer做了一层封装,提供了一些更加容易使用的API,并且添加了处理用户交互的功能;
2、CALayer和UIView的区别
首先声明,大多数情况下我们只需要使用UIView来做开发就够了,因为UIView的很多API更加高级和容易使用,而且还提供了处理用户交互的功能,我们就不必额外维护响应者链。但是在某些情况下,我们还是需要直接使用CALayer来实现某些效果,因为CALayer更加底层,所以它具备了很多UIView不具备的能力。
-
CALayer和UIView最大的区别就是:UIView能够响应用户交互,而CALayer则不关心响应者链(当然了,如果非要CALayer响应事件,也是可以通过-hitTest来实现的,下一篇会说到);
-
但是因为CALayer更加底层,所以它提供了一些UIView不具备的额外能力,例如:
-
视觉效果--切圆角、设置边框、设置阴影效果、使用寄宿图和mask属性切出指定形状的layer等这些基本操作;
-
因为CALayer处于三维坐标系下,所以它可以做3D变换,而UIView处于二位坐标系下,只能做二维变换;
-
CALayer提供了诸如CATextLayer、CAShapeLayer、CAGradientLayer等专用图层,某些场景下使用起来会很方便解决问题,另一方面也能提高绘图性能;
-
虽然UIView也提供了做动画的Api,但是CALayer层的动画效果是更丰富的。
-
三、CALayer的寄宿图属性
那么在开始学习CALayer的额外能力之前,我们再看一个CALayer一个好玩的属性,它就是contents,挺有意思的,通过它我们给任意一个UIView直接设置图片,而不是非要UIImageView了,哈哈。
1、contents和contentsScale属性
- contents属性:CALayer有一个contents的属性,是一个id类型,但是其实只能给它赋值图片,赋值其它的类型将会是空白没有效果
- contentsScale属性:contentsScale是一个控制Retina显示屏的参数,默认值为1,代表一个点会显示一个像素;如果值改为2,则每个点会显示2x2个像素,如果改为3,则每个点会显示3x3个像素点;所以我们使用寄宿图属性的时候一定要记得把这个值设为[UIScreen mainScreen].scale,否则图片在Retina显示屏上就会显示出错
//
// ViewController2.m
// CoreAnimation
//
// Created by 意一yiyi on 2017/12/6.
// Copyright © 2017年 意一yiyi. All rights reserved.
//
#import "ViewController2.h"
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define kScreenHeight [UIScreen mainScreen].bounds.size.height
@interface ViewController2 ()
@property (strong, nonatomic) CALayer *customLayer;
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark - layoutUI
- (void)layoutUI {
self.view.backgroundColor = [UIColor whiteColor];
[self.view.layer addSublayer:self.customLayer];
}
#pragma mark - setter, getter
- (CALayer *)customLayer {
if (_customLayer == nil) {
_customLayer = [[CALayer alloc] init];
_customLayer.frame = CGRectMake(0, 100, kScreenWidth, 100);
_customLayer.backgroundColor = [UIColor redColor].CGColor;
// 1、CALayer有一个contents的属性,是一个id类型,但是其实只能给它赋值图片,赋值其它的类型将会是空白没有效果
_customLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
// 2、contentsScale是一个控制Retina显示屏的参数,默认值为1,代表一个点会显示一个像素;如果值改为2,则每个点会显示2x2个像素,如果改为3,则每个点会显示3x3个像素点;所以我们使用寄宿图属性的时候一定要记得把这个值设为[UIScreen mainScreen].scale,否则图片在Retina显示屏上就会显示出错
_customLayer.contentsScale = [UIScreen mainScreen].scale;
}
return _customLayer;
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
1.png
2、contentsGravity和masksToBounds属性
但是我们看到上面效果中,图片被拉伸了,我们就可以使用contentsGravity属性来设置一下寄宿图的显示模式,并且也可以用masksToBounds属性来决定要不要切边。
- contentsGravity属性:对应于UIViewContentMode,CALayer也有一个属性来决定图片显示的模式,是cententsGravity
UIView | CALayer | 效果 |
---|---|---|
UIViewContentModeScaleToFill | kCAGravityResize | 缩放图片,缩放的方式为使得整个图片充满整个imageView,图片会变形 |
UIViewContentModeScaleAspectFill | kCAGravityResizeAspectFill | 缩放图片,缩放的方式为等宽高比缩放,然后充满整个 imageView,图片有可能会超出imageView的边界,当然我们可以通过clipsToBounds或者masksToBounds减掉超出边界的部分 |
UIViewContentModeScaleAspectFit | kCAGravityResizeAspect | 缩放图片,缩放的方式为等宽高比缩放,然后适应imageView,图片会完整地显示在imageView上,但是有可能图片撑不满imageView而使得imageView留下空白的部分 |
UIViewContentModeCenter | kCAGravityCenter | 图片保持原大小不进行缩放,显示在正中间 |
UIViewContentModeTop | kCAGravityTop | 类上 |
UIViewContentModeLeft | kCAGravityLeft | 类上 |
UIViewContentModeBottom | kCAGravityBottom | 类上 |
UIViewContentModeRight | kCAGravityRight | 类上 |
UIViewContentModeTopLeft | kCAGravityTopLeft | 类上 |
UIViewContentModeTopRight | kCAGravityTopRight | 类上 |
UIViewContentModeBottomLeft | kCAGravityBottomLeft | 类上 |
UIViewContentModeBottomRight | kCAGravityBottomRight | 类上 |
- masksToBounds属性:对应于UIView的clipsToBounds,CALayer也有一个属性是masksToBounds来切掉超出layer边界以外的部分
UIView | CALayer |
---|---|
clipsToBounds | masksToBounds |
修改部分代码如下:
- (CALayer *)customLayer {
if (_customLayer == nil) {
_customLayer = [[CALayer alloc] init];
_customLayer.frame = CGRectMake(0, 100, kScreenWidth, 100);
_customLayer.backgroundColor = [UIColor redColor].CGColor;
// 1、CALayer有一个contents的属性,是一个id类型,但是其实只能给它赋值图片,赋值其它的类型将会是空白没有效果
_customLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
// 2、contentsScale,是一个控制Retina显示屏的参数,默认值为1,代表一个点会显示一个像素;如果值改为2,则每个点会显示2x2个像素,如果改为3,则每个点会显示3x3个像素点;所以我们使用寄宿图属性的时候一定要记得把这个值设为[UIScreen mainScreen].scale,否则图片在Retina显示屏上就会显示出错
_customLayer.contentsScale = [UIScreen mainScreen].scale;
// 3、对应于UIViewContentMode,CALayer也有一个属性来决定图片显示的模式,是cententsGravity
_customLayer.contentsGravity = kCAGravityCenter;
// 4、对应于UIView的clipsToBounds,CALayer也有一个属性是masksToBounds来切掉超出layer边界以外的部分
_customLayer.masksToBounds = YES;
}
return _customLayer;
}
1.png
3、contentsRect属性
- 这个属性也是寄宿图比较好玩的一个属性,它允许我们在layer上显示寄宿图的某一个子区域,它采用的是单位坐标。我们在App中经常用它来载入拼合图片,因为一次性载入一张拼合的大图要比多次载入多张小图在内存占用、渲染性能上都要高。但这个属性也只是对图片有作用,其它类型的对象不起作用。
我们修改部分代码如下:
- (CALayer *)customLayer {
if (_customLayer == nil) {
_customLayer = [[CALayer alloc] init];
_customLayer.frame = CGRectMake(0, 100, kScreenWidth, 100);
_customLayer.backgroundColor = [UIColor redColor].CGColor;
// 1、CALayer有一个contents的属性,是一个id类型,但是其实只能给它赋值图片,赋值其它的类型将会是空白没有效果
_customLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
// 2、contentsScale,是一个控制Retina显示屏的参数,默认值为1,代表一个点会显示一个像素;如果值改为2,则每个点会显示2x2个像素,如果改为3,则每个点会显示3x3个像素点;所以我们使用寄宿图属性的时候一定要记得把这个值设为[UIScreen mainScreen].scale,否则图片在Retina显示屏上就会显示出错
_customLayer.contentsScale = [UIScreen mainScreen].scale;
// 3、对应于UIViewContentMode,CALayer也有一个属性来决定图片显示的模式,是cententsGravity
_customLayer.contentsGravity = kCAGravityCenter;
// 4、对应于UIView的clipsToBounds,CALayer也有一个属性是masksToBounds来切掉超出layer边界以外的部分
_customLayer.masksToBounds = YES;
// 5、contentsRect属性允许我们在layer上显示寄宿图的某一个子区域,它采用的是单位坐标。我们在App中经常用它来载入拼合图片,因为一次性载入一张拼合的大图要比多次载入多张小图在内存占用、渲染性能上都要高。但这个属性也只是对图片有作用,其它类型的对象不起作用
CALayer *chun = [[CALayer alloc] init];
chun.frame = CGRectMake(0, 200, 100, 100);
chun.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
chun.contentsScale = [UIScreen mainScreen].scale;
chun.contentsGravity = kCAGravityCenter;
// 截取春
chun.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
[self.view.layer addSublayer:chun];
CALayer *xia = [[CALayer alloc] init];
xia.frame = CGRectMake(100, 200, 100, 100);
xia.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
xia.contentsScale = [UIScreen mainScreen].scale;
xia.contentsGravity = kCAGravityCenter;
// 截取夏
xia.contentsRect = CGRectMake(0.5, 0, 0.5, 0.5);
[self.view.layer addSublayer:xia];
CALayer *qiu = [[CALayer alloc] init];
qiu.frame = CGRectMake(200, 200, 100, 100);
qiu.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
qiu.contentsScale = [UIScreen mainScreen].scale;
qiu.contentsGravity = kCAGravityCenter;
// 截取秋
qiu.contentsRect = CGRectMake(0, 0.5, 0.5, 0.5);
[self.view.layer addSublayer:qiu];
CALayer *dong = [[CALayer alloc] init];
dong.frame = CGRectMake(300, 200, 100, 100);
dong.contents = (__bridge id _Nullable)([UIImage imageNamed:@"春夏秋冬"].CGImage);
dong.contentsScale = [UIScreen mainScreen].scale;
dong.contentsGravity = kCAGravityCenter;
// 截取冬
dong.contentsRect = CGRectMake(0.5, 0.5, 0.5, 0.5);
[self.view.layer addSublayer:dong];
}
return _customLayer;
}
1.png
4、contentsCenter属性
- 这个属性看着像是用来调整寄宿图的位置的,但其实它是用来设置寄宿图不可被拉伸的区域的,对应于UIImage的resizableImage方法,只不过坐标是单位坐标。
UIImage | CALayer |
---|---|
resizableImage | contentsCenter |
例如(11, 22, 11, 22)代表上起11左起22下起11右起22的范围隔出来的四个角(左上、右上、左下、右下四个角)是不可被拉伸的 | 例如(0.25, 0.25, 0.5, 0.5)代表左边起0.25-0.75的范围及上边起0.25~0.75的范围隔出来的四个角(左上、右上、左下、右下四个角)是不可被拉伸的 |
以聊天气泡举例:
//
// ViewController2.m
// CoreAnimation
//
// Created by 意一yiyi on 2017/12/6.
// Copyright © 2017年 意一yiyi. All rights reserved.
//
#import "ViewController2.h"
#define kScreenWidth [UIScreen mainScreen].bounds.size.width
#define kScreenHeight [UIScreen mainScreen].bounds.size.height
@interface ViewController2 ()
@property (strong, nonatomic) CALayer *customLayer;
@end
@implementation ViewController2
- (void)viewDidLoad {
[super viewDidLoad];
[self layoutUI];
}
#pragma mark - layoutUI
- (void)layoutUI {
self.view.backgroundColor = [UIColor whiteColor];
// 原图
UIImageView *sourceImageView = [[UIImageView alloc] init];
sourceImageView.frame = CGRectMake(0, 100, kScreenWidth, 100);
sourceImageView.backgroundColor = [UIColor yellowColor];
sourceImageView.image = [UIImage imageNamed:@"聊天气泡"];
sourceImageView.contentMode = UIViewContentModeCenter;
[self.view addSubview:sourceImageView];
// 通过UIImage的resizableImage方法来设置不可拉伸区域
UIImageView *qipao = [[UIImageView alloc] init];
qipao.frame = CGRectMake(0, 250, kScreenWidth, 100);
qipao.backgroundColor = [UIColor yellowColor];
// 设置图片不可被拉伸的区域
UIImage *image = [[UIImage imageNamed:@"聊天气泡"] resizableImageWithCapInsets:UIEdgeInsetsMake(100, 22, 0, 22) resizingMode:(UIImageResizingModeStretch)];
qipao.image = image;
[self.view addSubview:qipao];
//6、contentsCenter:对应于UIImage的resizableImage,CALayer的contentsCenter属性也是用来设置图片不可被拉伸的区域,只不过它用的是单位坐标。
CALayer *qipaoLayer = [[CALayer alloc] init];
qipaoLayer.frame = CGRectMake(0, 400, kScreenWidth, 100);
qipaoLayer.backgroundColor = [UIColor yellowColor].CGColor;
qipaoLayer.contentsScale = [UIScreen mainScreen].scale;
qipaoLayer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"聊天气泡"].CGImage);
// 设置图片不可被拉伸的区域
qipaoLayer.contentsCenter = CGRectMake(0.25, 1, 0, 0.5);
[self.view.layer addSublayer:qipaoLayer];
}
- (void)didReceiveMemoryWarning {
[super didReceiveMemoryWarning];
// Dispose of any resources that can be recreated.
}
@end
1.png