一个视图就是在屏幕上显示的一个矩阵块(比如图片、文字或者视频),它能够拦截点击以及触摸手势等用户输入。视图在层级关系中可以相互嵌套,一个视图可以管理他的所有所有子视图的位置。
在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中,使用了如下坐标系统:
contentsRect的默认值是{0,0,1,1},它表示的是,从寄宿图像素尺寸的原点(0,0)开始,分别截取宽、高的1倍长度,其实就是展示整个寄宿图。
contentsRect最有趣的一个用法是image sprites(图片拼合)。试想一下,如果我们需要将四张图片拼合在一块展示,我们会怎么做?创建4个UIImageView,分别设置不同的图片,然后将这四个imageView添加到一个View上?这样做一来占用内存,二来耗用渲染性能,三来增加载入时间。
那么有没有一个更好的解决方案呢?答案是有的,就是使用layer的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
拼合能够提高图片的载入性能,毕竟载入一张大图比载入多张小图要快。