[iOS Animation]-CALayer 显示方式-contentsScale

contentsScale

contentsScale属性定义了寄宿图的像素尺寸和视图大小的比例,默认状况下它是一个值为1.0的浮点数。 git

contentsScale的目的并非那么明显。它并非总会对屏幕上的寄宿图有影响。若是你尝试对咱们的例子设置不一样的值,你就会发现根本没任何影响。由于contents因为设置了contentsGravity属性,因此它已经被拉伸以适应图层的边界。 github

若是你只是单纯地想放大图层的contents图片,你能够经过使用图层的transform和affineTransform属性来达到这个目的(见第五章『Transforms』,里面对此有解释),这(指放大)也不是contengsScale的目的所在. 编程

contentsScale属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图建立的空间大小,和须要显示的图片的拉伸度(假设并无设置contentsGravity属性)。UIView有一个相似功能可是很是少用到的contentScaleFactor属性。 缓存

若是contentsScale设置为1.0,将会以每一个点1个像素绘制图片,若是设置为2.0,则会以每一个点2个像素绘制图片,这就是咱们熟知的Retina屏幕。(若是你对像素和点的概念不是很清楚的话,这个章节的后面部分将会对此作出解释)。 app

这并不会对咱们在使用kCAGravityResizeAspect时产生任何影响,由于它就是拉伸图片以适应图层而已,根本不会考虑到分辨率问题。可是若是咱们把contentsGravity设置为kCAGravityCenter(这个值并不会拉伸图片),那将会有很明显的变化(如图2.3) 工具

图2.3

图2.3 用错误的contentsScale属性显示Retina图片 布局

如你所见,咱们的雪人不只有点大还有点像素的颗粒感。那是由于和UIImage不一样,CGImage没有拉伸的概念。当咱们使用UIImage类去读取咱们的雪人图片的时候,他读取了高质量的Retina版本的图片。可是当咱们用CGImage来设置咱们的图层的内容时,拉伸这个因素在转换的时候就丢失了。不过咱们能够经过手动设置contentsScale来修复这个问题(如2.2清单),图2.4是结果 性能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@implementation ViewController
 
- (void)viewDidLoad
{
  [super viewDidLoad];
  //load an image
  UIImage *image = [UIImage imageNamed:@"Snowman.png"];
   
  //add it directly to our view's layer
  self.layerView.layer.contents = (__bridge id)image.CGImage;
   
  //center the image
  self.layerView.layer.contentsGravity = kCAGravityCenter;
 
  //set the contentsScale to match image
  self.layerView.layer.contentsScale = image.scale;
}
 

图2.4

图2.4 一样的Retina图片设置了正确的contentsScale以后 ui

当用代码的方式来处理寄宿图的时候,必定要记住要手动的设置图层的contentsScale属性,不然,你的图片在Retina设备上就显示得不正确啦。代码以下: atom

1
layer.contentsScale = [UIScreen mainScreen].scale;

maskToBounds

如今咱们的雪人总算是显示了正确的大小,不过你也许已经发现了另一些事情:他超出了视图的边界。默认状况下,UIView仍然会绘制超过边界的内容或是子视图,在CALayer下也是这样的。

UIView有一个叫作clipsToBounds的属性能够用来决定是否显示超出边界的内容,CALayer对应的属性叫作masksToBounds,把它设置为YES,雪人就在边界里啦~(如图2.5)

图2.5

图2.5 使用masksToBounds来修建图层内容

contentsRect

CALayer的contentsRect属性容许咱们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,因此要比contentsGravity灵活多了

和bounds,frame不一样,contentsRect不是按点来计算的,它使用了单位坐标,单位坐标指定在0到1之间,是一个相对值(像素和点就是绝对值)。因此他们是相对与寄宿图的尺寸的。iOS使用了如下的坐标系统:

  • 点 —— 在iOS和Mac OS中最多见的坐标体系。点就像是虚拟的像素,也被称做逻辑像素。在标准设备上,一个点就是一个像素,可是在Retina设备上,一个点等于2*2个像素。iOS用点做为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。
  • 像素 —— 物理像素坐标并不会用来屏幕布局,可是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,因此指定点来度量大小。可是一些底层的图片表示如CGImage就会使用像素,因此你要清楚在Retina设备和普通设备上,他们表现出来了不一样的大小。
  • 单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不须要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得不少,Core Animation中也用到了单位坐标。

