本系列文章算是一系列读书笔记,想了解更多,请看原文html
一个视图就是在屏幕上显示的一个矩形块(好比图片,文字或者视频),它可以拦截相似于鼠标点击或者触摸手势等用户输入。视图在层级关系中能够互相嵌套,一个视图能够管理它的全部子视图的位置。
在iOS中,全部的视图都是从UIView
这个基类派生出来的。UIView
能够处理触摸时间,支持Core Graphics
绘图,能够仿射变换等等操做。ios
CALayer
平时你们也很常见,好比简单的设置个圆角,或者边线等操做都会用到。CALayer
类在概念上和UIView
相似,也是一些被层级关系树管理的矩形块,也能够包含一些内容,而且管理子视图的位置。git
和UIView
最大的区别是CALayer
不能处理用户的操做交互缓存
CALayer
不清楚具体的响应链,可是它提供了一些方法来判断是否某个触点在某个图层范围内。性能
每一个UIView
都对应着一个CALayer
,视图的职责是建立并管理这个图层,以确保党子视图在层级关系中添加或者被移除的时候,他们对应的图层也一样的在对应的层级关系树中有相同的操做。动画
真正用来在屏幕上显示的是图层(CALayer
),UIView
是对它的一个封装,提供一些交互触摸功能,和一些Core Animation
底层的接口。atom
iO S提供UIView
和CALayer
两个平行的层级关系,应该也是为了解耦,作职责分离。 以便能适应 iOS 和 Mac OS 的系统。spa
对于简单的需求咱们无需深刻了解
CALayer
使用UIView
就很方便灵活了。可是有时候咱们只使用UIView
仍是会有些捉襟见肘的,CALayer
暴露了一些UIView
没有提供的功能:代理
- 阴影、圆角、边框
- 3D变换
- 非矩形范围
- 透明遮罩
- 非线性动画
CALayer
有一个属性叫作contents
,这个属性是id
类型的,能够是任何类型的对象。也便是意味着在写代码的时候,能够给contents
赋任何值(显示不显示是另外一回事)。只有赋CGImage
的时候才能正确显示。指针
contents
这个奇怪的表现是由 Mac OS 的历史缘由形成的,由于在 Mac OS 系统上,这个属性对CGImage
和NSImage
类型的值都起做用。可是在 iOS上,若是将UIImage
的值赋给它,只能获得一个空白的图层。
事实上,真正赋值的类型应该是CGImageRef
,这是一个指向CGImage
结构的指针。UIImage
有一个CGImage
属性,它返回一个CGImageRef
,可是这个值不能直接赋值给CALayer
的contents
,由于CGImageRef
不是一个真正的Cocoa
对象,而是Core Foundation
类型。
Core Foundation
和Cocoa
对象是不兼容的,能够经过bridged
转换:layer.contents = (__bridge id)image.CGImage;
既然CALayer
的contents
能够赋值各类类型,咱们能够尝试一下用CALayer
实现UIImageView
的效果。代码以下:
@implementation ViewController - (void)viewDidLoad { [super viewDidLoad]; self.view.backgroundColor = [UIColor lightGrayColor]; UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 50, 100)]; layerView.backgroundColor = [UIColor whiteColor]; [self.view addSubview:layerView]; UIImage *image = [UIImage imageNamed:@"test"]; layerView.layer.contents = (__bridge id)image.CGImage; }
运行一下,效果以下:
虽然能够实现相似UIImageView
的显示效果,但日常并不推荐使用这种方法。
上面示例的图片有点扁,由于咱们设置的frame
是个长方形,而图片自己是一个正方形。因此被挤压了。平时使用UIImageView
时遇到相似状况,能够设置contentMode
来解决。一样:
layerView.contentMode = UIViewContentModeScaleAspectFill;
这样就能够解决了。
UIView
大多数视觉相关的属性好比contentMode
,对这些属性的操做实际上是对对应图层的操做。CALayer
与contentMode
对应的属性叫作contentsGravity
,这是一个NSString
类型,而UIKit
部分是枚举。contentsGravity
可选的常量值有以下:
和contentMode
同样, contentsGravity
目的是决定内容在图层中怎么对齐,将上面设置contentMode
的代码能够替换以下:
layerView.layer.contentsGravity = kCAGravityResizeAspectFill;
运行后的效果是一致的。
contentsScale
属性定义了寄宿图的像素尺寸和视图大小的比例,默认状况下是一个1.0
的浮点数。contentsScale
并非总会对寄宿图的效果有影响,由于contents
设置了contentsGravity
属性,致使常常设置了contentsScale
却没反应。
若是单纯的想放大图层的contents
图片,可使用图层的transform
和affineTransform
。
contentsScale
其实属于支持高分辨率屏幕机制的一部分,是用来判断在绘制图层的时候应该为寄宿图建立的空间大小,和须要显示的图片拉伸度(假设没有设置contentsGravity
)。UIView
有一个相似可是不多用的contentScaleFactor
属性。
若是contentsScale
设置为1.0,将会以每一个点1个像素绘制图片,若是2.0,则以每一个点2个像素绘制图片(这就是Retina屏)。
修改contentsScale
并不会对咱们使用kCAGravityResizeAspectFill
有影响,由于kCAGravityResizeAspectFill
就是拉伸图片适应图层而已。可是若是把contentsGravity
设置成kCAGravityCenter
(这个值不会拉伸图片),变化见下图:
如图所示,图片会变的有点大,并且有像素的颗粒感。由于CGImage
和UIImage
不同,它没有拉伸的感念。用UIImage
读取图片时,读取了高质量的Retina图片。但用CGImage
设置的时候,拉伸的概念就被丢失了,不过能够手动设置contentsScale
来作到一样效果:
layerView.layer.contentsScale = [UIScreen mainScreen].scale;
如今效果以下:
为了突出layerView
的存在感,我把layerView
的frame
调整到CGRectMake(100, 200, 100, 150)
。
看上面最新的运行图,发现图片超出了视图的边界。由于默认状况下,UIView
仍会绘制超过边界的内容,在CALayer
也不例外。UIView
有个clipsToBounds
属性来决定是否显示超出边界的内容。CALayer
对应的属性叫作maskToBounds
,把它设置成YES
就能够不显示超出部分的图片了。
CALayer
的contentsRect
属性容许咱们在图层边框里显示寄宿图的一个子域。和bounds
、frame
不一样,contentsRect
不是按点来计算的。它使用单位坐标。单位坐标指定在0到1以前,是一个相对值(像素和点就是绝对值)。
默认的contentsRect
是{0, 0, 1, 1}
,意味着整个寄宿图默认都是课件的。若是指定小一点的矩形,图片就会被裁剪:
上图设置的contentsRect
是{0, 0, 0.5, 0.5}
事实上contentsRect
设置一个负数的原点或者大于{1, 1}
的尺寸也是能够的。这种状况下,最外面的像素会被拉伸。
contentsRect
在 App 中最有趣的地方能够用做 image sprites(图片拼合)。图片拼合后能够打包到一张大图上一次载入,相比屡次载入不一样的图片,这样作的性能更优。
@interface ViewController () @property (weak, nonatomic) IBOutlet UIView *view1; @property (weak, nonatomic) IBOutlet UIView *view2; @property (weak, nonatomic) IBOutlet UIView *view3; @property (weak, nonatomic) IBOutlet UIView *view4; @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]; UIImage *image = [UIImage imageNamed:@"test_1"]; [self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.view1.layer]; [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.view2.layer]; [self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.view3.layer]; [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.view4.layer]; }
运行的效果以下:
原本原文是用四张不一样的图作拼接,我只是展现下这种功能实现,因此偷懒只用了一张图片。若是有不解之处请看原文
contentsCenter
看名字大部分人会误觉得是和位置有关,其实它是一个CGRect
。它定义了一个苦丁的边框和在图层上可拉伸的区域。
默认状况下,contentsCenter
是{0, 0, 1, 1}
,意味着若是大小改变(contentsGravity
),寄宿图会被均匀的拉伸。
假设咱们增长原点的值,并减少尺寸的值,例如将它变为{0.25, 0.25, 0.5, 0.5}
将会在寄宿图周围留出一个边框。以下图:
上图是借用原书的图。
这效果看起来和UIImage
里的resizableImageWithCapInsets:
很是相似,它能够运用到任何寄宿图,包括在Core Graphics
运行时绘制的图形。
![]()
同一图片使用不一样的contentsCenter
。
contentsCenter
使用起来也很方便,能够用代码:
layer.contentsCenter = CGRectMake(0.25, 0.25, 0.5, 0.5);
也能够在XIB里面设置:
除了给contents
赋值CGImage
来设置寄宿图以外,还能够直接用Core Graphics
来绘制寄宿图。
-drawRect:
经过继承UIView
来实现此方法进行自定义绘制。这个方法默认是没有被实现的。由于对于UIView
来讲,寄宿图不是必须的。若是UIView
检测到-drawRect:
被调用,会自动给视图分配一个寄宿图。这个寄宿图的像素尺寸等于视图大小乘以contentsScale
。
若是你不须要寄宿图,不要写这个方法,会形成资源浪费,详细部分见 《内存恶鬼drawRect》
视图在屏幕上出现的时候-drawRect:
会自动被调用。-drawRect:
方法里面的代码利用Core Graphics
绘制一个寄宿图,而后被缓存起来直到须要被更显(通常是调用了- setNeedDisplay
方法)。
CALayer
有一个可选的delegate
属性<CALayerDelegate>
,当CALayer
须要内容的时候,会从这个delegate
里面查询。
当须要被重绘时,CALayer
会从下面这个代理方法请求一个寄宿图来展现:
- (void)displayLayer:(CALayer *)layer;
若是这个方法没有被实现,CALayer
会尝试下面这个:
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
在drawLayer:
被调用以前,CALayer
建立了一个合适尺寸的寄宿图(尺寸由bounds
和contentsScale
决定)和一个Core Graphics
的绘制上下文环境,并做为ctx
传入。
下面咱们使用CALayerDelegate
是作个示例。
- (void)viewDidLoad { [super viewDidLoad]; UIView *layerView = [[UIView alloc] initWithFrame:CGRectMake(100, 200, 150, 150)]; layerView.backgroundColor = [UIColor lightGrayColor]; [self.view addSubview:layerView]; CALayer *blueLayer = [CALayer layer]; blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); blueLayer.backgroundColor = [UIColor blueColor].CGColor; blueLayer.delegate = self; blueLayer.contentsScale = [UIScreen mainScreen].scale; [layerView.layer addSublayer:blueLayer]; // [blueLayer display]; } - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx { //draw a thick red circle CGContextSetLineWidth(ctx, 10.0f); CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); CGContextStrokeEllipseInRect(ctx, layer.bounds); }
- 在
blueLayer
上显式调用了-display
。由于当图层显示在屏幕上时,CALayer
不会自动重绘,这和UIView
不一样。须要手动调用。- 咱们没有调用
masksToBounds
。可是绘制的圆仍然被裁剪了。这是由于咱们在CALayerDelegate
方法中,没有对超出边界歪的内容提供绘制支持。
除非建立一个单独的图层,咱们平时基本不会用到CALayerDelegate
。由于UIView
在建立时,会自动的吧图层的代理设置为本身,而后提供了一个-displayLayer:
方法实现。