iOS 动画学习

图层树、寄宿图以及图层几何学
(一)图层的树状结构
git

技术交流新QQ群:414971585github

巨妖有图层,洋葱也有图层,你有吗?咱们都有图层 -- 史莱克算法

Core Animation实际上是一个使人误解的命名。你可能认为它只是用来作动画的,但实际上它是从一个叫作Layer Kit这么一个不怎么和动画有关的名字演变而来,因此作动画这只是Core Animation特性的冰山一角。编程

Core Animation是一个复合引擎,它的职责就是尽量快地组合屏幕上不一样的可视内容,这个内容是被分解成独立的图层,存储在一个叫作图层树的体系之中。因而这个树造成了UIKit以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。缓存

在咱们讨论动画以前,咱们将从图层树开始,涉及一下Core Animation的静态组合以及布局特性。app

图层和视图框架

若是你曾经在iOS或者Mac OS平台上写过应用程序,你可能会对视图的概念比较熟悉。一个视图就是在屏幕上显示的一个矩形块(好比图片,文字或者视频),它可以拦截相似于鼠标点击或者触摸手势等用户输入。视图在层级关系中能够互相嵌套,一个视图能够管理它的全部子视图的位置。图1.1显示了一种典型的视图层级关系ide

05.jpeg

图1.1 一种典型的iOS屏幕(左边)和造成视图的层级关系(右边)函数

在iOS当中,全部的视图都从一个叫作UIVIew的基类派生而来,UIView能够处理触摸事件,能够支持基于Core Graphics绘图,能够作仿射变换(例如旋转或者缩放),或者简单的相似于滑动或者渐变的动画。工具

CALayer

CALayer类在概念上和UIView相似,一样也是一些被层级关系树管理的矩形块,一样也能够包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来作动画和变换。和UIView最大的不一样是CALayer不处理用户的交互。

CALayer并不清楚具体的响应链(iOS经过视图层级关系用来传送触摸事件的机制),因而它并不可以响应事件,即便它提供了一些方法来判断是否一个触点在图层的范围以内(具体见第三章,“图层的几何学”)

平行的层级关系

每个UIview都有一个CALayer实例的图层属性,也就是所谓的backing layer,视图的职责就是建立并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也一样对应在层级关系树当中有相同的操做(见图1.2)。

06.jpeg

图1.2 图层的树状结构(左边)以及对应的视图层级(右边)

实际上这些背后关联的图层才是真正用来在屏幕上显示和作动画,UIView仅仅是对它的一个封装,提供了一些iOS相似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。

可是为何iOS要基于UIView和CALayer提供两个平行的层级关系呢?为何不用一个简单的层级来处理全部事情呢?缘由在于要作职责分离,这样也能避免不少重复代码。在iOS和Mac OS两个平台上,事件和用户交互有不少地方的不一样,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为何iOS有UIKit和UIView,可是Mac OS有AppKit和NSView的缘由。他们功能上很类似,可是在实现上有着显著的区别。

绘图,布局和动画,相比之下就是相似Mac笔记本和桌面系列同样应用于iPhone和iPad触屏的概念。把这种功能的逻辑分开并应用到独立的Core Animation框架,苹果就可以在iOS和Mac OS之间共享代码,使得对苹果本身的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。

实际上,这里并非两个层级关系,而是四个,每个都扮演不一样的角色,除了视图层级和图层树以外,还存在呈现树和渲染树,将在第七章“隐式动画”和第十二章“性能调优”分别讨论。

图层的能力

若是说CALayer是UIView内部实现细节,那咱们为何要全面地了解它呢?苹果固然为咱们提供了优美简洁的UIView接口,那么咱们是否就不必直接去处理Core Animation的细节了呢?

某种意义上说的确是这样,对一些简单的需求来讲,咱们确实不必处理CALayer,由于苹果已经经过UIView的高级API间接地使得动画变得很简单。

可是这种简单会不可避免地带来一些灵活上的缺陷。若是你略微想在底层作一些改变,或者使用一些苹果没有在UIView上实现的接口功能,这时除了介入Core Animation底层以外别无选择。