默认的contentsRect是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,若是咱们指定一个小一点的矩形,图片就会被裁剪(如图2.6)

图2.6

图2.6 一个自定义的contentsRect(左)和以前显示的内容(右)

事实上给contentsRect设置一个负数的原点或是大于{1, 1}的尺寸也是能够的。这种状况下,最外面的像素会被拉伸以填充剩下的区域。

contentsRect在app中最有趣的地方在于一个叫作image sprites(图片拼合)的用法。若是你有游戏编程的经验,那么你必定对图片拼合的概念很熟悉,图片可以在屏幕上独立地变动位置。抛开游戏编程不谈,这个技术经常使用来指代载入拼合的图片,跟移动图片一点关系也没有。

典型地,图片拼合后能够打包整合到一张大图上一次性载入。相比屡次载入不一样的图片,这样作可以带来不少方面的好处:内存使用,载入时间,渲染性能等等

2D游戏引擎入Cocos2D使用了拼合技术,它使用OpenGL来显示图片。不过咱们可使用拼合在一个普通的UIKit应用中,对!就是使用contentsRect

首先,咱们须要一个拼合后的图表 —— 一个包含小一些的拼合图的大图片。如图2.7所示:

图2.7

接下来,咱们要在app中载入并显示这些拼合图。规则很简单:像日常同样载入咱们的大图,而后把它赋值给四个独立的图层的contents,而后设置每一个图层的contentsRect来去掉咱们不想显示的部分。

咱们的工程中须要一些额外的视图。(为了不太多代码。咱们将使用Interface Builder来拜访他们的位置,若是你愿意仍是能够用代码的方式来实现的)。清单2.3有须要的代码,图2.8展现告终果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@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

图2.8

拼合不只给app提供了一个整洁的载入方式,还有效地提升了载入性能(单张大图比多张小图载入地更快),可是若是有手动安排的话,他们仍是有一些不方便的,若是你须要在一个已经建立好的品和图上作一些尺寸上的修改或者其余变更,无疑是比较麻烦的。

Mac上有一些商业软件能够为你自动拼合图片,这些工具自动生成一个包含拼合后的坐标的XML或者plist文件,拼合图片的使用大大简化。这个文件能够和图片一同载入,并给每一个拼合的图层设置contentsRect,这样开发者就不用手动写代码来摆放位置了。

这些文件一般在OpenGL游戏中使用,不过呢,你要是有兴趣在一些常见的app中使用拼合技术,那么一个叫作LayerSprites的开源库(https://github.com/nicklockwood/LayerSprites),它可以读取Cocos2D格式中的拼合图并在普通的Core Animation层中显示出来。

contentsCenter

本章咱们介绍的最后一个和内容有关的属性是contentsCenter,看名字你可能会觉得它可能跟图片的位置有关,不过这名字着实误导了你。contentsCenter实际上是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。 改变contentsCenter的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看获得效果。

默认状况下,contentsCenter是{0, 0, 1, 1},这意味着若是大小(由conttensGravity决定)改变了,那么寄宿图将会均匀地拉伸开。可是若是咱们增长原点的值并减少尺寸。咱们会在图片的周围创造一个边框。图2.9展现了contentsCenter设置为{0.25, 0.25, 0.5, 0.5}的效果。

图2.9

图2.9 contentsCenter的例子

这意味着咱们能够随意重设尺寸,边框仍然会是连续的。他工做起来的效果和UIImage里的-resizableImageWithCapInsets: 方法效果很是相似,只是它能够运用到任何寄宿图,甚至包括在Core Graphics运行时绘制的图形(本章稍后会讲到)。

图2.10

图2.10 同一图片使用不一样的contentsCenter

清单2.4 演示了如何编写这些可拉伸视图。不过,contentsCenter的另外一个很酷的特性就是,它能够在Interface Builder里面配置,根本不用写代码。如图2.11

清单2.4 用contentsCenter设置可拉伸视图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
@interface ViewController ()
 
@property (nonatomic, weak) IBOutlet UIView *button1;
@property (nonatomic, weak) IBOutlet UIView *button2;
 
@end
 
@implementation ViewController
 
- (void)addStretchableImage:(UIImage *)image withContentCenter:(CGRect)rect toLayer:(CALayer *)layer
  //set image
  layer.contents = (__bridge id)image.CGImage;
 
  //set contentsCenter
  layer.contentsCenter = rect;
}
 
