欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-03-16 17:08:46
...

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


作者感言

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


简介

前面我们已经简单介绍了什么是CALayer, CALayer主要是用来做什么的, 现在我们来更加深入的了解CALayer到底有些什么东西供给我们去使用的.


CALayer的Contents属性

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

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

从字面上意思来看, 这是一个提供图层内容的对象, 并且是id类型, 也就意味着, 我们可以给这个contents属性赋任意的值, 毕竟是id类型嘛, 但在实际操作中, 是行不通的, 为什么?

这里就要牵扯到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赋值吗? 肯定不是啦, 我们可以通过一些关键字进行赋值, 就能够得到对应的效果了, 代码如下:

- (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? 估计连你自己都会这么问你自己了.

效果图:

看完效果图之后, 我们再看看它的层级结构, 你就会发现和我们使用的UIImageView完全一模一样, 不信? 我给你们加个UIImageView看看~

虽然我们并不是通过使用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;
复制代码

效果图:

酱紫图片显示正确啦~~貌似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;
}
复制代码

效果图:


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;
}
复制代码

效果图:


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);
}
复制代码

效果图:

补充一些额外的知识点:

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];
}
复制代码

效果图:

补充一个知识点, 如果你是使用Storyboard或者是xib的话, 你可以在右侧的栏目看到contentsCenter


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);
}
复制代码

效果图:

看到效果图代码之后, 有两点我们是需要注意一下的:

  • 不同的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


最后

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