CALayer的寄宿图

一个视图就是在屏幕上显示的一个矩阵块(比如图片、文字或者视频),它能够拦截点击以及触摸手势等用户输入。视图在层级关系中可以相互嵌套,一个视图可以管理他的所有所有子视图的位置。

在iOS当中,所有的视图都是从一个叫做UIView的基类派生而来,UIView可以处理触摸事件,支持基于CoreGraphics的绘图,可以做仿射变换(例如旋转或缩放),或者简单的滑动以及渐变动画。

CALayer类在概念上和UIView类似,同样也是一些被层级关系树管理的矩阵块,同样也可以包含一些内容(像图片、文本、背景色),管理子图层的位置。和UIView最大的不同是,CALayer不处理用户的交互

实际上,CALayer才是真正用来在屏幕上显示和做动画的,UIView仅仅是对它的一个封装,提供了处理触摸事件的功能,以及CoreAnimation底层方法的高级接口

但是为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢?为什么不用一个简单的层级来处理所有的事情呢?原因在于要做职责分离,这样能避免很多重复代码。

在iOS和MacOS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和UIView,而MacOS有APPKit和NSView的原因。UIView和NSView都有一个用于展示的CALayer属性对象,二者的区别就是处理用户触摸事件的机制的不同。所以,将处理界面展示的CALayer独立出来并应用到独立的Core Animation框架,这样苹果就能够在iOS和MacOS之间共享代码,使得开发更加便携

如果说CALayer是UIView的实现细节,那我们为什么要全面地了解他呢?苹果当然会为我们提供简洁优雅的UIView接口,那么我们是否就没必要去处理CoreAnimation的细节了呢?

某种意义上说的确是这样,对于一些简单的需求而言,我们确实没必要处理CALayer,因为苹果已经通过UIView的高级API间接地使得动画变得很简单。

但是这种简单会不可避免地带来一些灵活性上的缺陷,如果你略微想在底层上做一些改变,或者使用一些苹果没有在UIView上实现的接口功能,这时除了介入CoreAnimation底层之外别无选择。

寄宿图

事实上,CALayer类能够包含一张你喜欢的图片,layer中所包含的这张图片称为CALayer的寄宿图

contents属性

CALayer有一个属性叫做contents,这个属性的类型被定义为id,这意味着它可以是任何类型的对象。如下所示:

@property(nullable, strong) id contents;

也就是说,名义上你可以给contents赋任何值,你的APP都能够编译通过。但是在实践中,如果你给contents赋的不是CGImage,那么你得到的图层将是空白的。

contents的这个奇异表现是由MacOS的历史原因造成的。contents之所以被定义为 id 类型,是因为在MacOS中,该属性对CGImage和NSImage类型都起作用。但是如果你在iOS中试图将UIImage类型的对象赋值给它,那么你将得到一片空白

事实上,你真正要赋值的类型是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个“CGImageRef”,如下:

@property(nullable, nonatomic,readonly) CGImageRef CGImage; 
- (nullable CGImageRef)CGImage;

如果你想把这个值直接赋值给contents,那么你将得到一个编译错误,因为CGImageRef并不是一个真正的cocoa对象,而是CoreFoundation类型。

CoreFoundation类型与Cocoa对象很像,但是他们并不是类型兼容的,不过可以通过__bridge关键字进行转换。如果要给layer的contents属性赋值,可以使用如下方法:

layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"216.jpg"].CGImage);

下面我一段完整的代码:

    UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 300, 300)];
    view.center = self.view.center;
    [self.view addSubview:view];
    view.layer.contents = (__bridge id _Nullable)([UIImage imageNamed:@"216.jpg"].CGImage);

效果图如下:

上面我们通过CALayer在一个普通的UIView中展示了一张图片。通常而言,我们展示一张图片需要使用UIImageView,但是我们却可以利用CALayer在UIView上展示一张图片,是不是很有趣?

当图片的尺寸与它所在的控件的尺寸不一致的时候,我们可以通过UIView的 contentMode 属性来控制图片的伸缩以及位置等,像下面这样:

view.contentMode = UIViewContentModeScaleAspectFit;

UIView的 contentMode 属性值有如下选择:

typedef NS_ENUM(NSInteger, UIViewContentMode) {
    UIViewContentModeScaleToFill,
    UIViewContentModeScaleAspectFit,      // contents scaled to fit with fixed aspect. remainder is transparent
    UIViewContentModeScaleAspectFill,     // contents scaled to fill with fixed aspect. some portion of content may be clipped.
    UIViewContentModeRedraw,              // redraw on bounds change (calls -setNeedsDisplay)
    UIViewContentModeCenter,              // contents remain same size. positioned adjusted.
    UIViewContentModeTop,
    UIViewContentModeBottom,
    UIViewContentModeLeft,
    UIViewContentModeRight,
    UIViewContentModeTopLeft,
    UIViewContentModeTopRight,
    UIViewContentModeBottomLeft,
    UIViewContentModeBottomRight,
};

实际上,UIView的绝大多数视觉相关的属性,比如contentMode,对这些属性的操作其实是对对应图层的操作

CALayer中与UIView的 contentMode 属性相对应的属性是contentsGravity,它的值是一个NSString类型,有如下选项:

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

使用代码如下:

view.layer.contentsGravity = kCAGravityCenter;

contentsScale

contentsScale属性定义了寄宿图的像素尺寸和视图展示大小的比例,默认情况下它是一个值为1.0的浮点数,即默认是展示寄宿图的原始大小。