咱们已经证明了图层不能像视图那样处理触摸事件,那么他能作哪些视图不能作的呢?这里有一些UIView没有暴露出来的CALayer的功能:

  • 阴影,圆角,带颜色的边框

  • 3D变换

  • 非矩形范围

  • 透明遮罩

  • 多级非线性动画

咱们将会在后续章节中探索这些功能,首先咱们要关注一下在应用程序当中CALayer是怎样被利用起来的。

使用图层

首先咱们来建立一个简单的项目,来操纵一些layer的属性。打开Xcode,使用Single View Application模板建立一个工程。

在屏幕中央建立一个小视图(大约200 X 200的尺寸),固然你能够手工编码,或者使用Interface Builder(随你方便)。确保你的视图控制器要添加一个视图的属性以即可以直接访问它。咱们把它称做layerView。

运行项目,应该能在浅灰色屏幕背景中看见一个白色方块(图1.3),若是没看见,可能须要调整一下背景window或者view的颜色

07.jpg

图1.3 灰色背景上的一个白色UIView

这并无什么使人激动的地方,咱们来添加一个色块,在白色方块中间添加一个小的蓝色块。

咱们固然能够简单地在已经存在的UIView上添加一个子视图(随意用代码或者IB),但这不能真正学到任何关于图层的东西。

因而咱们来建立一个CALayer,而且把它做为咱们视图相关图层的子图层。尽管UIView类的接口中暴露了图层属性,可是标准的Xcode项目模板并无包含Core Animation相关头文件。因此若是咱们不给项目添加合适的库,是不可以使用任何图层相关的方法或者访问它的属性。因此首先须要添加QuartzCore框架到Build Phases标签(图1.4),而后在vc的.m文件中引入库。

1.4.jpg

图1.4 把QuartzCore库添加到项目

以后就能够在代码中直接引用CALayer的属性和方法。在清单1.1中,咱们用建立了一个CALayer,设置了它的backgroundColor属性,而后添加到layerView背后相关图层的子图层(这段代码的前提是经过IB建立了layerView并作好了链接),图1.5显示告终果。

清单1.1 给视图添加一个蓝色子图层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#import "ViewController.h"
#import
@ interface  ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
?
@end
@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;
     //add it to our view
     [self.layerView.layer addSublayer:blueLayer];
}
@end

08.jpg

图1.5 白色UIView内部嵌套的蓝色CALayer

一个视图只有一个相关联的图层(自动建立),同时它也能够支持添加无数多个子图层,从清单1.1能够看出,你能够显示建立一个单独的图层,而且把它直接添加到视图关联图层的子图层。尽管能够这样添加图层,但每每咱们只是见简单地处理视图,他们关联的图层并不须要额外地手动添加子图层。

在Mac OS平台,10.8版本以前,一个显著的性能缺陷就是因为用了视图层级而不是单独在一个视图内使用CALayer树状层级。可是在iOS平台,使用轻量级的UIView类并无显著的性能影响(固然在Mac OS 10.8以后,NSView的性能一样也获得很大程度的提升)。

使用图层关联的视图而不是CALayer的好处在于,你能在使用全部CALayer底层特性的同时,也可使用UIView的高级API(好比自动排版,布局和事件处理)。

然而,当知足如下条件的时候,你可能更须要使用CALayer而不是UIView

  • 开发同时能够在Mac OS上运行的跨平台应用

  • 使用多种CALayer的子类(见第六章,“特殊的图层“),而且不想建立额外的UIView去包封装它们全部

  • 作一些对性能特别挑剔的工做,好比对UIView一些可忽略不计的操做都会引发显著的不一样(尽管如此,你可能会直接想使用OpenGL绘图)

可是这些例子都不多见,总的来讲,处理视图会比单独处理图层更加方便。

总结

