玩转iOS开发:2.《Core Animation》初识CALayer

文章转至个人我的技术博客:https://cainluo.github.io/14771496782021.htmlhtml


做者感言

前面咱们简单介绍了一些图层与视图的关系, 也介绍了CALayer以及UIView的相同点和区别, 没看过的朋友能够去看看《Core Animation》基础概念今天咱们就着重的来说讲CALayer. 最后: 若是你有更好的建议或者对这篇文章有不满的地方, 请联系我, 我会参考大家的意见再进行修改, 联系我时, 请备注Core Animation若是以为好的话, 但愿你们也能够打赏一下~嘻嘻~祝你们学习愉快~谢谢~git


简介

前面咱们已经简单介绍了什么是CALayer, CALayer主要是用来作什么的, 如今咱们来更加深刻的了解CALayer到底有些什么东西供给咱们去使用的.github


CALayer的Contents属性

CALayer中, 有这么一个contents属性, 那么contents这个属性是用来作什么的呢? 咱们先来看一段官方解释.缓存

An object that provides the contents of the layer. Animatable.微信

从字面上意思来看, 这是一个提供图层内容的对象, 而且是id类型, 也就意味着, 咱们能够给这个contents属性赋任意的值, 毕竟是id类型嘛, 但在实际操做中, 是行不通的, 为何?ide

这里就要牵扯到Mac OS了, 由于在Mac OS当中, 给CALayer中的contents属性赋值, 不管是CGImage仍是NSImage, 都能获得对应的效果, 而在iOS当中, 只能赋值CGImage, 或许到了这里, 你会以为挺简单的, 但呵呵了, 这里还要牵扯到指针的问题(我的对指针有些晕).布局

实际上, 咱们给contents属性赋CGImage的时候, 真正赋值的是CGImageRef, 它是指向CGImage的指针, 用过UIImage的朋友应该会发现,UIImage当中有一个CGImage的属性, 这个属性的返回值就是CGImageRef.性能

某些童鞋会说, 既然是CGImageRef类型的话, 那直接赋值给contents不就行了么, 其实并非滴, 直接这么赋值会报编译错误滴, 为何??学习

由于CGImageRef并非一个真正的Cocoa对象, 它是属于Core Foundation里的东西(什么是Core Foundation? 嘿嘿, 自行百度去吧~~), 那么咱们就没办法给contents赋值吗? 确定不是啦, 咱们能够经过一些关键字进行赋值, 就可以获得对应的效果了, 代码以下:ui

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
}
复制代码

这里提一点哈, 这个关键字在非ARC内存管理机制中是不须要加滴, 可是呢, 特么的, 你为啥不用ARC? 估计连你本身都会这么问你本身了.

效果图:

1

看完效果图以后, 咱们再看看它的层级结构, 你就会发现和咱们使用的UIImageView彻底如出一辙, 不信? 我给大家加个UIImageView看看~

2

3

虽然咱们并非经过使用UIImageView来实现加载图片的, 但咱们能够经过Layer层给UIView进行加载图片, 酱紫的话, 你们是否是对苹果如何封装UIImageView有了一个思路呢?


CALayer的ContentGravity属性

不知道大家有没有发现, 咱们所展现的图片有一些变形, 若是是用UIImageView的话, 咱们能够直接设置contentModel这个属性, 使得图片正常显示, 但若是是在CALayer呢?

固然CALayer也有一个相似contentModel的属性, 它叫作contentGravity, 虽然名字有些差异, 可是使用效果都是差很少的~~

进入头文件以后咱们会看到contentGravity能够赋值的选项有:

  • kCAGravityCenter
  • kCAGravityTop
  • kCAGravityBottom
  • kCAGravityLeft
  • kCAGravityRight
  • kCAGravityTopLeft
  • kCAGravityTopRight
  • kCAGravityBottomLeft
  • kCAGravityBottomRight
  • kCAGravityResize
  • kCAGravityResizeAspect
  • kCAGravityResizeAspectFill

如今咱们就来改改工程里的代码:

layerView.layer.contentsGravity = kCAGravityResizeAspect;
复制代码

效果图:

4

酱紫图片显示正确啦~~貌似UIImageViewcontentModel也就是这么实现的~