- (void)viewDidLoad
{
  [super viewDidLoad]; //load button image
  UIImage *image = [UIImage imageNamed:@"Button.png"];
 
  //set button 1
  [self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button1.layer];
 
  //set button 2
  [self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button2.layer];
}
 
@end

图2.11

图2.11 用Interface Builder 探测窗口控制contentsCenter属性

Custome Drawing

给contents赋CGImage的值不是惟一的设置寄宿图的方法。咱们也能够直接用Core Graphics直接绘制寄宿图。可以经过继承UIView并实现-drawRect:方法来自定义绘制。

-drawRect: 方法没有默认的实现,由于对UIView来讲,寄宿图并非必须的,它不在乎那究竟是单调的颜色仍是有一个图片的实例。若是UIView检测到-drawRect: 方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 contentsScale的值。

若是你不须要寄宿图,那就不要建立这个方法了,这会形成CPU资源和内存的浪费,这也是为何苹果建议:若是没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。

当视图在屏幕上出现的时候 -drawRect:方法就会被自动调用。-drawRect:方法里面的代码利用Core Graphics去绘制一个寄宿图,而后内容就会被缓存起来直到它须要被更新(一般是由于开发者调用了-setNeedsDisplay方法,尽管影响到表现效果的属性值被更改时,一些视图类型会被自动重绘,如bounds属性)。虽然-drawRect:方法是一个UIView方法,事实上都是底层的CALayer安排了重绘工做和保存了所以产生的图片。

CALayer有一个可选的delegate属性,实现了CALayerDelegate协议,当CALayer须要一个内容特定的信息时,就会从协议中请求。CALayerDelegate是一个非正式协议,其实就是说没有CALayerDelegate @protocol可让你在类里面引用啦。你只须要调用你想调用的方法,CALayer会帮你作剩下的。(delegate属性被声明为id类型,全部的代理方法都是可选的)。

当须要被重绘时,CALayer会请求它的代理给他一个寄宿图来显示。它经过调用下面这个方法作到的:

1
- (void)displayLayer:(CALayerCALayer *)layer;

趁着这个机会,若是代理想直接设置contents属性的话,它就能够这么作,否则没有别的方法能够调用了。若是代理不实现-displayLayer:方法,CALayer就会转而尝试调用下面这个方法:

1
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;

在调用这个方法以前,CALayer建立了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图作准备,他做为ctx参数传入。

让咱们来继续第一章的项目让它实现CALayerDelegate并作一些绘图工做吧(见清单2.5).图2.12是他的结果

清单2.5 实现CALayerDelegate

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@implementation ViewController
- (void)viewDidLoad
{
  [super viewDidLoad];
  
  //create sublayer
  CALayer *blueLayer = [CALayer layer];
  blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
  blueLayer.backgroundColor = [UIColor blueColor].CGColor;
 
  //set controller as layer delegate
  blueLayer.delegate = self;
 
  //ensure that layer backing image uses correct scale
  blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view
  [self.layerView.layer addSublayer:blueLayer];
 
  //force layer to redraw
  [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);
}
@end

图2.12

图2.12 实现CALayerDelegate来绘制图层

注意一下一些有趣的事情:

  • 咱们在blueLayer上显式地调用了-display。不一样于UIView,当图层显示在屏幕上时,CALayer不会自动重绘它的内容。它把重绘的决定权交给了开发者。
  • 尽管咱们没有用masksToBounds属性,绘制的那个圆仍然沿边界被裁剪了。这是由于当你使用CALayerDelegate绘制寄宿图的时候,并无对超出边界外的内容提供绘制支持。

如今你理解了CALayerDelegate,并知道怎么使用它。可是除非你建立了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。由于当UIView建立了它的宿主图层时,它就会自动地把图层的delegate设置为它本身,并提供了一个-displayLayer:的实现,那全部的问题就都没了。

当使用寄宿了视图的图层的时候,你也没必要实现-displayLayer:和-drawLayer:inContext:方法来绘制你的寄宿图。一般作法是实现UIView的-drawRect:方法,UIView就会帮你作完剩下的工做,包括在须要重绘的时候调用-display方法。

总结

本章介绍了寄宿图和一些相关的属性。你学到了如何显示和放置图片, 使用拼合技术来显示, 以及用CALayerDelegate和Core Graphics来绘制图层内容。

相关文章
相关标签/搜索