首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >CALayer的寄宿图

CALayer的寄宿图

作者头像
拉维
发布2019-08-12 15:55:00
9910
发布2019-08-12 15:55:00
举报
文章被收录于专栏:iOS小生活iOS小生活

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

在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

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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-06-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 iOS小生活 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档