如果你将layer的contentsGravity属性值设为kCAGravityResizeAspect,那么寄宿图就会被拉伸以适应图层的边界。此时,contentsScale属性就不起任何作用了。

如果你只是单纯地想放大图层的contents图片,那么你可以通过layer的transform和affineTransform属性来达到此目的,放大和缩小图片并不是contentsScale的目的所在

contentsScale属性其实属于支持高分辨率(Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间的大小,和需要显示的图片的拉伸度(假设没有设置contentsGravity属性)

如果contentsScale的值设置为1.0,就会以每个点1个像素绘制图片;如果设置为2.0,就会以每个点两个像素绘制图片,这就是我们所熟知的Retina屏幕

当我们将layer的contentsGravity属性值设置为kCAGravityResizeAspect时,contentsScale并不会对寄宿图的大小产生任何影响,因为本身就是拉伸图片以适应图层;但是当我们将layer的contentsGravity属性值设置为kCAGravityCenter(这个值并不会拉伸图片)时,contentsScale的值就会对寄宿图的大小产生明显影响。

当用CGImage来设置图层的内容的时候,默认显示图片的原本像素大小(除非有一些特殊设置,比如将contentsGravity设置为kCAGravityResizeAspect),此时修改contentsScale的值,就可以改变绘制图片时每个点的像素数,进而改变展示在屏幕上的图片大小

我们知道,通常情况下会将图片导入Assets,每个图片都会有一个1倍图、一个2倍图和一个3倍图,当我们获取图片的时候,系统会根据Retina屏幕的分辨率自动选择是获取1倍、2倍还是3倍图。假设目前是在plus的设备上,通过设置layer的contents来展示一张图片,那么获取到的图片是一个3倍图,如果不设置contentsScale的值,那么就会展示图的原始像素大小,所以此时要将layer的contentsScale设置为3.0。那么Retina设备的scale有1、2和3,我们怎么获取到呢,可以通过如下方法获取和设置:

layer.contentsScale = [UIScreen mainScreen].scale;

maskToBounds

当图片大小超过了视图的边界时,默认情况下,UIView会绘制超过边界的内容或者子视图,在CALayer下也是这样的。

UIView中有一个clipsToBounds属性,可用来决定是否展示超出边界的内容。CALayer中也有一个对应的属性,叫做masksToBounds,它的作用跟UIView的clipsToBounds属性是一样的。

contentsRect

CALayer的contentsRect属性允许我们在图层边框里显示寄宿图的一个子域。

和bounds、frame不同,contentsRect不是按点来计算的,而是使用的单位坐标,单位坐标指定在0到1之间,是一个相对值(像素和点都是绝对值),所以contentsRect是相对于寄宿图的尺寸而言的。在iOS中,使用了如下坐标系统:

  • 点——在iOS和MacOS中最常见的坐标体系。点就像是一个虚拟的像素,也被称为逻辑像素。在标准设备上,一个点就是一个像素;但是在Retina屏幕上,按照不同的屏幕尺寸,一个点可以表示一到多个像素。iOS用点作为屏幕的坐标测算体系,就是为了在Retina屏幕和普通设备上能有一直的视觉效果
  • 像素——物理像素坐标并不会用于屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以它是用点来度量大小。但是CGImage是使用像素来表示大小,所以如果不给layer设置contentsScale,那么它上面的图片就会展示原始的像素大小,在Retina屏幕上会根据分辨率的不同而展示出不同的大小
  • 单位——单位坐标实际就是一个比例坐标。

contentsRect的默认值是{0,0,1,1},它表示的是,从寄宿图像素尺寸的原点(0,0)开始,分别截取宽、高的1倍长度,其实就是展示整个寄宿图。

contentsRect最有趣的一个用法是image sprites(图片拼合)。试想一下,如果我们需要将四张图片拼合在一块展示,我们会怎么做?创建4个UIImageView,分别设置不同的图片,然后将这四个imageView添加到一个View上?这样做一来占用内存,二来耗用渲染性能,三来增加载入时间。

那么有没有一个更好的解决方案呢?答案是有的,就是使用layer的contentsRect。具体做法如下:

  1. 首先,让UI准备一个拼合后的图片——一个包含小一些的拼合图的大图片,如下图所示:
  1. 创建4个UIView,通过这4个view的frame来设置拼合图的位置
  2. 像平常一样载入大图,然后把它赋值给四个独立图层的contents,然后设置每个图层的contentsRect来去掉我们不想显示的部分

具体代码如下:

@interface ViewController ()
@property (nonatomic, weak) IBOutlet UIView *coneView;
@property (nonatomic, weak) IBOutlet UIView *shipView;
@property (nonatomic, weak) IBOutlet UIView *iglooView;
@property (nonatomic, weak) IBOutlet UIView *anchorView;
@end

@implementation ViewController

- (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer //set image
{
  layer.contents = (__bridge id)image.CGImage;

  //scale contents to fit
  layer.contentsGravity = kCAGravityResizeAspect;

  //set contentsRect
  layer.contentsRect = rect;
}

- (void)viewDidLoad 
{
  [super viewDidLoad]; //load sprite sheet
  UIImage *image = [UIImage imageNamed:@"Sprites.png"];
  //set igloo sprite
  [self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.iglooView.layer];
  //set cone sprite
  [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.coneView.layer];
  //set anchor sprite
  [self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.anchorView.layer];
  //set spaceship sprite
  [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.shipView.layer];
}
@end

拼合能够提高图片的载入性能,毕竟载入一张大图比载入多张小图要快。

原文发布于微信公众号 - iOS小生活(iOSHappyLife)

原文发表时间:2019-06-22

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券