这一章阐述了图层的树状结构,说明了如何在iOS中由UIView的层级关系造成的一种平行的CALayer层级关系,在后面的实验中,咱们建立了本身的CALayer,并把它添加到图层树中。

在第二章,“图层关联的图片”,咱们将要研究一下CALayer关联的图片,以及Core Animation提供的操做显示的一些特性。
--------------------------------------------------------------------------------------------------------------------------------------------------------

(二)寄宿图

图片赛过千言万语,界面抵得上千图片 ——Ben Shneiderman

咱们在第一章『图层树』中介绍了CALayer类并建立了一个简单的有蓝色背景的图层。背景颜色还好啦,可是若是它仅仅是展示了一个单调的颜色未免也太无聊了。事实上CALayer类可以包含一张你喜欢的图片,这一章节咱们未来探索CALayer的寄宿图(即图层中包含的图)。

contents属性

CALyer 有一个属性叫作contents,这个属性的类型被定义为id,意味着它能够是任何类型的对象。在这种状况下,你能够给contents属性赋任何值,你的app仍然可以编译经过。可是,在实践中,若是你给contents赋的不是CGImage,那么你获得的图层将是空白的。

contents这个奇怪的表现是由Mac OS的历史缘由形成的。它之因此被定义为id类型,是由于在Mac OS系统上,这个属性对CGImage和NSImage类型的值都起做用。若是你试图在iOS平台上将UIImage的值赋给它,只能获得一个空白的图层。一些初识Core Animation的iOS开发者可能会对这个感到困惑。

头疼的不只仅是咱们刚才提到的这个问题。事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个"CGImageRef",若是你想把这个值直接赋值给CALayer的contents,那你将会获得一个编译错误。由于CGImageRef并非一个真正的Cocoa对象,而是一个Core Foundation类型。

尽管Core Foundation类型跟Cocoa对象在运行时貌似很像(被称做toll-free bridging),他们并非类型兼容的,不过你能够经过bridged关键字转换。若是要给图层的寄宿图赋值,你能够按照如下这个方法:

1
layer.contents = (__bridge id)image.CGImage;

若是你没有使用ARC(自动引用计数),你就不须要__bridge这部分。可是,你干吗不用ARC?!

让咱们来继续修改咱们在第一章新建的工程,以便可以展现一张图片而不只仅是一个背景色。咱们已经用代码的方式创建一个图层,那咱们就不须要额外的图层了。那么咱们就直接把layerView的宿主图层的contents属性设置成图片。

清单2.1 更新后的代码。

1
2
3
4
5
6
7
8
9
@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;
}
@end

图表2.1 在UIView的宿主图层中显示一张图片

09.png

咱们用这些简单的代码作了一件颇有趣的事情:咱们利用CALayer在一个普通的UIView中显示了一张图片。这不是一个UIImageView,它不是咱们一般用来展现图片的方法。经过直接操做图层,咱们使用了一些新的函数,使得UIView更加有趣了。

contentGravity

你可能已经注意到了咱们的雪人看起来有点。。。胖 ==! 咱们加载的图片并不恰好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用UIImageView的时候遇到过一样的问题,解决方法就是把contentMode属性设置成更合适的值,像这样:

1
view.contentMode = UIViewContentModeScaleAspectFit;

这个方法基本和咱们遇到的状况的解决方法已经接近了(你能够试一下 :) ),不过UIView大多数视觉相关的属性好比contentMode,对这些属性的操做实际上是对对应图层的操做。

CALayer与contentMode对应的属性叫作contentsGravity,可是它是一个NSString类型,而不是像对应的UIKit部分,那里面的值是枚举。contentsGravity可选的常量值有如下一些:

1
2
3
4
5
6
7
8
9
10
11
12
kCAGravityCenter
kCAGravityTop
kCAGravityBottom
kCAGravityLeft
kCAGravityRight
kCAGravityTopLeft
kCAGravityTopRight
kCAGravityBottomLeft
kCAGravityBottomRight
kCAGravityResize
kCAGravityResizeAspect
kCAGravityResizeAspectFill

