本系列文章算是一系列读书笔记,想了解更多,请看原文
1.图层树
1.1 视图
一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。
在iOS中,所有的视图都是从UIView
这个基类派生出来的。UIView
可以处理触摸时间,支持Core Graphics
绘图,可以仿射变换等等操作。
1.2 CALayer
CALayer
平时大家也很常见,比如简单的设置个圆角,或者边线等操作都会用到。CALayer
类在概念上和UIView
类似,也是一些被层级关系树管理的矩形块,也可以包含一些内容,并且管理子视图的位置。
和UIView
最大的区别是CALayer
不能处理用户的操作交互
CALayer
不清楚具体的响应链,但是它提供了一些方法来判断是否某个触点在某个图层范围内。
1.3 平行的层级关系
每个UIView
都对应着一个CALayer
,视图的职责是创建并管理这个图层,以确保党子视图在层级关系中添加或者被移除的时候,他们对应的图层也同样的在对应的层级关系树中有相同的操作。
真正用来在屏幕上显示的是图层(CALayer
),UIView
是对它的一个封装,提供一些交互触摸功能,和一些Core Animation
底层的接口。
iO S提供UIView
和CALayer
两个平行的层级关系,应该也是为了解耦,做职责分离。 以便能适应 iOS 和 Mac OS 的系统。
对于简单的需求我们无需深入了解
CALayer
使用UIView
就很方便灵活了。但是有时候我们只使用UIView
还是会有些捉襟见肘的,CALayer
暴露了一些UIView
没有提供的功能:
- 阴影、圆角、边框
- 3D变换
- 非矩形范围
- 透明遮罩
- 非线性动画
2.寄宿图
2.1 contents属性
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;
2.1.1 示例
既然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
的显示效果,但平常并不推荐使用这种方法。
2.1.2 contentGravity
上面示例的图片有点扁,因为我们设置的frame
是个长方形,而图片本身是一个正方形。所以被挤压了。平时使用UIImageView
时遇到类似情况,可以设置contentMode
来解决。同样:
layerView.contentMode = UIViewContentModeScaleAspectFill;
这样就可以解决了。
UIView
大多数视觉相关的属性比如contentMode
,对这些属性的操作其实是对对应图层的操作。CALayer
与contentMode
对应的属性叫做contentsGravity
,这是一个NSString
类型,而UIKit
部分是枚举。contentsGravity
可选的常量值有如下:
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
和contentMode
一样, contentsGravity
目的是决定内容在图层中怎么对齐,将上面设置contentMode
的代码可以替换如下:
layerView.layer.contentsGravity = kCAGravityResizeAspectFill;
运行后的效果是一致的。
2.1.3 contentsScale
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)
。
2.1.4 maskToBounds
看上面最新的运行图,发现图片超出了视图的边界。因为默认情况下,UIView
仍会绘制超过边界的内容,在CALayer
也不例外。UIView
有个clipsToBounds
属性来决定是否显示超出边界的内容。CALayer
对应的属性叫做maskToBounds
,把它设置成YES
就可以不显示超出部分的图片了。
2.1.5 contentsRect
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(图片拼合)。图片拼合后可以打包到一张大图上一次载入,相比多次载入不同的图片,这样做的性能更优。
2.1.6 图片拼接代码示例:
@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];
}
运行的效果如下:
本来原文是用四张不同的图做拼接,我只是展示下这种功能实现,所以偷懒只用了一张图片。如果有不解之处请看原文
2.1.7 contentsCenter
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里面设置:
2.2 Custom Drawing
除了给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
传入。
2.2.1示例:
下面我们使用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:
方法实现。