本系列文章算是一系列读书笔记,想了解更多,请看原文
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
属性
,当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:
方法实现。