和cotentMode同样,contentsGravity的目的是为了决定内容在图层的边界中怎么对齐,咱们将使用kCAGravityResizeAspect,它的效果等同于UIViewContentModeScaleAspectFit, 同时它还能在图层中等比例拉伸以适应图层的边界。

1
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;

图2.2 能够看到结果

10.png

图2.2 正确地设置contentsGravity的值

contentsScale

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

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

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

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

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

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

2.3.png

图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
@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;
}
@end

2.4.png

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

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

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

maskToBounds

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

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

2.5.png

图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.jpg

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

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

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

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

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

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

2.7.jpg

接下来,咱们要在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
@ 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.9.jpg

拼合不只给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.png

图2.9 contentsCenter的例子

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

2.10.png

图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
@ 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.jpg

图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
@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 (1).png

图2.12 实现CALayerDelegate来绘制图层

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

  • 咱们在blueLayer上显式地调用了-display。不一样于UIView,当图层显示在屏幕上时,CALayer不会自动重绘它的内容。它把重绘的决定权交给了开发者。

  • 尽管咱们没有用masksToBounds属性,绘制的那个圆仍然沿边界被裁剪了。这是由于当你使用CALayerDelegate绘制寄宿图的时候,并无对超出边界外的内容提供绘制支持。

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

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

总结

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

在第三章,"图层几何学"中,咱们将会探讨一下图层的几何,观察他们是如何放置和改变相互的尺寸的。
--------------------------------------------------------------------------------------------------------------------------------------------------------

(三)图层几何学

不熟悉几何学的人就不要来这里了 --柏拉图学院入口的签名

在第二章里面,咱们介绍了图层背后的图片,和一些控制图层坐标和旋转的属性。在这一章中,咱们将要看一看图层内部是如何根据父图层和兄弟图层来控制位置和尺寸的。另外咱们也会涉及如何管理图层的几何结构,以及它是如何被自动调整和自动布局影响的。

布局

UIView有三个比较重要的布局属性:frame,bounds和center,CALayer对应地叫作frame,bounds和position。为了能清楚区分,图层用了“position”,视图用了“center”,可是他们都表明一样的值。

frame表明了图层的外部坐标(也就是在父图层上占据的空间),bounds是内部坐标({0, 0}一般是图层的左上角),center和position都表明了相对于父图层anchorPoint所在的位置。anchorPoint的属性将会在后续介绍到,如今把它想成图层的中心点就行了。图3.1显示了这些属性是如何相互依赖的。

3.1.jpeg

图3.1 UIView和CALayer的坐标系

视图的frame,bounds和center属性仅仅是存取方法,当操纵视图的frame,其实是在改变位于视图下方CALayer的frame,不可以独立于图层以外改变视图的frame。

对于视图或者图层来讲,frame并非一个很是清晰的属性,它实际上是一个虚拟属性,是根据bounds,position和transform计算而来,因此当其中任何一个值发生改变,frame都会变化。相反,改变frame的值一样会影响到他们当中的值

记住当对图层作变换的时候,好比旋转或者缩放,frame实际上表明了覆盖在图层旋转以后的整个轴对齐的矩形区域,也就是说frame的宽高可能和bounds的宽高再也不一致了(图3.2)

3.2.jpeg

图3.2 旋转一个视图或者图层以后的frame属性

锚点

以前提到过,视图的center属性和图层的position属性都指定了相对于父图层anchorPoint的位置。图层的anchorPoint经过position来控制它的frame的位置,你能够认为anchorPoint是用来移动图层的把柄。

默认来讲,anchorPoint位于图层的中点,因此图层的将会以这个点为中心放置。anchorPoint属性并无被UIView接口暴露出来,这也是视图的position属性被叫作“center”的缘由。可是图层的anchorPoint能够被移动,好比你能够把它置于图层frame的左上角,因而图层的内容将会向右下角的position方向移动(图3.3),而不是居中了。

3.3.jpeg

图3.3 改变anchorPoint的效果