CALayer的ContentsScale属性

其实我在想, 我是否是不该该把CALayer里的全部属性都拿出来说一讲? 只是简单讲一些重要的好了, 接下来的就是contentsScale.

contentsScale这个属性主要是定义图层的像素和视图比例大小, 默认状况是1.0, 并且是CGFloat类型.

但若是你的CALayer已经设置了contentsGravity, 那么再设置contentsScale, 效果就是没多大影响, 或者直接说压根就没影响吧, 若是你只是想着单纯的放大缩小CALayer, 能够直接使用transformaffineTransForm实现你想要的效果, 后续会详细讲解transforms.

固然放大缩小确定也不是contentsScale属性的主要做用, 这里就须要解释一下contentsScale:

contentsScale主要是支持Retina机制的一部分, 它是用来判断绘制图层时应该须要建立多大的空间, 和须要显示图片的拉伸度,UIView也有一个相似的属性, 叫作contentScaleFactor, 只是咱们很是少的去使用罢了.

这个时候咱们来改改工程里的代码, 让contentsScale呈现效果:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    layerView.layer.contentsGravity = kCAGravityCenter;
    layerView.layer.contentsScale = image.scale;
}
复制代码

效果图:

5


CALayer的MaskToBounds属性

看完了contentsScale属性, 如今咱们继续来看maskToBounds属性.

咱们先来看一段官方文字介绍:

A Boolean indicating whether sublayers are clipped to the layer’s bounds. Animatable.

看完以后, 咱们知道这个属性是一个BOOL类型, 问你若是子图层超出了视图层, 是否剪切掉, 若是你设置为YES, 那就剪切掉了, 默认为NO.

就拿咱们刚刚的工程做为一个事例来说, 那张图片确定是超过了视图层的, 若是咱们把maskToBounds属性设置为YES, 效果就不同了:

- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    layerView.layer.contentsGravity = kCAGravityCenter;
    layerView.layer.contentsScale = image.scale;
    layerView.layer.masksToBounds = YES;
}
复制代码

效果图:

6


CALayer的ContentsRect属性

CALayer有一个属性叫作contentsRect, 它是能够根据输入的坐标轴来显示区域块的图层, 而frame,bounds则是以点来计算的, 在这里它们有一些区别.

还有一个注意点, contentsRect的坐标轴默认是**{0, 0, 1, 1}, 单位坐标通常指定的是0~1**之间, 若是小于这个数, 或者是大于这个数, 哼哼, 你本身试试看吧~

说到这里, 应该会有人有些疑惑, 神马是点? 难道和像素不同的么? 那就先来普及一下先吧(这里我是搜到的一些比较中肯的说法)~


> 点: > > * 在**iOS**和**Mac OS**中最多见的坐标体系。 > * 点就像是虚拟的像素, 也被称做逻辑像素。 > * 在标准设备上, 一个点就是一个像素, 可是在**Retina**设备上, 一个点等于**2*2**个像素。 > * **iOS**用点做为屏幕的坐标测算体系就是为了在**Retina**设备和普通设备上能有一致的视觉效果。
> 像素: > > * 物理像素坐标并不会用来屏幕布局, 可是仍然与图片有相对关系。 > * **UIImage**是一个屏幕分辨率解决方案, 因此指定点来度量大小。 > * 可是一些底层的图片表示如**CGImage**就会使用像素, 因此你要清楚在**Retina**设备和普通设备上, 他们表现出来了不一样的大小。
> 单位: > > * 对于与图片大小或是图层边界相关的显示, 单位坐标是一个方便的度量方式, 当大小改变的时候, 也不须要再次调整。 > * 单位坐标在**OpenGL**这种纹理坐标系统中用得不少,**Core Animation**中也用到了单位坐标。
> 这里咱们仍是继续拿刚刚的工程来演示:
- (void)viewDidLoad {
    [super viewDidLoad];

    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    layerView.layer.contentsGravity = kCAGravityCenter;
    layerView.layer.contentsScale = image.scale;
    layerView.layer.masksToBounds = YES;
    layerView.layer.contentsRect = CGRectMake(0, 0, 0.5, 0.5);
}
复制代码

效果图:

7

补充一些额外的知识点:

contentsRectApp当中, 还有一个更好玩的用法叫作image sprites, 若是你有游戏开发经验的话, 你确定对image sprites不陌生, 甚至是很是熟练的使用, 可使图片独立的变动在屏幕上显示的位置.

但若是咱们抛开游戏开发来讲的话, 在咱们平常生活当中, 微博就是一个经典的表明, 把图片拼接成一张大图片, 而后再分享出去, 这里使用的就是contentsRect, 这样子的好处就是能够减小内存的使用, 载入的时间, 还有渲染的性能等等.

这里我就不作演示了, 感兴趣的童鞋能够 到网上找找资料.


CALayer的ContentsCenter属性

讲到这里, 已经算是CALayer最后的一个属性了, 它叫作contentsCenter, 它的意思比较拗口, 它是一个CGRect, 且定义了一个固定的边框和一个图层上可拉伸的区域.

若是你只是单单改变contentsCenter的值, 并不会影响到CALayer的显示效果, 要同时去改变这个图层的大小, 才能看到效果.

咱们仍是直接看代码吧, 在咱们原先的项目上添加一个方法, 而且加多一个UIView类:

- (void)viewDidLoad {
    [super viewDidLoad];
    
    self.view.backgroundColor = [UIColor lightGrayColor];
    
    UIView *layerView = [[UIView alloc] init];
    layerView.backgroundColor = [UIColor whiteColor];
    layerView.center = self.view.center;
    layerView.bounds = CGRectMake(0, 0, 200, 200);
    
    [self.view addSubview:layerView];
    
    
    UIImage *image = [UIImage imageNamed:@"bear"];
    
    /** * Contents */
    layerView.layer.contents = (__bridge id _Nullable)(image.CGImage);
    
    /** * ContentsGravity */
    layerView.layer.contentsGravity = kCAGravityCenter;
    
    /** * ContentsScale */
    layerView.layer.contentsScale = image.scale;
    
    /** * MasksToBounds */
    layerView.layer.masksToBounds = YES;
    
    /** * contentsRect */
    layerView.layer.contentsRect = CGRectMake(0, 0, 1.1, 1.1);
    
    
    [self addImage:[UIImage imageNamed:@"bear"] withContensRect:CGRectMake(0.25, 0.25, 0.5, 0.5)];
}

- (void)addImage:(UIImage *)image withContensRect:(CGRect)rect {
    
    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)];
    
    view.layer.contents = (__bridge id _Nullable)(image.CGImage);
    view.layer.contentsCenter = rect;
    
    [self.view addSubview:view];
}
复制代码

效果图:

8

补充一个知识点, 若是你是使用Storyboard或者是xib的话, 你能够在右侧的栏目看到contentsCenter

9


CALayer的Delegate

降到这里, 基本上就已经介绍完了CALayer, 但还有一点也是须要提一提的, CALayer除了使用contents赋值CGImage来显示图层以后, 还可使用Core Graphics去进行绘制, 在UIView就能够看到这个方法, 叫作**-drawRect:**.

-drawRect:方法默认没有去实现, 由于在UIView中,backing image并非必须的, 但若是你去调用**-drawRect:方法, 那么UIView就会给你生成一个新的backing image**, 而这个backing image的像素尺寸等于视图大小乘以contentsScale的值.

这里须要注意一个点, 若是你的视图里不须要建立一个backing image的话, 千万不要去写一个空的**-drawRect:**方法, 这样子就会对CPU与内存形成浪费, 这也是苹果官方建议的.

咱们先来解释一下**-drawRect:**方法的实现原理:

  • 当**-drawRect:被调用,UIView会建立一个新的backing image**.
  • 会使用Core Graphicsbacking image进行描绘.
  • 而后这个描绘好的backing image会被缓存起来, 等到它须要被更新的时候, 就会去使用.

固然, 咱们本身也能够手动去调用, 好比去调用**-setNeedsDisplay:, 那么被从新绘制的backing image**就会立马显示出来了.

总而言之, -drawRect:看似是UIView的方法, 但实际上都是在内部对CALayer进行了重绘以及缓存的操做.