和第二章提到的contentsRect和contentsCenter属性相似,anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},所以默认坐标是{0.5, 0.5}。anchorPoint能够经过指定x和y值小于0或者大于1,使它放置在图层范围以外。

注意在图3.3中,当改变了anchorPoint,position属性保持固定的值并无发生改变,可是frame却移动了。

那在什么场合须要改变anchorPoint呢?既然咱们能够随意改变图层位置,那改变anchorPoint不会形成困惑么?为了举例说明,咱们来举一个实用的例子,建立一个模拟闹钟的项目。

钟面和钟表由四张图片组成(图3.4),为了简单说明,咱们仍是用传统的方式来装载和加载图片,使用四个UIImageView实例(固然你也能够用正常的视图,设置他们图层的contents图片)。

3.4.jpeg

图3.4 组成钟面和钟表的四张图片

闹钟的组件经过IB来排列(图3.5),这些图片视图嵌套在一个容器视图以内,而且自动调整和自动布局都被禁用了。这是由于自动调整会影响到视图的frame,而根据图3.2的演示,当视图旋转的时候,frame是会发生改变的,这将会致使一些布局上的失灵。

咱们用NSTimer来更新闹钟,使用视图的transform属性来旋转钟表(若是你对这个属性不太熟悉,不要着急,咱们将会在第5章“变换”当中详细说明),具体代码见清单3.1

3.5.jpg

图3.5 在Interface Builder中布局闹钟视图

清单3.1 Clock

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
@ interface  ViewController ()
@property (nonatomic, weak) IBOutlet UIImageView *hourHand;
@property (nonatomic, weak) IBOutlet UIImageView *minuteHand;
@property (nonatomic, weak) IBOutlet UIImageView *secondHand;
@property (nonatomic, weak) NSTimer *timer;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //start timer
     self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES];
                   ?
     //set initial hand positions
     [self tick];
}
- (void)tick
{
     //convert time to hours, minutes and seconds
     NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar];
     NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit;
     NSDateComponents *components = [calendar components:units fromDate:[NSDate date]];
     CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0;
     //calculate hour hand angle //calculate minute hand angle
     CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0;
     //calculate second hand angle
     CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0;
     //rotate hands
     self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle);
     self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle);
     self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle);
}
@end

运行项目,看起来有点奇怪(图3.6),由于钟表的图片在围绕着中心旋转,这并非咱们期待的一个支点。

3.6.jpeg

图3.6 钟面,和不对齐的钟指针

你也许会认为能够在Interface Builder当中调整指针图片的位置来解决,但其实并不能达到目的,由于若是不放在钟面中间的话,一样不能正确的旋转。

也许在图片末尾添加一个透明空间也是个解决方案,但这样会让图片变大,也会消耗更多的内存,这样并不优雅。

更好的方案是使用anchorPoint属性,咱们来在-viewDidLoad方法中添加几行代码来给每一个钟指针的anchorPoint作一些平移(清单3.2),图3.7显示了正确的结果。

清单3.2

1
2
3
4
5
6
7
8
9
- (void)viewDidLoad 
{
     [ super  viewDidLoad];
     // adjust anchor points
     self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); 
     self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); 
     self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f);
     // start timer
}

3.7.jpeg

图3.7 钟面,和正确对齐的钟指针

坐标系

和视图同样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的position依赖于它父图层的bounds,若是父图层发生了移动,它的全部子图层也会跟着移动。

这样对于放置图层会更加方便,由于你能够经过移动根图层来将它的子图层做为一个总体来移动,可是有时候你须要知道一个图层的绝对位置,或者是相对于另外一个图层的位置,而不是它当前父图层的位置。

CALayer给不一样坐标系之间的图层转换提供了一些工具类方法:

1
2
3
4
- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer; 
- (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; 
- (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer;
- (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;

这些方法能够把定义在一个图层坐标系下的点或者矩形转换成另外一个图层坐标系下的点或者矩形

翻转的几何结构

常规说来,在iOS上,一个图层的position位于父图层的左上角,可是在Mac OS上,一般是位于左下角。Core Animation能够经过geometryFlipped属性来适配这两种状况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个BOOL类型。在iOS上经过设置它为YES意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是一般的顶部(它的全部子图层也同理,除非把它们的geometryFlipped属性也设为YES)。

Z坐标轴

和UIView严格的二维坐标系不一样,CALayer存在于一个三维空间当中。除了咱们已经讨论过的position和anchorPoint属性以外,CALayer还有另外两个属性,zPosition和anchorPointZ,两者都是在Z轴上描述图层位置的浮点类型。

注意这里并无更深的属性来描述由宽和高作成的bounds了,图层是一个彻底扁平的对象,你能够把它们想象成相似于一页二维的坚硬的纸片,用胶水粘成一个空洞,就像三维结构的折纸同样。

zPosition属性在大多数状况下其实并不经常使用。在第五章,咱们将会涉及CATransform3D,你会知道如何在三维空间移动和旋转图层,除了作变换以外,zPosition最实用的功能就是改变图层的显示顺序了。

一般,图层是根据它们子图层的sublayers出现的顺序来类绘制的,这就是所谓的画家的算法--就像一个画家在墙上做画--后被绘制上的图层将会遮盖住以前的图层,可是经过增长图层的zPosition,就能够把图层向相机方向前置,因而它就在全部其余图层的前面了(或者至少是小于它的zPosition值的图层的前面)。

这里所谓的“相机”其实是相对于用户是视角,这里和iPhone背后的内置相机没任何关系。

图3.8显示了在Interface Builder内的一对视图,正如你所见,首先出如今视图层级绿色的视图被绘制在红色视图的后面。

3.11.jpg

图3.8 在视图层级中绿色视图被绘制在红色视图的后面

咱们但愿在真实的应用中也能显示出绘图的顺序,一样地,若是咱们提升绿色视图的zPosition(清单3.3),咱们会发现顺序就反了(图3.9)。其实并不须要增长太多,视图都很是地薄,因此给zPosition提升一个像素就可让绿色视图前置,固然0.1或者0.0001也可以作到,可是最好不要这样,由于浮点类型四舍五入的计算可能会形成一些不便的麻烦。

清单3.3

1
2
3
4
5
6
7
8
9
10
11
12
13
@ interface  ViewController ()
@property (nonatomic, weak) IBOutlet UIView *greenView;
@property (nonatomic, weak) IBOutlet UIView *redView;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     ?
     //move the green view zPosition nearer to the camera
     self.greenView.layer.zPosition = 1.0f;
}
@end

3.9.jpeg

图3.9 绿色视图被绘制在红色视图的前面

Hit Testing

第一章“图层树”证明了最好使用图层相关视图,而不是建立独立的图层关系。其中一个缘由就是要处理额外复杂的触摸事件。

CALayer并不关心任何响应链事件,因此不能直接处理触摸事件或者手势。可是它有一系列的方法帮你处理事件:-containsPoint:和-hitTest:。

-containsPoint:接受一个在本图层坐标系下的CGPoint,若是这个点在图层frame范围内就返回YES。如清单3.4所示第一章的项目的另外一个合适的版本,也就是使用-containsPoint:方法来判断究竟是白色仍是蓝色的图层被触摸了 (图3.10)。这须要把触摸坐标转换成每一个图层坐标系下的坐标,结果很不方便。

清单3.4 使用containsPoint判断被点击的图层

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
35
36
37
38
39
40
41
@ interface  ViewController ()
@property (nonatomic, weak) IBOutlet UIView *layerView;
@property (nonatomic, weak) CALayer *blueLayer;
@end
@implementation ViewController
- (void)viewDidLoad
{
     [ super  viewDidLoad];
     //create sublayer
     self.blueLayer = [CALayer layer];
     self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f);
     self.blueLayer.backgroundColor = [UIColor blueColor].CGColor;
     //add it to our view
     [self.layerView.layer addSublayer:self.blueLayer];
}
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
     //get touch position relative to main view
     CGPoint point = [[touches anyObject] locationInView:self.view];
     //convert point to the white layer's coordinates
     point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer];
     //get layer using containsPoint:
     if  ([self.layerView.layer containsPoint:point]) {
         //convert point to blueLayer’s coordinates
         point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer];
         if  ([self.blueLayer containsPoint:point]) {
             [[[UIAlertView alloc] initWithTitle:@ "Inside Blue Layer" 
                                         message:nil
                                        delegate:nil 
                               cancelButtonTitle:@ "OK"
                               otherButtonTitles:nil] show];
         else  {
             [[[UIAlertView alloc] initWithTitle:@ "Inside White Layer"
                                         message:nil 
                                        delegate:nil
                               cancelButtonTitle:@ "OK"
                               otherButtonTitles:nil] show];
         }
     }
}
@end

3.10.jpeg

图3.10 点击图层被正确标识

-hitTest:方法一样接受一个CGPoint类型参数,而不是BOOL类型,它返回图层自己,或者包含这个坐标点的叶子节点图层。这意味着再也不须要像使用-containsPoint:那样,人工地在每一个子图层变换或者测试点击的坐标。若是这个点在最外面图层的范围以外,则返回nil。具体使用-hitTest:方法被点击图层的代码如清单3.5所示。

清单3.5 使用hitTest判断被点击的图层

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
     //get touch position
     CGPoint point = [[touches anyObject] locationInView:self.view];
     //get touched layer
     CALayer *layer = [self.layerView.layer hitTest:point];
     //get layer using hitTest
     if  (layer == self.blueLayer) {
         [[[UIAlertView alloc] initWithTitle:@ "Inside Blue Layer"
                                     message:nil
                                    delegate:nil
                           cancelButtonTitle:@ "OK"
                           otherButtonTitles:nil] show];
     else  if  (layer == self.layerView.layer) {
         [[[UIAlertView alloc] initWithTitle:@ "Inside White Layer"
                                     message:nil
                                    delegate:nil
                           cancelButtonTitle:@ "OK"
                           otherButtonTitles:nil] show];
     }
}

注意当调用图层的-hitTest:方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理事件相似)。以前提到的zPosition属性能够明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。

这意味着若是改变了图层的z轴顺序,你会发现将不可以检测到最前方的视图点击事件,这是由于被另外一个图层遮盖住了,虽然它的zPosition值较小,可是在图层树中的顺序靠前。咱们将在第五章详细讨论这个问题。

自动布局

你可能用过UIViewAutoresizingMask类型的一些常量,应用于当父视图改变尺寸的时候,相应UIView的frame也跟着更新的场景(一般用于横竖屏切换)。

在iOS6中,苹果介绍了自动排版机制,它和自动调整不一样,而且更加复杂。

在Mac OS平台,CALayer有一个叫作layoutManager的属性能够经过CALayoutManager协议和CAConstraintLayoutManager类来实现自动排版的机制。但因为某些缘由,这在iOS上并不适用。

当使用视图的时候,能够充分利用UIView类接口暴露出来的UIViewAutoresizingMask和NSLayoutConstraintAPI,但若是想随意控制CALayer的布局,就须要手工操做。最简单的方法就是使用CALayerDelegate以下函数:

1
- (void)layoutSublayersOfLayer:(CALayer *)layer;

当图层的bounds发生改变,或者图层的-setNeedsLayout方法被调用的时候,这个函数将会被执行。这使得你能够手动地从新摆放或者从新调整子图层的大小,可是不能像UIView的autoresizingMask和constraints属性作到自适应屏幕旋转。

这也是为何最好使用视图而不是单独的图层来构建应用程序的另外一个重要缘由之一。

总结

本章涉及了CALayer的集合结构,包括它的frame,position和bounds,介绍了三维空间内图层的概念,以及如何在独立的图层内响应事件,最后简单说明了在iOS平台,Core Animation对自动调整和自动布局支持的缺少。

相关文章
相关标签/搜索