还有, CALayer也有一个delegate的属性, 并且是id类型, 并实现CALayerDelegate协议, 当CALayer须要一个特定内容时, 就会从代理方法里去请求, 因为CALayerDelegate是一个非正式的协议, 因此并无神马属性给你引用, 直接调用代理方法就能够了.

当须要被重绘的时候, CALayer就会去调用:

-(void)displayLayer:(CALayer *)layer;
复制代码

若是你还想再重绘的时候设置一下contents的话, 那么就要在这个方法里去实现, 否则在别的方法里就无法作到了.

但若是没有实现以上的方法时, 那么就会去调用:

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
复制代码

在调用这个方法以前, CALayer会建立一个适合尺寸的backing image, 固然, 尺寸确定是由boundscontentsScale决定的, 还有一个Core Graphics绘制的上下文, 未绘制backing image作准备, 等的就是ctx的传入.

说了那么多咱们直接用代码演示吧~

- (void)createNewLayerWithSuperView {
    
    // Background View
    UIView *backgroundView = [[UIView alloc] initWithFrame:CGRectMake(220, 0, 100, 100)];
    backgroundView.backgroundColor = [UIColor whiteColor];
    
    [self.view addSubview:backgroundView];
    
    // Blue Layer
    CALayer *blueLayer = [CALayer layer];
    
    blueLayer.frame = CGRectMake(25, 25, 50, 50);
    blueLayer.backgroundColor = [UIColor blueColor].CGColor;
    
    // Set Layer Delegate
    blueLayer.delegate = self;
    
    // Set Layer contentsScale
    blueLayer.contentsScale = [UIScreen mainScreen].scale;
    
    [backgroundView.layer addSublayer:blueLayer];
    
    [blueLayer display];
}

- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx {
    
    CGContextSetLineWidth(ctx, 5);
    CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
    CGContextStrokeEllipseInRect(ctx, layer.bounds);
}
复制代码

效果图:

10

看到效果图代码以后, 有两点咱们是须要注意一下的:

  • 不一样的CALayer在不一样的UIView视图中使用, 是不会自动去重载它的内容的, 因此在事例当中, 咱们用blueLayer调用了display这个方法.
  • 在事例当中, 咱们并无对blueLayer设置masksToBounds属性, 但所绘制的那个圆仍然被裁剪了一些, 这个是由于咱们在使用CALayerDelegate的时候, 并无让须要描绘的backing image支持超出边界外的支持.

聊到这里, 虽然咱们知道了CALayerDelegate, 但在实际开发当中, 咱们基本上很是很是少去接触它, 由于当UIView建立backing image的时候, 就会默认把CALayerDelegate设置为它本身, 同时也会提供一个**- (void)displayLayer:(CALayer *)layer;**的实现, 因此基本上不会遇到什么问题.

当你使用有backing imageUIView时, 你也没必要实现下面两个方法

- (void)displayLayer:(CALayer *)layer;
    - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
复制代码

由于UIView提供了一个**- (void)drawRect:(CGRect)rect;的方法, 只要你实现了这个方法, 那么剩下的东西UIView**都是所有帮你完成.


总结一下

不得不说, 此次的内容有些多, 仍是来总结一下:

  • contents是给CALayer设置内容的一个属性
  • ContentGravity是给CALayer设置内容的显示, 相似UIViewcontentModel.
  • contentsScale定义图层的像素和视图比例大小, 默认大小为1.0f, 而且是CGFloat类型.
  • maskToBounds是一个BOOL类型, 默认为NO, 若是设置为YES, 则会裁剪掉超出视图的部分.
  • contentsRect是一个坐标轴, 默认是CGRectMake(0, 0, 1, 1), 输入对应的坐标轴, 可让CALayer显示所输入坐标轴的区域内容.
  • cntentsCenter是用来定义一个固定的边框和一个图层上可拉伸的区域.
  • delegate是用来定义CALayerDelegate对象, 当UIView建立CALayer的时候, 默认就会实现, 而且提供一个**- (void)displayLayer:(CALayer *)layer;**方法的实现.

工程地址

项目地址: https://github.com/CainRun/CoreAnimation


最后

码字很费脑, 看官赏点饭钱可好

微信

支付宝
相关文章
相关标签/搜索