├── 1-图层树 ├── 1.1.jpeg ├── 1.2.jpeg ├── 1.3.jpeg ├── 1.4.jpeg ├── 1.5.jpeg └── 图层树.md ├── 10-缓冲 ├── 10.1.jpeg ├── 10.2.jpeg ├── 10.3.jpeg ├── 10.4.jpeg ├── 10.5.jpeg ├── 10.6.jpeg └── 缓冲.md ├── 11-基于定时器的动画 ├── 11.1.jpeg ├── 11.2.jpeg └── 基于定时器的动画.md ├── 12-性能调优 ├── 12.1.jpeg ├── 12.10.jpeg ├── 12.11.jpeg ├── 12.12.jpeg ├── 12.13.jpeg ├── 12.2.jpeg ├── 12.3.jpeg ├── 12.4.jpeg ├── 12.5.jpeg ├── 12.6.jpeg ├── 12.7.jpeg ├── 12.8.jpeg ├── 12.9.jpeg └── 性能调优.md ├── 13-高效绘图 ├── 13-高效绘图.md ├── 13.1.png ├── 13.2.png ├── 13.3.png └── 13.4.png ├── 14-图像IO ├── 14.1.jpeg ├── 14.2.jpeg ├── 14.3.jpeg ├── 14.4.jpeg ├── 14.5.jpeg └── 图像IO.md ├── 15-图层性能 ├── 15-图层性能.md └── 15.1.png ├── 2-寄宿图 ├── 2.1.png ├── 2.10.png ├── 2.11.png ├── 2.12.png ├── 2.2.png ├── 2.3.png ├── 2.4.png ├── 2.5.png ├── 2.6.png ├── 2.7.png ├── 2.8.png ├── 2.9.png └── 寄宿图.md ├── 3-图层几何学 ├── 3.1.jpeg ├── 3.10.jpeg ├── 3.2.jpeg ├── 3.3.jpeg ├── 3.4.jpeg ├── 3.5.jpeg ├── 3.6.jpeg ├── 3.7.jpeg ├── 3.8.jpeg ├── 3.9.jpeg └── 图层几何学.md ├── 4-视觉效果 ├── 4-视觉效果.md ├── 4.1.png ├── 4.10.png ├── 4.11.png ├── 4.12.png ├── 4.13.png ├── 4.14.png ├── 4.15.png ├── 4.16.png ├── 4.17.png ├── 4.18.png ├── 4.19.png ├── 4.2.png ├── 4.20.png ├── 4.21.png ├── 4.3.png ├── 4.4.png ├── 4.5.png ├── 4.6.png ├── 4.7.png ├── 4.8.png └── 4.9.png ├── 5-变换 ├── 5.1.jpeg ├── 5.10.jpeg ├── 5.11.jpeg ├── 5.12.jpeg ├── 5.13.jpeg ├── 5.14.jpeg ├── 5.15.jpeg ├── 5.16.jpeg ├── 5.17.jpeg ├── 5.18.jpeg ├── 5.19.jpeg ├── 5.2.jpeg ├── 5.20.jpeg ├── 5.21.jpeg ├── 5.22.jpeg ├── 5.23.jpeg ├── 5.3.jpeg ├── 5.4.jpeg ├── 5.5.jpeg ├── 5.6.jpeg ├── 5.7.jpeg ├── 5.8.jpeg ├── 5.9.jpeg └── 变换.md ├── 6-专有图层 ├── 6-专有图层.md ├── 6.1.png ├── 6.10.png ├── 6.11.png ├── 6.12.png ├── 6.13.png ├── 6.14.png ├── 6.15.png ├── 6.16.png ├── 6.17.png ├── 6.2.png ├── 6.3.png ├── 6.4.png ├── 6.5.png ├── 6.6.png ├── 6.7.png ├── 6.8.png └── 6.9.png ├── 7-隐式动画 ├── 7.1.jpeg ├── 7.2.jpeg ├── 7.3.jpeg ├── 7.4.jpeg └── 隐式动画.md ├── 8-显式动画 ├── 8.1.jpeg ├── 8.2.jpeg ├── 8.3.jpeg ├── 8.4.jpeg ├── 8.5.jpeg ├── 8.6.jpeg └── 显式动画.md ├── 9-图层时间 ├── 9.1.jpeg ├── 9.2.jpeg ├── 9.3.jpeg └── 图层时间.md └── README.md /1-图层树/1.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/1-图层树/1.1.jpeg -------------------------------------------------------------------------------- /1-图层树/1.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/1-图层树/1.2.jpeg -------------------------------------------------------------------------------- /1-图层树/1.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/1-图层树/1.3.jpeg -------------------------------------------------------------------------------- /1-图层树/1.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/1-图层树/1.4.jpeg -------------------------------------------------------------------------------- /1-图层树/1.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/1-图层树/1.5.jpeg -------------------------------------------------------------------------------- /1-图层树/图层树.md: -------------------------------------------------------------------------------- 1 | #图层的树状结构 2 | 3 | >巨妖有图层,洋葱也有图层,你有吗?我们都有图层 -- 史莱克 4 | 5 | Core Animation其实是一个令人误解的命名。你可能认为它只是用来做动画的,但实际上它是从一个叫做*Layer Kit*这么一个不怎么和动画有关的名字演变而来,所以做动画这只是Core Animation特性的冰山一角。 6 | 7 | Core Animation是一个*复合引擎*,它的职责就是尽可能快地组合屏幕上不同的可视内容,这个内容是被分解成独立的*图层*,存储在一个叫做*图层树*的体系之中。于是这个树形成了**UIKit**以及在iOS应用程序当中你所能在屏幕上看见的一切的基础。 8 | 9 | 在我们讨论动画之前,我们将从图层树开始,涉及一下Core Animation的*静态*组合以及布局特性。 10 | 11 | ##图层和视图 12 | 如果你曾经在iOS或者Mac OS平台上写过应用程序,你可能会对*视图*的概念比较熟悉。一个视图就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置。图1.1显示了一种典型的视图层级关系 13 | 14 | 图1.1 15 | 16 | 图1.1 一种典型的iOS屏幕(左边)和形成视图的层级关系(右边) 17 | 18 | 在iOS当中,所有的视图都从一个叫做`UIVIew`的基类派生而来,`UIView`可以处理触摸事件,可以支持基于*Core Graphics*绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。 19 | 20 | ###CALayer 21 | `CALayer`类在概念上和`UIView`类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和`UIView`最大的不同是`CALayer`不处理用户的交互。 22 | 23 | `CALayer`并不清楚具体的*响应链*(iOS通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,即使它提供了一些方法来判断是否一个触点在图层的范围之内(具体见第三章,“图层的几何学”) 24 | 25 | ###平行的层级关系 26 | 每一个`UIview`都有一个`CALayer`实例的图层属性,也就是所谓的*backing layer*,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作(见图1.2)。 27 | 28 | 图1.2 29 | 30 | 图1.2 图层的树状结构(左边)以及对应的视图层级(右边) 31 | 32 | 实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,`UIView`仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。 33 | 34 | 但是为什么iOS要基于`UIView`和`CALayer`提供两个平行的层级关系呢?为什么不用一个简单的层级来处理所有事情呢?原因在于要做职责分离,这样也能避免很多重复代码。在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和`UIView`,但是Mac OS有AppKit和`NSView`的原因。他们功能上很相似,但是在实现上有着显著的区别。 35 | 36 | 绘图,布局和动画,相比之下就是类似Mac笔记本和桌面系列一样应用于iPhone和iPad触屏的概念。把这种功能的逻辑分开并应用到独立的Core Animation框架,苹果就能够在iOS和Mac OS之间共享代码,使得对苹果自己的OS开发团队和第三方开发者去开发两个平台的应用更加便捷。 37 | 38 | 实际上,这里并不是两个层级关系,而是四个,每一个都扮演不同的角色,除了视图层级和图层树之外,还存在*呈现树*和*渲染树*,将在第七章“隐式动画”和第十二章“性能调优”分别讨论。 39 | 40 | ##图层的能力 41 | 如果说`CALayer`是`UIView`内部实现细节,那我们为什么要全面地了解它呢?苹果当然为我们提供了优美简洁的`UIView`接口,那么我们是否就没必要直接去处理Core Animation的细节了呢? 42 | 43 | 某种意义上说的确是这样,对一些简单的需求来说,我们确实没必要处理`CALayer`,因为苹果已经通过`UIView`的高级API间接地使得动画变得很简单。 44 | 45 | 但是这种简单会不可避免地带来一些灵活上的缺陷。如果你略微想在底层做一些改变,或者使用一些苹果没有在`UIView`上实现的接口功能,这时除了介入Core Animation底层之外别无选择。 46 | 47 | 我们已经证实了图层不能像视图那样处理触摸事件,那么他能做哪些视图不能做的呢?这里有一些`UIView`没有暴露出来的CALayer的功能: 48 | 49 | * 阴影,圆角,带颜色的边框 50 | * 3D变换 51 | * 非矩形范围 52 | * 透明遮罩 53 | * 多级非线性动画 54 | 55 | 我们将会在后续章节中探索这些功能,首先我们要关注一下在应用程序当中`CALayer`是怎样被利用起来的。 56 | 57 | ##使用图层 58 | 首先我们来创建一个简单的项目,来操纵一些`layer`的属性。打开Xcode,使用*Single View Application*模板创建一个工程。 59 | 60 | 在屏幕中央创建一个小视图(大约200 X 200的尺寸),当然你可以手工编码,或者使用Interface Builder(随你方便)。确保你的视图控制器要添加一个视图的属性以便可以直接访问它。我们把它称作`layerView`。 61 | 62 | 运行项目,应该能在浅灰色屏幕背景中看见一个白色方块(图1.3),如果没看见,可能需要调整一下背景window或者view的颜色 63 | 64 | 图1.3 65 | 66 | 图1.3 灰色背景上的一个白色`UIView` 67 | 68 | 这并没有什么令人激动的地方,我们来添加一个色块,在白色方块中间添加一个小的蓝色块。 69 | 70 | 我们当然可以简单地在已经存在的`UIView`上添加一个子视图(随意用代码或者IB),但这不能真正学到任何关于图层的东西。 71 | 72 | 于是我们来创建一个`CALayer`,并且把它作为我们视图相关图层的子图层。尽管`UIView`类的接口中暴露了图层属性,但是标准的Xcode项目模板并没有包含Core Animation相关头文件。所以如果我们不给项目添加合适的库,是不能够使用任何图层相关的方法或者访问它的属性。所以首先需要添加QuartzCore框架到Build Phases标签(图1.4),然后在vc的.m文件中引入库。 73 | 74 | 图1.4 75 | 76 | 图1.4 把QuartzCore库添加到项目 77 | 78 | 之后就可以在代码中直接引用`CALayer`的属性和方法。在清单1.1中,我们用创建了一个`CALayer`,设置了它的`backgroundColor`属性,然后添加到`layerView`背后相关图层的子图层(这段代码的前提是通过IB创建了`layerView`并做好了连接),图1.5显示了结果。 79 | 80 | 清单1.1 给视图添加一个蓝色子图层 81 | ``` objective-c 82 | #import "ViewController.h" 83 | #import @interface ViewController () 84 | @property (nonatomic, weak) IBOutlet UIView *layerView;  85 | @end 86 | @implementation ViewController 87 | - (void)viewDidLoad 88 | { [super viewDidLoad]; //create sublayer CALayer *blueLayer = [CALayer layer]; blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); 89 | blueLayer.backgroundColor = [UIColor blueColor].CGColor; //add it to our view [self.layerView.layer addSublayer:blueLayer]; 90 | } @end ``` 91 | 图1.5 92 | 93 | 图1.5 白色`UIView`内部嵌套的蓝色`CALayer` 94 | 95 | 一个视图只有一个相关联的图层(自动创建),同时它也可以支持添加无数多个子图层,从清单1.1可以看出,你可以显示创建一个单独的图层,并且把它直接添加到视图关联图层的子图层。尽管可以这样添加图层,但往往我们只是见简单地处理视图,他们关联的图层并不需要额外地手动添加子图层。 96 | 97 | 在Mac OS平台,10.8版本之前,一个显著的性能缺陷就是由于用了视图层级而不是单独在一个视图内使用`CALayer`树状层级。但是在iOS平台,使用轻量级的`UIView`类并没有显著的性能影响(当然在Mac OS 10.8之后,`NSView`的性能同样也得到很大程度的提高)。 98 | 99 | 使用图层关联的视图而不是`CALayer`的好处在于,你能在使用所有`CALayer`底层特性的同时,也可以使用`UIView`的高级API(比如自动排版,布局和事件处理)。 100 | 101 | 然而,当满足以下条件的时候,你可能更需要使用`CALayer`而不是`UIView` 102 | 103 | * 开发同时可以在Mac OS上运行的跨平台应用 104 | * 使用多种`CALayer`的子类(见第六章,“特殊的图层“),并且不想创建额外的`UIView`去包封装它们所有 105 | * 做一些对性能特别挑剔的工作,比如对`UIView`一些可忽略不计的操作都会引起显著的不同(尽管如此,你可能会直接想使用OpenGL绘图) 106 | 107 | 108 | 但是这些例子都很少见,总的来说,处理视图会比单独处理图层更加方便。 109 | 110 | ##总结 111 | 这一章阐述了图层的树状结构,说明了如何在iOS中由`UIView`的层级关系形成的一种平行的`CALayer`层级关系,在后面的实验中,我们创建了自己的`CALayer`,并把它添加到图层树中。 112 | 113 | 在第二章,“图层关联的图片”,我们将要研究一下`CALayer`关联的图片,以及Core Animation提供的操作显示的一些特性。 -------------------------------------------------------------------------------- /10-缓冲/10.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/10-缓冲/10.1.jpeg -------------------------------------------------------------------------------- /10-缓冲/10.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/10-缓冲/10.2.jpeg -------------------------------------------------------------------------------- /10-缓冲/10.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/10-缓冲/10.3.jpeg -------------------------------------------------------------------------------- /10-缓冲/10.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/10-缓冲/10.4.jpeg -------------------------------------------------------------------------------- /10-缓冲/10.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/10-缓冲/10.5.jpeg -------------------------------------------------------------------------------- /10-缓冲/10.6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/10-缓冲/10.6.jpeg -------------------------------------------------------------------------------- /10-缓冲/缓冲.md: -------------------------------------------------------------------------------- 1 | #缓冲 2 | 3 | >生活和艺术一样,最美的永远是曲线。 -- 爱德华布尔沃 - 利顿 4 | 5 | 在第九章“图层时间”中,我们讨论了动画时间和`CAMediaTiming`协议。现在我们来看一下另一个和时间相关的机制--所谓的*缓冲*。Core Animation使用缓冲来使动画移动更平滑更自然,而不是看起来的那种机械和人工,在这一章我们将要研究如何对你的动画控制和自定义缓冲曲线。 6 | 7 | ##动画速度 8 | 9 | 动画实际上就是一段时间内的变化,这就暗示了变化一定是随着某个特定的速率进行。速率由以下公式计算而来: 10 | 11 | velocity = change / time 12 | 13 | 这里的*变化*可以指的是一个物体移动的距离,*时间*指动画持续的时长,用这样的一个移动可以更加形象的描述(比如`position`和`bounds`属性的动画),但实际上它应用于任意可以做动画的属性(比如`color`和`opacity`)。 14 | 15 | 上面的等式假设了速度在整个动画过程中都是恒定不变的(就如同第八章“显式动画”的情况),对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是*完全不真实*的一种效果。 16 | 17 | 考虑一个场景,一辆车行驶在一定距离内,它并不会一开始就以60mph的速度行驶,然后到达终点后突然变成0mph。一是因为需要无限大的加速度(即使是最好的车也不会在0秒内从0跑到60),另外不然的话会干死所有乘客。在现实中,它会慢慢地加速到全速,然后当它接近终点的时候,它会慢慢地减速,直到最后停下来。 18 | 19 | 那么对于一个掉落到地上的物体又会怎样呢?它会首先停在空中,然后一直加速到落到地面,然后突然停止(然后由于积累的动能转换伴随着一声巨响,砰!)。 20 | 21 | 现实生活中的任何一个物体都会在运动中加速或者减速。那么我们如何在动画中实现这种加速度呢?一种方法是使用*物理引擎*来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为*缓冲函数*,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。 22 | 23 | ###`CAMediaTimingFunction` 24 | 25 | 那么该如何使用缓冲方程式呢?首先需要设置`CAAnimation`的`timingFunction`属性,是`CAMediaTimingFunction`类的一个对象。如果想改变隐式动画的计时函数,同样也可以使用`CATransaction`的`+setAnimationTimingFunction:`方法。 26 | 27 | 这里有一些方式来创建`CAMediaTimingFunction`,最简单的方式是调用`+timingFunctionWithName:`的构造方法。这里传入如下几个常量之一: 28 | 29 | kCAMediaTimingFunctionLinear 30 | kCAMediaTimingFunctionEaseIn 31 | kCAMediaTimingFunctionEaseOut 32 | kCAMediaTimingFunctionEaseInEaseOut 33 | kCAMediaTimingFunctionDefault 34 | 35 | `kCAMediaTimingFunctionLinear`选项创建了一个线性的计时函数,同样也是`CAAnimation`的`timingFunction`属性为空时候的默认函数。线性步调对于那些立即加速并且保持匀速到达终点的场景会有意义(例如射出枪膛的子弹),但是默认来说它看起来很奇怪,因为对大多数的动画来说确实很少用到。 36 | 37 | `kCAMediaTimingFunctionEaseIn`常量创建了一个慢慢加速然后突然停止的方法。对于之前提到的自由落体的例子来说很适合,或者比如对准一个目标的导弹的发射。 38 | 39 | `kCAMediaTimingFunctionEaseOut`则恰恰相反,它以一个全速开始,然后慢慢减速停止。它有一个削弱的效果,应用的场景比如一扇门慢慢地关上,而不是砰地一声。 40 | 41 | `kCAMediaTimingFunctionEaseInEaseOut`创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。如果只可以用一种缓冲函数的话,那就必须是它了。那么你会疑惑为什么这不是默认的选择,实际上当使用`UIView`的动画方法时,他的确是默认的,但当创建`CAAnimation`的时候,就需要手动设置它了。 42 | 43 | 最后还有一个`kCAMediaTimingFunctionDefault`,它和`kCAMediaTimingFunctionEaseInEaseOut`很类似,但是加速和减速的过程都稍微有些慢。它和`kCAMediaTimingFunctionEaseInEaseOut`的区别很难察觉,可能是苹果觉得它对于隐式动画来说更适合(然后对UIKit就改变了想法,而是使用`kCAMediaTimingFunctionEaseInEaseOut`作为默认效果),虽然它的名字说是默认的,但还是要记住当创建*显式*的`CAAnimation`它并不是默认选项(换句话说,默认的图层行为动画用`kCAMediaTimingFunctionDefault`作为它们的计时方法)。 44 | 45 | 你可以使用一个简单的测试工程来实验一下(清单10.1),在运行之前改变缓冲函数的代码,然后点击任何地方来观察图层是如何通过指定的缓冲移动的。 46 | 47 | 清单10.1 缓冲函数的简单测试 48 | 49 | ```objective-c 50 | @interface ViewController () 51 | 52 | @property (nonatomic, strong) CALayer *colorLayer; 53 | 54 | @end 55 | 56 | @implementation ViewController 57 | 58 | - (void)viewDidLoad 59 | { 60 | [super viewDidLoad]; 61 | //create a red layer 62 | self.colorLayer = [CALayer layer]; 63 | self.colorLayer.frame = CGRectMake(0, 0, 100, 100); 64 | self.colorLayer.position = CGPointMake(self.view.bounds.size.width/2.0, self.view.bounds.size.height/2.0); 65 | self.colorLayer.backgroundColor = [UIColor redColor].CGColor; 66 | [self.view.layer addSublayer:self.colorLayer]; 67 | } 68 | 69 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 70 | { 71 | //configure the transaction 72 | [CATransaction begin]; 73 | [CATransaction setAnimationDuration:1.0]; 74 | [CATransaction setAnimationTimingFunction:[CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionEaseOut]]; 75 | //set the position 76 | self.colorLayer.position = [[touches anyObject] locationInView:self.view]; 77 | //commit transaction 78 | [CATransaction commit]; 79 | } 80 | 81 | @end 82 | ``` 83 | 84 | ###`UIView`的动画缓冲 85 | 86 | UIKit的动画也同样支持这些缓冲方法的使用,尽管语法和常量有些不同,为了改变`UIView`动画的缓冲选项,给`options`参数添加如下常量之一: 87 | 88 | UIViewAnimationOptionCurveEaseInOut 89 | UIViewAnimationOptionCurveEaseIn 90 | UIViewAnimationOptionCurveEaseOut 91 | UIViewAnimationOptionCurveLinear 92 | 93 | 它们和`CAMediaTimingFunction`紧密关联,`UIViewAnimationOptionCurveEaseInOut`是默认值(这里没有`kCAMediaTimingFunctionDefault`相对应的值了)。 94 | 95 | 具体使用方法见清单10.2(注意到这里不再使用`UIView`额外添加的图层,因为UIKit的动画并不支持这类图层)。 96 | 97 | 清单10.2 使用UIKit动画的缓冲测试工程 98 | 99 | ```objective-c 100 | @interface ViewController () 101 | 102 | @property (nonatomic, strong) UIView *colorView; 103 | 104 | @end 105 | 106 | @implementation ViewController 107 | 108 | - (void)viewDidLoad 109 | { 110 | [super viewDidLoad]; 111 | //create a red layer 112 | self.colorView = [[UIView alloc] init]; 113 | self.colorView.bounds = CGRectMake(0, 0, 100, 100); 114 | self.colorView.center = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); 115 | self.colorView.backgroundColor = [UIColor redColor]; 116 | [self.view addSubview:self.colorView]; 117 | } 118 | 119 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 120 | { 121 | //perform the animation 122 | [UIView animateWithDuration:1.0 delay:0.0 123 | options:UIViewAnimationOptionCurveEaseOut 124 | animations:^{ 125 | //set the position 126 | self.colorView.center = [[touches anyObject] locationInView:self.view]; 127 | } 128 | completion:NULL]; 129 | 130 | } 131 | 132 | @end 133 | ``` 134 | 135 | ###缓冲和关键帧动画 136 | 137 | 或许你会回想起第八章里面颜色切换的关键帧动画由于线性变换的原因(见清单8.5)看起来有些奇怪,使得颜色变换非常不自然。为了纠正这点,我们来用更加合适的缓冲方法,例如`kCAMediaTimingFunctionEaseIn`,给图层的颜色变化添加一点*脉冲*效果,让它更像现实中的一个彩色灯泡。 138 | 139 | 我们不想给整个动画过程应用这个效果,我们希望对每个动画的过程重复这样的缓冲,于是每次颜色的变换都会有脉冲效果。 140 | 141 | `CAKeyframeAnimation`有一个`NSArray`类型的`timingFunctions`属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于`keyframes`数组的元素个数*减一*,因为它是描述每一帧之间动画速度的函数。 142 | 143 | 在这个例子中,我们自始至终想使用同一个缓冲函数,但我们同样需要一个函数的数组来告诉动画不停地重复每个步骤,而不是在整个动画序列只做一次缓冲,我们简单地使用包含多个相同函数拷贝的数组就可以了(见清单10.3)。 144 | 145 | 运行更新后的代码,你会发现动画看起来更加自然了。 146 | 147 | 清单10.3 对`CAKeyframeAnimation`使用`CAMediaTimingFunction` 148 | 149 | ```objective-c 150 | @interface ViewController () 151 | 152 | @property (nonatomic, weak) IBOutlet UIView *layerView; 153 | @property (nonatomic, weak) IBOutlet CALayer *colorLayer; 154 | 155 | @end 156 | 157 | @implementation ViewController 158 | 159 | - (void)viewDidLoad 160 | { 161 | [super viewDidLoad]; 162 | //create sublayer 163 | self.colorLayer = [CALayer layer]; 164 | self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); 165 | self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; 166 | //add it to our view 167 | [self.layerView.layer addSublayer:self.colorLayer]; 168 | } 169 | 170 | - (IBAction)changeColor 171 | { 172 | //create a keyframe animation 173 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; 174 | animation.keyPath = @"backgroundColor"; 175 | animation.duration = 2.0; 176 | animation.values = @[ 177 | (__bridge id)[UIColor blueColor].CGColor, 178 | (__bridge id)[UIColor redColor].CGColor, 179 | (__bridge id)[UIColor greenColor].CGColor, 180 | (__bridge id)[UIColor blueColor].CGColor ]; 181 | //add timing function 182 | CAMediaTimingFunction *fn = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn]; 183 | animation.timingFunctions = @[fn, fn, fn]; 184 | //apply animation to layer 185 | [self.colorLayer addAnimation:animation forKey:nil]; 186 | } 187 | 188 | @end 189 | ``` 190 | 191 | ##自定义缓冲函数 192 | 193 | 在第八章中,我们给时钟项目添加了动画。看起来很赞,但是如果有合适的缓冲函数就更好了。在显示世界中,钟表指针转动的时候,通常起步很慢,然后迅速啪地一声,最后缓冲到终点。但是标准的缓冲函数在这里每一个适合它,那该如何创建一个新的呢? 194 | 195 | 除了`+functionWithName:`之外,`CAMediaTimingFunction`同样有另一个构造函数,一个有四个浮点参数的`+functionWithControlPoints::::`(注意这里奇怪的语法,并没有包含具体每个参数的名称,这在objective-C中是合法的,但是却违反了苹果对方法命名的指导方针,而且看起来是一个奇怪的设计)。 196 | 197 | 使用这个方法,我们可以创建一个自定义的缓冲函数,来匹配我们的时钟动画,为了理解如何使用这个方法,我们要了解一些`CAMediaTimingFunction`是如何工作的。 198 | 199 | ###三次贝塞尔曲线 200 | 201 | `CAMediaTimingFunction`函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。我们可以用一个简单的图标来解释,横轴代表时间,纵轴代表改变的量,于是线性的缓冲就是一条从起点开始的简单的斜线(图10.1)。 202 | 203 | 图10.1 204 | 205 | 图10.1 线性缓冲函数的图像 206 | 207 | 这条曲线的斜率代表了速度,斜率的改变代表了加速度,原则上来说,任何加速的曲线都可以用这种图像来表示,但是`CAMediaTimingFunction`使用了一个叫做*三次贝塞尔曲线*的函数,它只可以产出指定缓冲函数的子集(我们之前在第八章中创建`CAKeyframeAnimation`路径的时候提到过三次贝塞尔曲线)。 208 | 209 | 你或许会回想起,一个三次贝塞尔曲线通过四个点来定义,第一个和最后一个点代表了曲线的起点和终点,剩下中间两个点叫做*控制点*,因为它们控制了曲线的形状,贝塞尔曲线的控制点其实是位于曲线之外的点,也就是说曲线并不一定要穿过它们。你可以把它们想象成吸引经过它们曲线的磁铁。 210 | 211 | 图10.2展示了一个三次贝塞尔缓冲函数的例子 212 | 213 | 图10.2 214 | 215 | 图10.2 三次贝塞尔缓冲函数 216 | 217 | 实际上它是一个很奇怪的函数,先加速,然后减速,最后快到达终点的时候又加速,那么标准的缓冲函数又该如何用图像来表示呢? 218 | 219 | `CAMediaTimingFunction`有一个叫做`-getControlPointAtIndex:values:`的方法,可以用来检索曲线的点,这个方法的设计的确有点奇怪(或许也就只有苹果能回答为什么不简单返回一个`CGPoint`),但是使用它我们可以找到标准缓冲函数的点,然后用`UIBezierPath`和`CAShapeLayer`来把它画出来。 220 | 221 | 曲线的起始和终点始终是{0, 0}和{1, 1},于是我们只需要检索曲线的第二个和第三个点(控制点)。具体代码见清单10.4。所有的标准缓冲函数的图像见图10.3。 222 | 223 | 清单10.4 使用`UIBezierPath`绘制`CAMediaTimingFunction` 224 | 225 | ```objective-c 226 | @interface ViewController () 227 | 228 | @property (nonatomic, weak) IBOutlet UIView *layerView; 229 | 230 | @end 231 | 232 | @implementation ViewController 233 | 234 | - (void)viewDidLoad 235 | { 236 | [super viewDidLoad]; 237 | //create timing function 238 | CAMediaTimingFunction *function = [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut]; 239 | //get control points 240 | CGPoint controlPoint1, controlPoint2; 241 | [function getControlPointAtIndex:1 values:(float *)&controlPoint1]; 242 | [function getControlPointAtIndex:2 values:(float *)&controlPoint2]; 243 | //create curve 244 | UIBezierPath *path = [[UIBezierPath alloc] init]; 245 | [path moveToPoint:CGPointZero]; 246 | [path addCurveToPoint:CGPointMake(1, 1) 247 | controlPoint1:controlPoint1 controlPoint2:controlPoint2]; 248 | //scale the path up to a reasonable size for display 249 | [path applyTransform:CGAffineTransformMakeScale(200, 200)]; 250 | //create shape layer 251 | CAShapeLayer *shapeLayer = [CAShapeLayer layer]; 252 | shapeLayer.strokeColor = [UIColor redColor].CGColor; 253 | shapeLayer.fillColor = [UIColor clearColor].CGColor; 254 | shapeLayer.lineWidth = 4.0f; 255 | shapeLayer.path = path.CGPath; 256 | [self.layerView.layer addSublayer:shapeLayer]; 257 | //flip geometry so that 0,0 is in the bottom-left 258 | self.layerView.layer.geometryFlipped = YES; 259 | } 260 | 261 | @end 262 | ``` 263 | 264 | 图10.3 265 | 266 | 图10.3 标准`CAMediaTimingFunction`缓冲曲线 267 | 268 | 那么对于我们自定义时钟指针的缓冲函数来说,我们需要初始微弱,然后迅速上升,最后缓冲到终点的曲线,通过一些实验之后,最终结果如下: 269 | 270 | [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1]; 271 | 272 | 如果把它转换成缓冲函数的图像,最后如图10.4所示,如果把它添加到时钟的程序,就形成了之前一直期待的非常赞的效果(见代清单10.5)。 273 | 274 | 图10.4 275 | 276 | 图10.4 自定义适合时钟的缓冲函数 277 | 278 | 清单10.5 添加了自定义缓冲函数的时钟程序 279 | 280 | ```objective-c 281 | - (void)setAngle:(CGFloat)angle forHand:(UIView *)handView animated:(BOOL)animated 282 | { 283 | //generate transform 284 | CATransform3D transform = CATransform3DMakeRotation(angle, 0, 0, 1); 285 | if (animated) { 286 | //create transform animation 287 | CABasicAnimation *animation = [CABasicAnimation animation]; 288 | animation.keyPath = @"transform"; 289 | animation.fromValue = [handView.layer.presentationLayer valueForKey:@"transform"]; 290 | animation.toValue = [NSValue valueWithCATransform3D:transform]; 291 | animation.duration = 0.5; 292 | animation.delegate = self; 293 | animation.timingFunction = [CAMediaTimingFunction functionWithControlPoints:1 :0 :0.75 :1]; 294 | //apply animation 295 | handView.layer.transform = transform; 296 | [handView.layer addAnimation:animation forKey:nil]; 297 | } else { 298 | //set transform directly 299 | handView.layer.transform = transform; 300 | } 301 | } 302 | ``` 303 | 304 | ###更加复杂的动画曲线 305 | 306 | 考虑一个橡胶球掉落到坚硬的地面的场景,当开始下落的时候,它会持续加速知道落到地面,然后经过几次反弹,最后停下来。如果用一张图来说明,它会如图10.5所示。 307 | 308 | 图10.5 309 | 310 | 图10.5 一个没法用三次贝塞尔曲线描述的反弹的动画 311 | 312 | 这种效果没法用一个简单的三次贝塞尔曲线表示,于是不能用`CAMediaTimingFunction`来完成。但如果想要实现这样的效果,可以用如下几种方法: 313 | 314 | * 用`CAKeyframeAnimation`创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数(具体下节介绍)。 315 | * 使用定时器逐帧更新实现动画(见第11章,“基于定时器的动画”)。 316 | 317 | ###基于关键帧的缓冲 318 | 319 | 为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过`keyTimes`来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。 320 | 321 | 清单10.6展示了实现反弹球动画的代码(见图10.6) 322 | 323 | 清单10.6 使用关键帧实现反弹球的动画 324 | 325 | ```objective-c 326 | @interface ViewController () 327 | 328 | @property (nonatomic, weak) IBOutlet UIView *containerView; 329 | @property (nonatomic, strong) UIImageView *ballView; 330 | 331 | @end 332 | 333 | @implementation ViewController 334 | 335 | - (void)viewDidLoad 336 | { 337 | [super viewDidLoad]; 338 | //add ball image view 339 | UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; 340 | self.ballView = [[UIImageView alloc] initWithImage:ballImage]; 341 | [self.containerView addSubview:self.ballView]; 342 | //animate 343 | [self animate]; 344 | } 345 | 346 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 347 | { 348 | //replay animation on tap 349 | [self animate]; 350 | } 351 | 352 | - (void)animate 353 | { 354 | //reset ball to top of screen 355 | self.ballView.center = CGPointMake(150, 32); 356 | //create keyframe animation 357 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; 358 | animation.keyPath = @"position"; 359 | animation.duration = 1.0; 360 | animation.delegate = self; 361 | animation.values = @[ 362 | [NSValue valueWithCGPoint:CGPointMake(150, 32)], 363 | [NSValue valueWithCGPoint:CGPointMake(150, 268)], 364 | [NSValue valueWithCGPoint:CGPointMake(150, 140)], 365 | [NSValue valueWithCGPoint:CGPointMake(150, 268)], 366 | [NSValue valueWithCGPoint:CGPointMake(150, 220)], 367 | [NSValue valueWithCGPoint:CGPointMake(150, 268)], 368 | [NSValue valueWithCGPoint:CGPointMake(150, 250)], 369 | [NSValue valueWithCGPoint:CGPointMake(150, 268)] 370 | ]; 371 | 372 | animation.timingFunctions = @[ 373 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn], 374 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut], 375 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn], 376 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut], 377 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn], 378 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseOut], 379 | [CAMediaTimingFunction functionWithName: kCAMediaTimingFunctionEaseIn] 380 | ]; 381 | 382 | animation.keyTimes = @[@0.0, @0.3, @0.5, @0.7, @0.8, @0.9, @0.95, @1.0]; 383 | //apply animation 384 | self.ballView.layer.position = CGPointMake(150, 268); 385 | [self.ballView.layer addAnimation:animation forKey:nil]; 386 | } 387 | 388 | @end 389 | ``` 390 | 391 | 图10.6 392 | 393 | 图10.6 使用关键帧实现的反弹球动画 394 | 395 | 这种方式还算不错,但是实现起来略显笨重(因为要不停地尝试计算各种关键帧和时间偏移)并且和动画强绑定了(因为如果要改变动画的一个属性,那就意味着要重新计算所有的关键帧)。那该如何写一个方法,用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面我们来实现它。 396 | 397 | ###流程自动化 398 | 399 | 在清单10.6中,我们把动画分割成相当大的几块,然后用Core Animation的缓冲进入和缓冲退出函数来大约形成我们想要的曲线。但如果我们把动画分割成更小的几部分,那么我们就可以用直线来拼接这些曲线(也就是线性缓冲)。为了实现自动化,我们需要知道如何做如下两件事情: 400 | 401 | * 自动把任意属性动画分割成多个关键帧 402 | * 用一个数学函数表示弹性动画,使得可以对帧做便宜 403 | 404 | 为了解决第一个问题,我们需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。对于简单的浮点起始值,公式如下(假设时间从0到1): 405 | 406 | value = (endValue – startValue) × time + startValue; 407 | 408 | 那么如果要插入一个类似于`CGPoint`,`CGColorRef`或者`CATransform3D`这种更加复杂类型的值,我们可以简单地对每个独立的元素应用这个方法(也就`CGPoint`中的x和y值,`CGColorRef`中的红,蓝,绿,透明值,或者是`CATransform3D`中独立矩阵的坐标)。我们同样需要一些逻辑在插值之前对对象拆解值,然后在插值之后在重新封装成对象,也就是说需要实时地检查类型。 409 | 410 | 一旦我们可以用代码获取属性动画的起始值之间的任意插值,我们就可以把动画分割成许多独立的关键帧,然后产出一个线性的关键帧动画。清单10.7展示了相关代码。 411 | 412 | 注意到我们用了60 x 动画时间(秒做单位)作为关键帧的个数,这时因为Core Animation按照每秒60帧去渲染屏幕更新,所以如果我们每秒生成60个关键帧,就可以保证动画足够的平滑(尽管实际上很可能用更少的帧率就可以达到很好的效果)。 413 | 414 | 我们在示例中仅仅引入了对`CGPoint`类型的插值代码。但是,从代码中很清楚能看出如何扩展成支持别的类型。作为不能识别类型的备选方案,我们仅仅在前一半返回了`fromValue`,在后一半返回了`toValue`。 415 | 416 | 清单10.7 使用插入的值创建一个关键帧动画 417 | 418 | ```objective-c 419 | float interpolate(float from, float to, float time) 420 | { 421 | return (to - from) * time + from; 422 | } 423 | 424 | - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time 425 | { 426 | if ([fromValue isKindOfClass:[NSValue class]]) { 427 | //get type 428 | const char *type = [fromValue objCType]; 429 | if (strcmp(type, @encode(CGPoint)) == 0) { 430 | CGPoint from = [fromValue CGPointValue]; 431 | CGPoint to = [toValue CGPointValue]; 432 | CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); 433 | return [NSValue valueWithCGPoint:result]; 434 | } 435 | } 436 | //provide safe default implementation 437 | return (time < 0.5)? fromValue: toValue; 438 | } 439 | 440 | - (void)animate 441 | { 442 | //reset ball to top of screen 443 | self.ballView.center = CGPointMake(150, 32); 444 | //set up animation parameters 445 | NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; 446 | NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; 447 | CFTimeInterval duration = 1.0; 448 | //generate keyframes 449 | NSInteger numFrames = duration * 60; 450 | NSMutableArray *frames = [NSMutableArray array]; 451 | for (int i = 0; i < numFrames; i++) { 452 | float time = 1 / (float)numFrames * i; 453 | [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]]; 454 | } 455 | //create keyframe animation 456 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; 457 | animation.keyPath = @"position"; 458 | animation.duration = 1.0; 459 | animation.delegate = self; 460 | animation.values = frames; 461 | //apply animation 462 | [self.ballView.layer addAnimation:animation forKey:nil]; 463 | } 464 | ``` 465 | 466 | 这可以起到作用,但效果并不是很好,到目前为止我们所完成的只是一个非常复杂的方式来使用线性缓冲复制`CABasicAnimation`的行为。这种方式的好处在于我们可以更加精确地控制缓冲,这也意味着我们可以应用一个完全定制的缓冲函数。那么该如何做呢? 467 | 468 | 缓冲背后的数学并不很简单,但是幸运的是我们不需要一一实现它。罗伯特·彭纳有一个网页关于缓冲函数([http://www.robertpenner.com/easing](http://www.robertpenner.com/easing)),包含了大多数普遍的缓冲函数的多种编程语言的实现的链接,包括C。这里是一个缓冲进入缓冲退出函数的示例(实际上有很多不同的方式去实现它)。 469 | 470 | ```c 471 | float quadraticEaseInOut(float t) 472 | { 473 | return (t < 0.5)? (2 * t * t): (-2 * t * t) + (4 * t) - 1; 474 | } 475 | ``` 476 | 477 | 对我们的弹性球来说,我们可以使用`bounceEaseOut`函数: 478 | ```c 479 | float bounceEaseOut(float t) 480 | { 481 | if (t < 4/11.0) { 482 | return (121 * t * t)/16.0; 483 | } else if (t < 8/11.0) { 484 | return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; 485 | } else if (t < 9/10.0) { 486 | return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; 487 | } 488 | return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; 489 | } 490 | ``` 491 | 492 | 如果修改清单10.7的代码来引入`bounceEaseOut`方法,我们的任务就是仅仅交换缓冲函数,现在就可以选择任意的缓冲类型创建动画了(见清单10.8)。 493 | 494 | 清单10.8 用关键帧实现自定义的缓冲函数 495 | 496 | ```objective-c 497 | - (void)animate 498 | { 499 | //reset ball to top of screen 500 | self.ballView.center = CGPointMake(150, 32); 501 | //set up animation parameters 502 | NSValue *fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; 503 | NSValue *toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; 504 | CFTimeInterval duration = 1.0; 505 | //generate keyframes 506 | NSInteger numFrames = duration * 60; 507 | NSMutableArray *frames = [NSMutableArray array]; 508 | for (int i = 0; i < numFrames; i++) { 509 | float time = 1/(float)numFrames * i; 510 | //apply easing 511 | time = bounceEaseOut(time); 512 | //add keyframe 513 | [frames addObject:[self interpolateFromValue:fromValue toValue:toValue time:time]]; 514 | } 515 | //create keyframe animation 516 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; 517 | animation.keyPath = @"position"; 518 | animation.duration = 1.0; 519 | animation.delegate = self; 520 | animation.values = frames; 521 | //apply animation 522 | [self.ballView.layer addAnimation:animation forKey:nil]; 523 | } 524 | ``` 525 | 526 | ##总结 527 | 在这一章中,我们了解了有关缓冲和`CAMediaTimingFunction`类,它可以允许我们创建自定义的缓冲函数来完善我们的动画,同样了解了如何用`CAKeyframeAnimation`来避开`CAMediaTimingFunction`的限制,创建完全自定义的缓冲函数。 528 | 529 | 在下一章中,我们将要研究基于定时器的动画--另一个给我们对动画更多控制的选择,并且实现对动画的实时操纵。 530 | -------------------------------------------------------------------------------- /11-基于定时器的动画/11.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/11-基于定时器的动画/11.1.jpeg -------------------------------------------------------------------------------- /11-基于定时器的动画/11.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/11-基于定时器的动画/11.2.jpeg -------------------------------------------------------------------------------- /11-基于定时器的动画/基于定时器的动画.md: -------------------------------------------------------------------------------- 1 | #基于定时器的动画 2 | 3 | > *我可以指导你,但是你必须按照我说的做。* -- 骇客帝国 4 | 5 | 在第10章“缓冲”中,我们研究了`CAMediaTimingFunction`,它是一个通过控制动画缓冲来模拟物理效果例如加速或者减速来增强现实感的东西,那么如果想更加真实地模拟物理交互或者实时根据用户输入修改动画改怎么办呢?在这一章中,我们将继续探索一种能够允许我们精确地控制一帧一帧展示的基于定时器的动画。 6 | 7 | ##定时帧 8 | 9 | 动画看起来是用来显示一段连续的运动过程,但实际上当在固定位置上展示像素的时候并不能做到这一点。一般来说这种显示都无法做到连续的移动,能做的仅仅是足够快地展示一系列静态图片,只是看起来像是做了运动。 10 | 11 | 我们之前提到过iOS按照每秒60次刷新屏幕,然后`CAAnimation`计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,`CAAnimation`最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。 12 | 13 | 在第10章中,我们解决了如何自定义缓冲函数,然后根据需要展示的帧的数组来告诉`CAKeyframeAnimation`的实例如何去绘制。所有的Core Animation实际上都是按照一定的序列来显示这些帧,那么我们可以自己做到这些么? 14 | 15 | ###`NSTimer` 16 | 17 | 实际上,我们在第三章“图层几何学”中已经做过类似的东西,就是时钟那个例子,我们用了`NSTimer`来对钟表的指针做定时动画,一秒钟更新一次,但是如果我们把频率调整成一秒钟更新60次的话,原理是完全相同的。 18 | 19 | 我们来试着用`NSTimer`来修改第十章中弹性球的例子。由于现在我们在定时器启动之后连续计算动画帧,我们需要在类中添加一些额外的属性来存储动画的`fromValue`,`toValue`,`duration`和当前的`timeOffset`(见清单11.1)。 20 | 21 | 清单11.1 使用`NSTimer`实现弹性球动画 22 | 23 | ```objective-c 24 | @interface ViewController () 25 | @property (nonatomic, weak) IBOutlet UIView *containerView; 26 | @property (nonatomic, strong) UIImageView *ballView; 27 | @property (nonatomic, strong) NSTimer *timer; @property (nonatomic, assign) NSTimeInterval duration; 28 | @property (nonatomic, assign) NSTimeInterval timeOffset; 29 | @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue; 30 | 31 | @end 32 | @implementation ViewController 33 | - (void)viewDidLoad 34 | { [super viewDidLoad]; //add ball image view UIImage *ballImage = [UIImage imageNamed:@"Ball.png"]; 35 | self.ballView = [[UIImageView alloc] initWithImage:ballImage]; 36 | [self.containerView addSubview:self.ballView]; //animate [self animate]; 37 | } 38 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 39 | { //replay animation on tap [self animate]; 40 | } 41 | float interpolate(float from, float to, float time) 42 | { return (to - from) * time + from; 43 | } 44 | - (id)interpolateFromValue:(id)fromValue toValue:(id)toValue time:(float)time { if ([fromValue isKindOfClass:[NSValue class]]) { //get type const char *type = [(NSValue *)fromValue objCType]; 45 | if (strcmp(type, @encode(CGPoint)) == 0) { CGPoint from = [fromValue CGPointValue]; CGPoint to = [toValue CGPointValue]; CGPoint result = CGPointMake(interpolate(from.x, to.x, time), interpolate(from.y, to.y, time)); 46 | return [NSValue valueWithCGPoint:result]; } 47 | } //provide safe default implementation return (time < 0.5)? fromValue: toValue; 48 | } 49 | float bounceEaseOut(float t) 50 | { if (t < 4/11.0) { return (121 * t * t)/16.0; 51 | } else if (t < 8/11.0) { return (363/40.0 * t * t) - (99/10.0 * t) + 17/5.0; 52 | } else if (t < 9/10.0) { return (4356/361.0 * t * t) - (35442/1805.0 * t) + 16061/1805.0; 53 | } return (54/5.0 * t * t) - (513/25.0 * t) + 268/25.0; 54 | } 55 | 56 | - (void)animate 57 | { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; 58 | self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.timer = [NSTimer scheduledTimerWithTimeInterval:1/60.0 59 | target:self 60 | selector:@selector(step:) 61 | userInfo:nil repeats:YES]; 62 | } 63 | - (void)step:(NSTimer *)step 64 | { //update time offset self.timeOffset = MIN(self.timeOffset + 1/60.0, self.duration); 65 | //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; 66 | //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue 67 | toValue:self.toValue time:time]; 68 | //move ball view to new position 69 | self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; 70 | self.timer = nil; 71 | } } 72 | @end 73 | ``` 74 | 75 | 很赞,而且和基于关键帧例子的代码一样很多,但是如果想一次性在屏幕上对很多东西做动画,很明显就会有很多问题。 76 | 77 | `NSTimer`并不是最佳方案,为了理解这点,我们需要确切地知道`NSTimer`是如何工作的。iOS上的每个线程都管理了一个`NSRunloop`,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项: 78 | 79 | * 处理触摸事件 80 | * 发送和接受网络数据包 81 | * 执行使用gcd的代码 82 | * 处理计时器行为 83 | * 屏幕重绘 84 | 85 | 当你设置一个`NSTimer`,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。 86 | 87 | 屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。 88 | 89 | 我们可以通过一些途径来优化: 90 | * 我们可以用`CADisplayLink`让更新频率严格控制在每次屏幕刷新之后。 91 | * 基于真实帧的持续时间而不是假设的更新频率来做动画。 92 | * 调整动画计时器的`run loop`模式,这样就不会被别的事件干扰。 93 | 94 | ###`CADisplayLink` 95 | 96 | `CADisplayLink`是CoreAnimation提供的另一个类似于`NSTimer`的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和`NSTimer`很类似,所以它实际上就是一个内置实现的替代,但是和`timeInterval`以秒为单位不同,`CADisplayLink`有一个整型的`frameInterval`属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定`frameInterval`为2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。 97 | 98 | 用`CADisplayLink`而不是`NSTimer`,会保证帧率足够连续,使得动画看起来更加平滑,但即使`CADisplayLink`也不能*保证*每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用`NSTimer`的时候,一旦有机会计时器就会开启,但是`CADisplayLink`却不一样:如果它丢失了帧,就会直接忽略它们,然后在下一次更新的时候接着运行。 99 | 100 | ###计算帧的持续时间 101 | 102 | 无论是使用`NSTimer`还是`CADisplayLink`,我们仍然需要处理一帧的时间超出了预期的六十分之一秒。由于我们不能够计算出一帧真实的持续时间,所以需要手动测量。我们可以在每帧开始刷新的时候用`CACurrentMediaTime()`记录当前时间,然后和上一帧记录的时间去比较。 103 | 104 | 通过比较这些时间,我们就可以得到真实的每帧持续的时间,然后代替硬编码的六十分之一秒。我们来更新一下上个例子(见清单11.2)。 105 | 106 | 清单11.2 通过测量没帧持续的时间来使得动画更加平滑 107 | 108 | ```objective-c 109 | @interface ViewController () 110 | @property (nonatomic, weak) IBOutlet UIView *containerView; 111 | @property (nonatomic, strong) UIImageView *ballView; 112 | @property (nonatomic, strong) CADisplayLink *timer; 113 | @property (nonatomic, assign) CFTimeInterval duration; 114 | @property (nonatomic, assign) CFTimeInterval timeOffset; 115 | @property (nonatomic, assign) CFTimeInterval lastStep; 116 | @property (nonatomic, strong) id fromValue; @property (nonatomic, strong) id toValue; 117 | @end 118 | @implementation ViewController 119 | 120 | ... 121 | - (void)animate 122 | { //reset ball to top of screen self.ballView.center = CGPointMake(150, 32); //configure the animation self.duration = 1.0; self.timeOffset = 0.0; self.fromValue = [NSValue valueWithCGPoint:CGPointMake(150, 32)]; 123 | self.toValue = [NSValue valueWithCGPoint:CGPointMake(150, 268)]; //stop the timer if it's already running [self.timer invalidate]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; 124 | [self.timer addToRunLoop:[NSRunLoop mainRunLoop] 125 | forMode:NSDefaultRunLoopMode]; } 126 | - (void)step:(CADisplayLink *)timer 127 | { //calculate time delta CFTimeInterval thisStep = CACurrentMediaTime(); 128 | CFTimeInterval stepDuration = thisStep - self.lastStep; 129 | self.lastStep = thisStep; //update time offset self.timeOffset = MIN(self.timeOffset + stepDuration, self.duration); //get normalized time offset (in range 0 - 1) float time = self.timeOffset / self.duration; 130 | //apply easing time = bounceEaseOut(time); //interpolate position id position = [self interpolateFromValue:self.fromValue toValue:self.toValue time:time]; 131 | //move ball view to new position 132 | self.ballView.center = [position CGPointValue]; //stop the timer if we've reached the end of the animation if (self.timeOffset >= self.duration) { [self.timer invalidate]; self.timer = nil; 133 | } } 134 | @end 135 | ``` 136 | 137 | ###Run Loop 模式 138 | 139 | 注意到当创建`CADisplayLink`的时候,我们需要指定一个`run loop`和`run loop mode`,对于run loop来说,我们就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。 140 | 一个典型的例子就是当是用`UIScrollview`滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的`NSTimer`和网络请求就不会启动,一些常见的run loop模式如下: 141 | * `NSDefaultRunLoopMode` - 标准优先级 142 | * `NSRunLoopCommonModes` - 高优先级 143 | * `UITrackingRunLoopMode` - 用于`UIScrollView`和别的控件的动画 144 | 145 | 在我们的例子中,我们是用了`NSDefaultRunLoopMode`,但是不能保证动画平滑的运行,所以就可以用`NSRunLoopCommonModes`来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。 146 | 147 | 同样可以同时对`CADisplayLink`指定多个run loop模式,于是我们可以同时加入`NSDefaultRunLoopMode`和`UITrackingRunLoopMode`来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样: 148 | 149 | self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode]; [self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode]; 150 | 和`CADisplayLink`类似,`NSTimer`同样也可以使用不同的run loop模式配置,通过别的函数,而不是`+scheduledTimerWithTimeInterval:`构造器 151 | self.timer = [NSTimer timerWithTimeInterval:1/60.0 152 | target:self selector:@selector(step:) 153 | userInfo:nil repeats:YES]; 154 | [[NSRunLoop mainRunLoop] addTimer:self.timer 155 | forMode:NSRunLoopCommonModes]; 156 | 157 | ##物理模拟 158 | 159 | 即使使用了基于定时器的动画来复制第10章中关键帧的行为,但还是会有一些本质上的区别:在关键帧的实现中,我们提前计算了所有帧,但是在新的解决方案中,我们实际上实在按需要在计算。意义在于我们可以根据用户输入实时修改动画的逻辑,或者和别的实时动画系统例如物理引擎进行整合。 160 | 161 | ###Chipmunk 162 | 163 | 我们来基于物理学创建一个真实的重力模拟效果来取代当前基于缓冲的弹性动画,但即使模拟2D的物理效果就已近极其复杂了,所以就不要尝试去实现它了,直接用开源的物理引擎库好了。 164 | 165 | 我们将要使用的物理引擎叫做Chipmunk。另外的2D物理引擎也同样可以(例如Box2D),但是Chipmunk使用纯C写的,而不是C++,好处在于更容易和Objective-C项目整合。Chipmunk有很多版本,包括一个和Objective-C绑定的“indie”版本。C语言的版本是免费的,所以我们就用它好了。在本书写作的时候6.1.4是最新的版本;你可以从http://chipmunk-physics.net下载它。 166 | Chipmunk完整的物理引擎相当巨大复杂,但是我们只会使用如下几个类: 167 | * `cpSpace` - 这是所有的物理结构体的容器。它有一个大小和一个可选的重力矢量 * `cpBody` - 它是一个固态无弹力的刚体。它有一个坐标,以及其他物理属性,例如质量,运动和摩擦系数等等。 168 | * `cpShape` - 它是一个抽象的几何形状,用来检测碰撞。可以给结构体添加一个多边形,而且`cpShape`有各种子类来代表不同形状的类型。 169 | 在例子中,我们来对一个木箱建模,然后在重力的影响下下落。我们来创建一个`Crate`类,包含屏幕上的可视效果(一个`UIImageView`)和一个物理模型(一个`cpBody`和一个`cpPolyShape`,一个`cpShape`的多边形子类来代表矩形木箱)。 170 | 171 | 用C版本的Chipmunk会带来一些挑战,因为它现在并不支持Objective-C的引用计数模型,所以我们需要准确的创建和释放对象。为了简化,我们把`cpShape`和`cpBody`的生命周期和`Crate`类进行绑定,然后在木箱的`-init`方法中创建,在`-dealloc`中释放。木箱物理属性的配置很复杂,所以阅读了Chipmunk文档会很有意义。 172 | 173 | 视图控制器用来管理`cpSpace`,还有和之前一样的计时器逻辑。在每一步中,我们更新`cpSpace`(用来进行物理计算和所有结构体的重新摆放)然后迭代对象,然后再更新我们的木箱视图的位置来匹配木箱的模型(在这里,实际上只有一个结构体,但是之后我们将要添加更多)。 174 | Chipmunk使用了一个和UIKit颠倒的坐标系(Y轴向上为正方向)。为了使得物理模型和视图之间的同步更简单,我们需要通过使用`geometryFlipped`属性翻转容器视图的集合坐标(第3章中有提到),于是模型和视图都共享一个相同的坐标系。 175 | 具体的代码见清单11.3。注意到我们并没有在任何地方释放`cpSpace`对象。在这个例子中,内存空间将会在整个app的生命周期中一直存在,所以这没有问题。但是在现实世界的场景中,我们需要像创建木箱结构体和形状一样去管理我们的空间,封装在标准的Cocoa对象中,然后来管理Chipmunk对象的生命周期。图11.1展示了掉落的木箱。 176 | 清单11.3 使用物理学来对掉落的木箱建模 177 | ```objective-c 178 | #import "ViewController.h" 179 | #import 180 | #import "chipmunk.h" 181 | @interface Crate : UIImageView 182 | @property (nonatomic, assign) cpBody *body; @property (nonatomic, assign) cpShape *shape; 183 | 184 | @end 185 | @implementation Crate 186 | #define MASS 100 187 | - (id)initWithFrame:(CGRect)frame 188 | { if ((self = [super initWithFrame:frame])) { //set image self.image = [UIImage imageNamed:@"Crate.png"]; 189 | self.contentMode = UIViewContentModeScaleAspectFill; //create the body self.body = cpBodyNew(MASS, cpMomentForBox(MASS, frame.size.width, frame.size.height)); //create the shape cpVect corners[] = { 190 | cpv(0, 0), cpv(0, frame.size.height), 191 | cpv(frame.size.width, frame.size.height), 192 | cpv(frame.size.width, 0), }; self.shape = cpPolyShapeNew(self.body, 4, corners, cpv(-frame.size.width/2, -frame.size.height/2)); //set shape friction & elasticity cpShapeSetFriction(self.shape, 0.5); 193 | cpShapeSetElasticity(self.shape, 0.8); //link the crate to the shape //so we can refer to crate from callback later on 194 | self.shape->data = (__bridge void *)self; //set the body position to match view cpBodySetPos(self.body, cpv(frame.origin.x + frame.size.width/2, 300 - frame.origin.y - frame.size.height/2)); } return self; 195 | } 196 | - (void)dealloc 197 | { //release shape and body cpShapeFree(_shape); cpBodyFree(_body); 198 | } 199 | @end 200 | @interface ViewController () 201 | @property (nonatomic, weak) IBOutlet UIView *containerView; 202 | @property (nonatomic, assign) cpSpace *space; @property (nonatomic, strong) CADisplayLink *timer; 203 | @property (nonatomic, assign) CFTimeInterval lastStep; 204 | @end 205 | @implementation ViewController 206 | 207 | #define GRAVITY 1000 208 | - (void)viewDidLoad 209 | { //invert view coordinate system to match physics self.containerView.layer.geometryFlipped = YES; //set up physics space self.space = cpSpaceNew(); 210 | cpSpaceSetGravity(self.space, cpv(0, -GRAVITY)); //add a crate Crate *crate = [[Crate alloc] initWithFrame:CGRectMake(100, 0, 100, 100)]; 211 | [self.containerView addSubview:crate]; cpSpaceAddBody(self.space, crate.body); cpSpaceAddShape(self.space, crate.shape); 212 | //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; 213 | [self.timer addToRunLoop:[NSRunLoop mainRunLoop] 214 | forMode:NSDefaultRunLoopMode]; 215 | } 216 | void updateShape(cpShape *shape, void *unused) 217 | { //get the crate object associated with the shape Crate *crate = (__bridge Crate *)shape->data; //update crate view position and angle to match physics shape cpBody *body = shape->body; crate.center = cpBodyGetPos(body); crate.transform = CGAffineTransformMakeRotation(cpBodyGetAngle(body)); } 218 | - (void)step:(CADisplayLink *)timer 219 | { //calculate step duration CFTimeInterval thisStep = CACurrentMediaTime(); 220 | CFTimeInterval stepDuration = thisStep - self.lastStep; 221 | self.lastStep = thisStep; //update physics cpSpaceStep(self.space, stepDuration); //update all the shapes cpSpaceEachShape(self.space, &updateShape, NULL); 222 | } 223 | @end ``` 224 | 225 | 图11.1 226 | 图11.1 一个木箱图片,根据模拟的重力掉落 227 | ###添加用户交互 228 | 下一步就是在视图周围添加一道不可见的墙,这样木箱就不会掉落出屏幕之外。或许你会用另一个矩形的`cpPolyShape`来实现,就和之前创建木箱那样,但是我们需要检测的是木箱何时离开视图,而不是何时碰撞,所以我们需要一个空心而不是固体矩形。 229 | 我们可以通过给`cpSpace`添加四个`cpSegmentShape`对象(`cpSegmentShape`代表一条直线,所以四个拼起来就是一个矩形)。然后赋给空间的`staticBody`属性(一个不被重力影响的结构体)而不是像木箱那样一个新的`cpBody`实例,因为我们不想让这个边框矩形滑出屏幕或者被一个下落的木箱击中而消失。 同样可以再添加一些木箱来做一些交互。最后再添加一个加速器,这样可以通过倾斜手机来调整重力矢量(为了测试需要在一台真实的设备上运行程序,因为模拟器不支持加速器事件,即使旋转屏幕)。清单11.4展示了更新后的代码,运行结果见图11.2。 230 | 231 | 由于示例只支持横屏模式,所以交换加速计矢量的x和y值。如果在竖屏下运行程序,请把他们换回来,不然重力方向就错乱了。试一下就知道了,木箱会沿着横向移动。 清单11.4 使用围墙和多个木箱的更新后的代码 232 | ```objetive-c 233 | - (void)addCrateWithFrame:(CGRect)frame 234 | { Crate *crate = [[Crate alloc] initWithFrame:frame]; 235 | [self.containerView addSubview:crate]; 236 | cpSpaceAddBody(self.space, crate.body); 237 | cpSpaceAddShape(self.space, crate.shape); } 238 | - (void)addWallShapeWithStart:(cpVect)start end:(cpVect)end 239 | { cpShape *wall = cpSegmentShapeNew(self.space->staticBody, start, end, 1); 240 | cpShapeSetCollisionType(wall, 2); cpShapeSetFriction(wall, 0.5); cpShapeSetElasticity(wall, 0.8); cpSpaceAddStaticShape(self.space, wall); 241 | } 242 | - (void)viewDidLoad 243 | { //invert view coordinate system to match physics self.containerView.layer.geometryFlipped = YES; //set up physics space self.space = cpSpaceNew(); 244 | cpSpaceSetGravity(self.space, cpv(0, -GRAVITY)); //add wall around edge of view [self addWallShapeWithStart:cpv(0, 0) end:cpv(300, 0)]; 245 | [self addWallShapeWithStart:cpv(300, 0) end:cpv(300, 300)]; 246 | [self addWallShapeWithStart:cpv(300, 300) end:cpv(0, 300)]; 247 | [self addWallShapeWithStart:cpv(0, 300) end:cpv(0, 0)]; //add a crates [self addCrateWithFrame:CGRectMake(0, 0, 32, 32)]; 248 | [self addCrateWithFrame:CGRectMake(32, 0, 32, 32)]; 249 | [self addCrateWithFrame:CGRectMake(64, 0, 64, 64)]; 250 | [self addCrateWithFrame:CGRectMake(128, 0, 32, 32)]; 251 | [self addCrateWithFrame:CGRectMake(0, 32, 64, 64)]; //start the timer self.lastStep = CACurrentMediaTime(); self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)]; 252 | [self.timer addToRunLoop:[NSRunLoop mainRunLoop] 253 | forMode:NSDefaultRunLoopMode]; //update gravity using accelerometer [UIAccelerometer sharedAccelerometer].delegate = self; [UIAccelerometer sharedAccelerometer].updateInterval = 1/60.0; 254 | } 255 | - (void)accelerometer:(UIAccelerometer *)accelerometer didAccelerate:(UIAcceleration *)acceleration { //update gravity cpSpaceSetGravity(self.space, cpv(acceleration.y * GRAVITY, -acceleration.x * GRAVITY)); } ``` 图11.2 256 | 图11.1 真实引力场下的木箱交互 257 | ###模拟时间以及固定的时间步长 258 | 对于实现动画的缓冲效果来说,计算每帧持续的时间是一个很好的解决方案,但是对模拟物理效果并不理想。通过一个可变的时间步长来实现有着两个弊端: 259 | * 如果时间步长不是固定的,精确的值,物理效果的模拟也就随之不确定。这意味着即使是传入相同的输入值,也可能在不同场合下有着不同的效果。有时候没多大影响,但是在基于物理引擎的游戏下,玩家就会由于相同的操作行为导致不同的结果而感到困惑。同样也会让测试变得麻烦。 260 | * 由于性能故常造成的丢帧或者像电话呼入的中断都可能会造成不正确的结果。考虑一个像子弹那样快速移动物体,每一帧的更新都需要移动子弹,检测碰撞。如果两帧之间的时间加长了,子弹就会在这一步移动更远的距离,穿过围墙或者是别的障碍,这样就丢失了碰撞。 261 | 我们想得到的理想的效果就是通过固定的时间步长来计算物理效果,但是在屏幕发生重绘的时候仍然能够同步更新视图(可能会由于在我们控制范围之外造成不可预知的效果)。 262 | 幸运的是,由于我们的模型(在这个例子中就是Chipmunk的`cpSpace`中的`cpBody`)被视图(就是屏幕上代表木箱的`UIView`对象)分离,于是就很简单了。我们只需要根据屏幕刷新的时间跟踪时间步长,然后根据每帧去计算一个或者多个模拟出来的效果。 我们可以通过一个简单的循环来实现。通过每次`CADisplayLink`的启动来通知屏幕将要刷新,然后记录下当前的`CACurrentMediaTime()`。我们需要在一个小增量中提前重复物理模拟(这里用120分之一秒)直到赶上显示的时间。然后更新我们的视图,在屏幕刷新的时候匹配当前物理结构体的显示位置。 263 | 清单11.5展示了固定时间步长版本的代码 264 | 265 | 清单11.5 固定时间步长的木箱模拟 ```objective-c 266 | #define SIMULATION_STEP (1/120.0) 267 | - (void)step:(CADisplayLink *)timer 268 | { //calculate frame step duration CFTimeInterval frameTime = CACurrentMediaTime(); //update simulation while (self.lastStep < frameTime) { cpSpaceStep(self.space, SIMULATION_STEP); self.lastStep += SIMULATION_STEP; 269 | }  270 | //update all the shapes cpSpaceEachShape(self.space, &updateShape, NULL); 271 | } ``` ###避免死亡螺旋 272 | 当使用固定的模拟时间步长时候,有一件事情一定要注意,就是用来计算物理效果的现实世界的时间并不会加速模拟时间步长。在我们的例子中,我们随意选择了120分之一秒来模拟物理效果。Chipmunk很快,我们的例子也很简单,所以`cpSpaceStep()`会完成的很好,不会延迟帧的更新。 273 | 但是如果场景很复杂,比如有上百个物体之间的交互,物理计算就会很复杂,`cpSpaceStep()`的计算也可能会超出1/120秒。我们没有测量出物理步长的时间,因为我们假设了相对于帧刷新来说并不重要,但是如果模拟步长更久的话,就会延迟帧率。 274 | 如果帧刷新的时间延迟的话会变得很糟糕,我们的模拟需要执行更多的次数来同步真实的时间。这些额外的步骤就会继续延迟帧的更新,等等。这就是所谓的死亡螺旋,因为最后的结果就是帧率变得越来越慢,直到最后应用程序卡死了。 我们可以通过添加一些代码在设备上来对物理步骤计算真实世界的时间,然后自动调整固定时间步长,但是实际上它不可行。其实只要保证你给容错留下足够的边长,然后在期望支持的最慢的设备上进行测试就可以了。如果物理计算超过了模拟时间的50%,就需要考虑增加模拟时间步长(或者简化场景)。如果模拟时间步长增加到超过1/60秒(一个完整的屏幕更新时间),你就需要减少动画帧率到一秒30帧或者增加`CADisplayLink`的`frameInterval`来保证不会随机丢帧,不然你的动画将会看起来不平滑。 ##总结 275 | 在这一章中,我们了解了如何通过一个计时器创建一帧帧的实时动画,包括缓冲,物理模拟等等一系列动画技术,以及用户输入(通过加速计)。 276 | 在第三部分中,我们将研究动画性能是如何被被设备限制所影响的,以及如何调整我们的代码来活的足够好的帧率。 277 | -------------------------------------------------------------------------------- /12-性能调优/12.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.1.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.10.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.11.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.12.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.12.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.13.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.2.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.3.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.4.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.5.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.6.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.7.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.8.jpeg -------------------------------------------------------------------------------- /12-性能调优/12.9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/12-性能调优/12.9.jpeg -------------------------------------------------------------------------------- /12-性能调优/性能调优.md: -------------------------------------------------------------------------------- 1 | #性能调优 2 | 3 | >*代码应该运行的尽量快,而不是更快* - 理查德 4 | 5 | 在第一和第二部分,我们了解了Core Animation提供的关于绘制和动画的一些特性。Core Animation功能和性能都非常强大,但如果你对背后的原理不清楚的话也会降低效率。让它达到最优的状态是一门艺术。在这章中,我们将探究一些动画运行慢的原因,以及如何去修复这些问题。 6 | 7 | ##CPU VS GPU 8 | 9 | 关于绘图和动画有两种处理的方式:CPU(中央处理器)和GPU(图形处理器)。在现代iOS设备中,都有可以运行不同软件的可编程芯片,但是由于历史原因,我们可以说CPU所做的工作都在软件层面,而GPU在硬件层面。 10 | 11 | 总的来说,我们可以用软件(使用CPU)做任何事情,但是对于图像处理,通常用硬件会更快,因为GPU使用图像对高度并行浮点运算做了优化。由于某些原因,我们想尽可能把屏幕渲染的工作交给硬件去处理。问题在于GPU并没有无限制处理性能,而且一旦资源用完的话,性能就会开始下降了(即使CPU并没有完全占用) 12 | 13 | 大多数动画性能优化都是关于智能利用GPU和CPU,使得它们都不会超出负荷。于是我们首先需要知道Core Animation是如何在这两个处理器之间分配工作的。 14 | 15 | ###动画的舞台 16 | 17 | Core Animation处在iOS的核心地位:应用内和应用间都会用到它。一个简单的动画可能同步显示多个app的内容,例如当在iPad上多个程序之间使用手势切换,会使得多个程序同时显示在屏幕上。在一个特定的应用中用代码实现它是没有意义的,因为在iOS中不可能实现这种效果(App都是被沙箱管理,不能访问别的视图)。 18 | 19 | 动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的*渲染服务*。在iOS5和之前的版本是*SpringBoard*进程(同时管理着iOS的主屏)。在iOS6之后的版本中叫做`BackBoard`。 20 | 21 | 当运行一段动画时候,这个过程会被四个分离的阶段被打破: 22 | 23 | * **布局** - 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。 24 | 25 | * **显示** - 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的`-drawRect:`和`-drawLayer:inContext:`方法的调用路径。 26 | 27 | * **准备** - 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。 28 | 29 | * **提交** - 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。 30 | 31 | 但是这些仅仅阶段仅仅发生在你的应用程序之内,在动画在屏幕上显示之前仍然有更多的工作。一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做*渲染树*的图层树(在第一章“图层树”中提到过)。使用这个树状结构,渲染服务对动画的每一帧做出如下工作: 32 | 33 | * 对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染 34 | 35 | * 在屏幕上渲染可见的三角形 36 | 37 | 所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。 38 | 39 | 这并不是个问题,因为在布局和显示阶段,你可以决定哪些由CPU执行,哪些交给GPU去做。那么改如何判断呢? 40 | 41 | ###GPU相关的操作 42 | 43 | GPU为一个具体的任务做了优化:它用来采集图片和形状(三角形),运行变换,应用纹理和混合然后把它们输送到屏幕上。现代iOS设备上可编程的GPU在这些操作的执行上又很大的灵活性,但是Core Animation并没有暴露出直接的接口。除非你想绕开Core Animation并编写你自己的OpenGL着色器,从根本上解决硬件加速的问题,那么剩下的所有都还是需要在CPU的软件层面上完成。 44 | 45 | 宽泛的说,大多数`CALayer`的属性都是用GPU来绘制。比如如果你设置图层背景或者边框的颜色,那么这些可以通过着色的三角板实时绘制出来。如果对一个`contents`属性设置一张图片,然后裁剪它 - 它就会被纹理的三角形绘制出来,而不需要软件层面做任何绘制。 46 | 47 | 但是有一些事情会降低(基于GPU)图层绘制,比如: 48 | 49 | * 太多的几何结构 - 这发生在需要太多的三角板来做变换,以应对处理器的栅格化的时候。现代iOS设备的图形芯片可以处理几百万个三角板,所以在Core Animation中几何结构并不是GPU的瓶颈所在。但由于图层在显示之前通过IPC发送到渲染服务器的时候(图层实际上是由很多小物体组成的特别重量级的对象),太多的图层就会引起CPU的瓶颈。这就限制了一次展示的图层个数(见本章后续“CPU相关操作”)。 50 | 51 | * 重绘 - 主要由重叠的半透明图层引起。GPU的*填充比率*(用颜色填充像素的比率)是有限的,所以需要避免*重绘*(每一帧用相同的像素填充多次)的发生。在现代iOS设备上,GPU都会应对重绘;即使是iPhone 3GS都可以处理高达2.5的重绘比率,并任然保持60帧率的渲染(这意味着你可以绘制一个半的整屏的冗余信息,而不影响性能),并且新设备可以处理更多。 52 | 53 | * 离屏绘制 - 这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或者是图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。但这不意味着你需要避免使用这些效果,只是要明白这会带来性能的负面影响。 54 | 55 | * 过大的图片 - 如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。 56 | 57 | ###CPU相关的操作 58 | 59 | 大多数工作在Core Animation的CPU都发生在动画开始之前。这意味着它不会影响到帧率,所以很好,但是他会延迟动画开始的时间,让你的界面看起来会比较迟钝。 60 | 61 | 以下CPU的操作都会延迟动画的开始时间: 62 | 63 | * 布局计算 - 如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应该是比老版的自动调整逻辑加强了CPU的工作。 64 | 65 | * 视图惰性加载 - iOS只会当视图控制器的视图显示到屏幕上时才会加载它。这对内存使用和程序启动时间很有好处,但是当呈现到屏幕上之前,按下按钮导致的许多工作都会不能被及时响应。比如控制器从数据库中获取数据,或者视图从一个nib文件中加载,或者涉及IO的图片显示(见后续“IO相关操作”),都会比CPU正常操作慢得多。 66 | 67 | * Core Graphics绘制 - 如果对视图实现了`-drawRect:`方法,或者`CALayerDelegate`的`-drawLayer:inContext:`方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。 68 | 69 | * 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片(14章“图片IO”会更详细讨论)。根据你加载图片的方式,第一次对图层内容赋值的时候(直接或者间接使用`UIImageView`)或者把它绘制到Core Graphics中,都需要对它解压,这样的话,对于一个较大的图片,都会占用一定的时间。 70 | 71 | 当图层被成功打包,发送到渲染服务器之后,CPU仍然要做如下工作:为了显示屏幕上的图层,Core Animation必须对渲染树种的每个可见图层通过OpenGL循环转换成纹理三角板。由于GPU并不知晓Core Animation图层的任何结构,所以必须要由CPU做这些事情。这里CPU涉及的工作和图层个数成正比,所以如果在你的层级关系中有太多的图层,就会导致CPU每一帧的渲染,即使这些事情不是你的应用程序可控的。 72 | 73 | ###IO相关操作 74 | 75 | 还有一项没涉及的就是IO相关工作。上下文中的IO(输入/输出)指的是例如闪存或者网络接口的硬件访问。一些动画可能需要从山村(甚至是远程URL)来加载。一个典型的例子就是两个视图控制器之间的过渡效果,这就需要从一个nib文件或者是它的内容中懒加载,或者一个旋转的图片,可能在内存中尺寸太大,需要动态滚动来加载。 76 | 77 | IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。总的来说,这就需要使用聪敏但尴尬的技术,也就是多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。这些技术将会在第14章中讨论。 78 | 79 | ##测量,而不是猜测 80 | 81 | 于是现在你知道有哪些点可能会影响动画性能,那该如何修复呢?好吧,其实不需要。有很多种诡计来优化动画,但如果盲目使用的话,可能会造成更多性能上的问题,而不是修复。 82 | 83 | 如何正确的测量而不是猜测这点很重要。根据性能相关的知识写出代码不同于仓促的优化。前者很好,后者实际上就是在浪费时间。 84 | 85 | 那该如何测量呢?第一步就是确保在真实环境下测试你的程序。 86 | 87 | ###真机测试,而不是模拟器 88 | 89 | 当你开始做一些性能方面的工作时,一定要在真机上测试,而不是模拟器。模拟器虽然是加快开发效率的一把利器,但它不能提供准确的真机性能参数。 90 | 91 | 模拟器运行在你的Mac上,然而Mac上的CPU往往比iOS设备要快。相反,Mac上的GPU和iOS设备的完全不一样,模拟器不得已要在软件层面(CPU)模拟设备的GPU,这意味着GPU相关的操作在模拟器上运行的更慢,尤其是使用`CAEAGLLayer`来写一些OpenGL的代码时候。 92 | 93 | 这就是说在模拟器上的测试出的性能会高度失真。如果动画在模拟器上运行流畅,可能在真机上十分糟糕。如果在模拟器上运行的很卡,也可能在真机上很平滑。你无法确定。 94 | 95 | 另一件重要的事情就是性能测试一定要用*发布*配置,而不是调试模式。因为当用发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码。你也可以自己做到这些,例如在发布环境禁用NSLog语句。你只关心发布性能,那才是你需要测试的点。 96 | 97 | 最后,最好在你支持的设备中性能最差的设备上测试:如果基于iOS6开发,这意味着最好在iPhone 3GS或者iPad2上测试。如果可能的话,测试不同的设备和iOS版本,因为苹果在不同的iOS版本和设备中做了一些改变,这也可能影响到一些性能。例如iPad3明显要在动画渲染上比iPad2慢很多,因为渲染4倍多的像素点(为了支持视网膜显示)。 98 | 99 | ###保持一致的帧率 100 | 101 | 为了做到动画的平滑,你需要以60FPS(帧每秒)的速度运行,以同步屏幕刷新速率。通过基于`NSTimer`或者`CADisplayLink`的动画你可以降低到30FPS,而且效果还不错,但是没办法通过Core Animation做到这点。如果不保持60FPS的速率,就可能随机丢帧,影响到体验。 102 | 103 | 你可以在使用的过程中明显感到有没有丢帧,但没办法通过肉眼来得到具体的数据,也没法知道你的做法有没有真的提高性能。你需要的是一系列精确的数据。 104 | 105 | 你可以在程序中用`CADisplayLink`来测量帧率(就像11章“基于定时器的动画”中那样),然后在屏幕上显示出来,但应用内的FPS显示并不能够完全真实测量出Core Animation性能,因为它仅仅测出应用内的帧率。我们知道很多动画都在应用之外发生(在渲染服务器进程中处理),但同时应用内FPS计数的确可以对某些性能问题提供参考,一旦找出一个问题的地方,你就需要得到更多精确详细的数据来定位到问题所在。苹果提供了一个强大的*Instruments*工具集来帮我们做到这些。 106 | 107 | ##Instruments 108 | 109 | Instruments是Xcode套件中没有被充分利用的一个工具。很多iOS开发者从没用过Instruments,或者只是用Leaks工具检测循环引用。实际上有很多Instruments工具,包括为动画性能调优的东西。 110 | 111 | 你可以通过在菜单中选择Profile选项来打开Instruments(在这之前,记住要把目标设置成iOS设备,而不是模拟器)。然后将会显示出图12.1(如果没有看到所有选项,你可能设置成了模拟器选项)。 112 | 113 | 图12.1 114 | 115 | 图12.1 Instruments工具选项窗口 116 | 117 | 就像之前提到的那样,你应该始终将程序设置成发布选项。幸运的是,配置文件默认就是发布选项,所以你不需要在分析的时候调整编译策略。 118 | 119 | 我们将讨论如下几个工具: 120 | 121 | * **时间分析器** - 用来测量被方法/函数打断的CPU使用情况。 122 | 123 | * **Core Animation** - 用来调试各种Core Animation性能问题。 124 | 125 | * **OpenGL ES驱动** - 用来调试GPU性能问题。这个工具在编写Open GL代码的时候很有用,但有时也用来处理Core Animation的工作。 126 | 127 | Instruments的一个很棒的功能在于它可以创建我们自定义的工具集。除了你初始选择的工具之外,如果在Instruments中打开Library窗口,你可以拖拽别的工具到左侧边栏。我们将创建以上我们提到的三个工具,然后就可以并行使用了(见图12.2)。 128 | 129 | 图12.2 130 | 131 | 图12.2 添加额外的工具到Instruments侧边栏 132 | 133 | ###时间分析器 134 | 135 | 时间分析器工具用来检测CPU的使用情况。它可以告诉我们程序中的哪个方法正在消耗大量的CPU时间。使用大量的CPU并*不一定*是个问题 - 你可能期望动画路径对CPU非常依赖,因为动画往往是iOS设备中最苛刻的任务。 136 | 137 | 但是如果你有性能问题,查看CPU时间对于判断性能是不是和CPU相关,以及定位到函数都很有帮助(见图12.3)。 138 | 139 | 图12.3 140 | 141 | 图12.3 时间分析器工具 142 | 143 | 时间分析器有一些选项来帮助我们定位到我们关心的的方法。可以使用左侧的复选框来打开。其中最有用的是如下几点: 144 | 145 | * 通过线程分离 - 这可以通过执行的线程进行分组。如果代码被多线程分离的话,那么就可以判断到底是哪个线程造成了问题。 146 | 147 | * 隐藏系统库 - 可以隐藏所有苹果的框架代码,来帮助我们寻找哪一段代码造成了性能瓶颈。由于我们不能优化框架方法,所以这对定位到我们能实际修复的代码很有用。 148 | 149 | * 只显示Obj-C代码 - 隐藏除了Objective-C之外的所有代码。大多数内部的Core Animation代码都是用C或者C++函数,所以这对我们集中精力到我们代码中显式调用的方法就很有用。 150 | 151 | ###Core Animation 152 | 153 | Core Animation工具用来监测Core Animation性能。它给我们提供了周期性的FPS,并且考虑到了发生在程序之外的动画(见图12.4)。 154 | 155 | 图12.4 156 | 157 | 图12.4 使用可视化调试选项的Core Animation工具 158 | 159 | Core Animation工具也提供了一系列复选框选项来帮助调试渲染瓶颈: 160 | 161 | * **Color Blended Layers** - 这个选项基于渲染程度对屏幕中的混合区域进行绿到红的高亮(也就是多个半透明图层的叠加)。由于重绘的原因,混合对GPU性能会有影响,同时也是滑动或者动画帧率下降的罪魁祸首之一。 162 | 163 | * **ColorHitsGreenandMissesRed** - 当使用`shouldRasterizep`属性的时候,耗时的图层绘制会被缓存,然后当做一个简单的扁平图片呈现。当缓存再生的时候这个选项就用红色对栅格化图层进行了高亮。如果缓存频繁再生的话,就意味着栅格化可能会有负面的性能影响了(更多关于使用`shouldRasterize`的细节见第15章“图层性能”)。 164 | 165 | * **Color Copied Images** - 有时候寄宿图片的生成意味着Core Animation被强制生成一些图片,然后发送到渲染服务器,而不是简单的指向原始指针。这个选项把这些图片渲染成蓝色。复制图片对内存和CPU使用来说都是一项非常昂贵的操作,所以应该尽可能的避免。 166 | 167 | * **Color Immediately** - 通常Core Animation Instruments以每毫秒10次的频率更新图层调试颜色。对某些效果来说,这显然太慢了。这个选项就可以用来设置每帧都更新(可能会影响到渲染性能,而且会导致帧率测量不准,所以不要一直都设置它)。 168 | 169 | * **Color Misaligned Images** - 这里会高亮那些被缩放或者拉伸以及没有正确对齐到像素边界的图片(也就是非整型坐标)。这些中的大多数通常都会导致图片的不正常缩放,如果把一张大图当缩略图显示,或者不正确地模糊图像,那么这个选项将会帮你识别出问题所在。 170 | 171 | * **Color Offscreen-Rendered Yellow** - 这里会把那些需要离屏渲染的图层高亮成黄色。这些图层很可能需要用`shadowPath`或者`shouldRasterize`来优化。 172 | 173 | * **Color OpenGL Fast Path Blue** - 这个选项会对任何直接使用OpenGL绘制的图层进行高亮。如果仅仅使用UIKit或者Core Animation的API,那么不会有任何效果。如果使用`GLKView`或者`CAEAGLLayer`,那如果不显示蓝色块的话就意味着你正在强制CPU渲染额外的纹理,而不是绘制到屏幕。 174 | 175 | * **Flash Updated Regions** - 这个选项会对重绘的内容高亮成黄色(也就是任何在软件层面使用Core Graphics绘制的图层)。这种绘图的速度很慢。如果频繁发生这种情况的话,这意味着有一个隐藏的bug或者说通过增加缓存或者使用替代方案会有提升性能的空间。 176 | 177 | 这些高亮图层的选项同样在iOS模拟器的调试菜单也可用(图12.5)。我们之前说过用模拟器测试性能并不好,但如果你能通过这些高亮选项识别出性能问题出在什么地方的话,那么使用iOS模拟器来验证问题是否解决也是比真机测试更有效的。 178 | 179 | 图12.5 180 | 181 | 图12.5 iOS模拟器中Core Animation可视化调试选项 182 | 183 | ###OpenGL ES驱动 184 | 185 | OpenGL ES驱动工具可以帮你测量GPU的利用率,同样也是一个很好的来判断和GPU相关动画性能的指示器。它同样也提供了类似Core Animation那样显示FPS的工具(图12.6)。 186 | 187 | 图12.6 188 | 189 | 图12.6 OpenGL ES驱动工具 190 | 191 | 侧栏的邮编是一系列有用的工具。其中和Core Animation性能最相关的是如下几点: 192 | 193 | * **Renderer Utilization** - 如果这个值超过了~50%,就意味着你的动画可能对帧率有所限制,很可能因为离屏渲染或者是重绘导致的过度混合。 194 | 195 | * **Tiler Utilization** - 如果这个值超过了~50%,就意味着你的动画可能限制于几何结构方面,也就是在屏幕上有太多的图层占用了。 196 | 197 | 198 | ##一个可用的案例 199 | 200 | 现在我们已经对Instruments中动画性能工具非常熟悉了,那么可以用它在现实中解决一些实际问题。 201 | 202 | 我们创建一个简单的显示模拟联系人姓名和头像列表的应用。注意即使把头像图片存在应用本地,为了使应用看起来更真实,我们分别实时加载图片,而不是用`–imageNamed:`预加载。同样添加一些图层阴影来使得列表显示得更真实。清单12.1展示了最初版本的实现。 203 | 204 | 清单12.1 使用假数据的一个简单联系人列表 205 | 206 | ```objective-c 207 | #import "ViewController.h" 208 | #import 209 | 210 | @interface ViewController () 211 | 212 | @property (nonatomic, strong) NSArray *items; 213 | @property (nonatomic, weak) IBOutlet UITableView *tableView; 214 | 215 | @end 216 | 217 | @implementation ViewController 218 | 219 | - (NSString *)randomName 220 | { 221 | NSArray *first = @[@"Alice", @"Bob", @"Bill", @"Charles", @"Dan", @"Dave", @"Ethan", @"Frank"]; 222 | NSArray *last = @[@"Appleseed", @"Bandicoot", @"Caravan", @"Dabble", @"Ernest", @"Fortune"]; 223 | NSUInteger index1 = (rand()/(double)INT_MAX) * [first count]; 224 | NSUInteger index2 = (rand()/(double)INT_MAX) * [last count]; 225 | return [NSString stringWithFormat:@"%@ %@", first[index1], last[index2]]; 226 | } 227 | 228 | - (NSString *)randomAvatar 229 | { 230 | NSArray *images = @[@"Snowman", @"Igloo", @"Cone", @"Spaceship", @"Anchor", @"Key"]; 231 | NSUInteger index = (rand()/(double)INT_MAX) * [images count]; 232 | return images[index]; 233 | } 234 | 235 | - (void)viewDidLoad 236 | { 237 | [super viewDidLoad]; 238 | //set up data 239 | NSMutableArray *array = [NSMutableArray array]; 240 | for (int i = 0; i < 1000; i++) { 241 | //add name 242 | [array addObject:@{@"name": [self randomName], @"image": [self randomAvatar]}]; 243 | } 244 | self.items = array; 245 | //register cell class 246 | [self.tableView registerClass:[UITableViewCell class] forCellReuseIdentifier:@"Cell"]; 247 | } 248 | 249 | - (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section 250 | { 251 | return [self.items count]; 252 | } 253 | 254 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 255 | { 256 | //dequeue cell 257 | UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" forIndexPath:indexPath]; 258 | //load image 259 | NSDictionary *item = self.items[indexPath.row]; 260 | NSString *filePath = [[NSBundle mainBundle] pathForResource:item[@"image"] ofType:@"png"]; 261 | //set image and text 262 | cell.imageView.image = [UIImage imageWithContentsOfFile:filePath]; 263 | cell.textLabel.text = item[@"name"]; 264 | //set image shadow 265 | cell.imageView.layer.shadowOffset = CGSizeMake(0, 5); 266 | cell.imageView.layer.shadowOpacity = 0.75; 267 | cell.clipsToBounds = YES; 268 | //set text shadow 269 | cell.textLabel.backgroundColor = [UIColor clearColor]; 270 | cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2); 271 | cell.textLabel.layer.shadowOpacity = 0.5; 272 | return cell; 273 | } 274 | 275 | @end 276 | ``` 277 | 278 | 当快速滑动的时候就会非常卡(见图12.7的FPS计数器)。 279 | 280 | 图12.7 281 | 282 | 图12.7 滑动帧率降到15FPS 283 | 284 | 仅凭直觉,我们猜测性能瓶颈应该在图片加载。我们实时从闪存加载图片,而且没有缓存,所以很可能是这个原因。我们可以用一些很赞的代码修复,然后使用GCD异步加载图片,然后缓存。。。等一下,在开始编码之前,测试一下假设是否成立。首先用我们的三个Instruments工具分析一下程序来定位问题。我们推测问题可能和图片加载相关,所以用Time Profiler工具来试试(图12.8)。 285 | 286 | 图12.8 287 | 288 | 图12.8 用The timing profile分析联系人列表 289 | 290 | `-tableView:cellForRowAtIndexPath:`中的CPU时间总利用率只有~28%(也就是加载头像图片的地方),非常低。于是建议是CPU/IO并不是真正的限制因素。然后看看是不是GPU的问题:在OpenGL ES Driver工具中检测GPU利用率(图12.9)。 291 | 292 | 图12.9 293 | 294 | 图12.9 OpenGL ES Driver工具显示的GPU利用率 295 | 296 | 渲染服务利用率的值达到51%和63%。看起来GPU需要做很多工作来渲染联系人列表。 297 | 298 | 为什么GPU利用率这么高呢?我们来用Core Animation调试工具选项来检查屏幕。首先打开Color Blended Layers(图12.10)。 299 | 300 | 图12.10 301 | 302 | 图12.10 使用Color Blended Layers选项调试程序 303 | 304 | 屏幕中所有红色的部分都意味着字符标签视图的高级别混合,这很正常,因为我们把背景设置成了透明色来显示阴影效果。这就解释了为什么渲染利用率这么高了。 305 | 306 | 那么离屏绘制呢?打开Core Animation工具的Color Offscreen - Rendered Yellow选项(图12.11)。 307 | 308 | 图12.11 309 | 310 | 图12.11 Color Offscreen–Rendered Yellow选项 311 | 312 | 所有的表格单元内容都在离屏绘制。这一定是因为我们给图片和标签视图添加的阴影效果。在代码中禁用阴影,然后看下性能是否有提高(图12.12)。 313 | 314 | 图12.12 315 | 316 | 图12.12 禁用阴影之后运行程序接近60FPS 317 | 318 | 问题解决了。干掉阴影之后,滑动很流畅。但是我们的联系人列表看起来没有之前好了。那如何保持阴影效果而且不会影响性能呢? 319 | 320 | 好吧,每一行的字符和头像在每一帧刷新的时候并不需要变,所以看起来`UITableViewCell`的图层非常适合做缓存。我们可以使用`shouldRasterize`来缓存图层内容。这将会让图层离屏之后渲染一次然后把结果保存起来,直到下次利用的时候去更新(见清单12.2)。 321 | 322 | 清单12.2 使用`shouldRasterize`提高性能 323 | 324 | ```objective-c 325 | - (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath 326 | { 327 | //dequeue cell 328 | UITableViewCell *cell = [self.tableView dequeueReusableCellWithIdentifier:@"Cell" 329 | forIndexPath:indexPath]; 330 | ... 331 | //set text shadow 332 | cell.textLabel.backgroundColor = [UIColor clearColor]; 333 | cell.textLabel.layer.shadowOffset = CGSizeMake(0, 2); 334 | cell.textLabel.layer.shadowOpacity = 0.5; 335 | //rasterize 336 | cell.layer.shouldRasterize = YES; 337 | cell.layer.rasterizationScale = [UIScreen mainScreen].scale; 338 | return cell; 339 | } 340 | ``` 341 | 342 | 我们仍然离屏绘制图层内容,但是由于显式地禁用了栅格化,Core Animation就对绘图缓存了结果,于是对提高了性能。我们可以验证缓存是否有效,在Core Animation工具中点击Color Hits Green and Misses Red选项(图12.13)。 343 | 344 | 图12.13 345 | 346 | 图12.13 Color Hits Green and Misses Red验证了缓存有效 347 | 348 | 结果和预期一致 - 大部分都是绿色,只有当滑动到屏幕上的时候会闪烁成红色。因此,现在帧率更加平滑了。 349 | 350 | 所以我们最初的设想是错的。图片的加载并不是真正的瓶颈所在,而且试图把它置于一个复杂的多线程加载和缓存的实现都将是徒劳。所以在动手修复之前验证问题所在是个很好的习惯! 351 | 352 | ##总结 353 | 354 | 在这章中,我们学习了Core Animation是如何渲染,以及我们可能出现的瓶颈所在。你同样学习了如何使用Instruments来检测和修复性能问题。 355 | 356 | 在下三章中,我们将对每个普通程序的性能陷阱进行详细讨论,然后学习如何修复。 357 | -------------------------------------------------------------------------------- /13-高效绘图/13-高效绘图.md: -------------------------------------------------------------------------------- 1 | #高效绘图 2 | 3 | > 不必要的效率考虑往往是性能问题的万恶之源。 4 | > ——William Allan Wulf 5 | 6 | 在第12章『速度的曲率』我们学习如何用Instruments来诊断Core Animation性能问题。在构建一个iOS app的时候会遇到很多潜在的性能陷阱,但是在本章我们将着眼于有关*绘制*的性能问题。 7 | 8 | ##软件绘图 9 | 10 | 术语*绘图*通常在Core Animation的上下文中指代软件绘图(意即:不由GPU协助的绘图)。在iOS中,软件绘图通常是由Core Graphics框架完成来完成。但是,在一些必要的情况下,相比Core Animation和OpenGL,Core Graphics要慢了不少。 11 | 12 | 软件绘图不仅效率低,还会消耗可观的内存。`CALayer`只需要一些与自己相关的内存:只有它的寄宿图会消耗一定的内存空间。即使直接赋给`contents`属性一张图片,也不需要增加额外的照片存储大小。如果相同的一张图片被多个图层作为`contents`属性,那么他们将会共用同一块内存,而不是复制内存块。 13 | 14 | 但是一旦你实现了`CALayerDelegate`协议中的`-drawLayer:inContext:`方法或者`UIView`中的`-drawRect:`方法(其实就是前者的包装方法),图层就创建了一个绘制上下文,这个上下文需要的大小的内存可从这个算式得出:图层宽\*图层高\*4字节,宽高的单位均为像素。对于一个在Retina iPad上的全屏图层来说,这个内存量就是 2048\*1526\*4字节,相当于12MB内存,图层每次重绘的时候都需要重新抹掉内存然后重新分配。 15 | 16 | 软件绘图的代价昂贵,除非绝对必要,你应该避免重绘你的视图。提高绘制性能的秘诀就在于尽量避免去绘制。 17 | 18 | ##矢量图形 19 | 20 | 我们用Core Graphics来绘图的一个通常原因就是只是用图片或是图层效果不能轻易地绘制出矢量图形。矢量绘图包含一下这些: 21 | 22 | * 任意多边形(不仅仅是一个矩形) 23 | * 斜线或曲线 24 | * 文本 25 | * 渐变 26 | 27 | 举个例子,清单13.1 展示了一个基本的画线应用。这个应用将用户的触摸手势转换成一个`UIBezierPath`上的点,然后绘制成视图。我们在一个`UIView`子类`DrawingView`中实现了所有的绘制逻辑,这个情况下我们没有用上view controller。但是如果你喜欢你可以在view controller中实现触摸事件处理。图13.1是代码运行结果。 28 | 29 | 清单13.1 用Core Graphics实现一个简单的绘图应用 30 | 31 | ```objective-c 32 | #import "DrawingView.h" 33 | 34 | @interface DrawingView () 35 | 36 | @property (nonatomic, strong) UIBezierPath *path; 37 | 38 | @end 39 | 40 | @implementation DrawingView 41 | 42 | - (void)awakeFromNib 43 | { 44 | //create a mutable path 45 | self.path = [[UIBezierPath alloc] init]; 46 | self.path.lineJoinStyle = kCGLineJoinRound; 47 | self.path.lineCapStyle = kCGLineCapRound; 48 |  49 | self.path.lineWidth = 5; 50 | } 51 | 52 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 53 | { 54 | //get the starting point 55 | CGPoint point = [[touches anyObject] locationInView:self]; 56 | 57 | //move the path drawing cursor to the starting point 58 | [self.path moveToPoint:point]; 59 | } 60 | 61 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 62 | { 63 | //get the current point 64 | CGPoint point = [[touches anyObject] locationInView:self]; 65 | 66 | //add a new line segment to our path 67 | [self.path addLineToPoint:point]; 68 | 69 | //redraw the view 70 | [self setNeedsDisplay]; 71 | } 72 | 73 | - (void)drawRect:(CGRect)rect 74 | { 75 | //draw path 76 | [[UIColor clearColor] setFill]; 77 | [[UIColor redColor] setStroke]; 78 | [self.path stroke]; 79 | } 80 | @end 81 | ``` 82 | 83 | ![图13.1](./13.1.png) 84 | 85 | 图13.1 用Core Graphics做一个简单的『素描』 86 | 87 | 这样实现的问题在于,我们画得越多,程序就会越慢。因为每次移动手指的时候都会重绘整个贝塞尔路径(`UIBezierPath`),随着路径越来越复杂,每次重绘的工作就会增加,直接导致了帧数的下降。看来我们需要一个更好的方法了。 88 | 89 | Core Animation为这些图形类型的绘制提供了专门的类,并给他们提供硬件支持(第六章『专有图层』有详细提到)。`CAShapeLayer`可以绘制多边形,直线和曲线。`CATextLayer`可以绘制文本。`CAGradientLayer`用来绘制渐变。这些总体上都比Core Graphics更快,同时他们也避免了创造一个寄宿图。 90 | 91 | 如果稍微将之前的代码变动一下,用`CAShapeLayer`替代Core Graphics,性能就会得到提高(见清单13.2).虽然随着路径复杂性的增加,绘制性能依然会下降,但是只有当非常非常浮躁的绘制时才会感到明显的帧率差异。 92 | 93 | 清单13.2 用`CAShapeLayer`重新实现绘图应用 94 | 95 | ```objective-c 96 | #import "DrawingView.h" 97 | #import 98 | 99 | @interface DrawingView () 100 | 101 | @property (nonatomic, strong) UIBezierPath *path; 102 | 103 | @end 104 |  105 | @implementation DrawingView 106 | 107 | + (Class)layerClass 108 | { 109 | //this makes our view create a CAShapeLayer 110 | //instead of a CALayer for its backing layer 111 | return [CAShapeLayer class]; 112 | } 113 | 114 | - (void)awakeFromNib 115 | { 116 | //create a mutable path 117 | self.path = [[UIBezierPath alloc] init]; 118 | 119 | //configure the layer 120 | CAShapeLayer *shapeLayer = (CAShapeLayer *)self.layer; 121 | shapeLayer.strokeColor = [UIColor redColor].CGColor; 122 | shapeLayer.fillColor = [UIColor clearColor].CGColor; 123 | shapeLayer.lineJoin = kCALineJoinRound; 124 | shapeLayer.lineCap = kCALineCapRound; 125 | shapeLayer.lineWidth = 5; 126 | } 127 | 128 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 129 | { 130 | //get the starting point 131 | CGPoint point = [[touches anyObject] locationInView:self]; 132 | 133 | //move the path drawing cursor to the starting point 134 | [self.path moveToPoint:point]; 135 | } 136 | 137 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 138 | { 139 | //get the current point 140 | CGPoint point = [[touches anyObject] locationInView:self]; 141 | 142 | //add a new line segment to our path 143 | [self.path addLineToPoint:point]; 144 | 145 | //update the layer with a copy of the path 146 | ((CAShapeLayer *)self.layer).path = self.path.CGPath; 147 | } 148 | @end 149 | ``` 150 | 151 | ##脏矩形 152 | 153 | 有时候用`CAShapeLayer`或者其他矢量图形图层替代Core Graphics并不是那么切实可行。比如我们的绘图应用:我们用线条完美地完成了矢量绘制。但是设想一下如果我们能进一步提高应用的性能,让它就像一个黑板一样工作,然后用『粉笔』来绘制线条。模拟粉笔最简单的方法就是用一个『线刷』图片然后将它粘贴到用户手指碰触的地方,但是这个方法用`CAShapeLayer`没办法实现。 154 | 155 | 我们可以给每个『线刷』创建一个独立的图层,但是实现起来有很大的问题。屏幕上允许同时出现图层上线数量大约是几百,那样我们很快就会超出的。这种情况下我们没什么办法,就用Core Graphics吧(除非你想用OpenGL做一些更复杂的事情)。 156 | 157 | 我们的『黑板』应用的最初实现见清单13.3,我们更改了之前版本的`DrawingView`,用一个画刷位置的数组代替`UIBezierPath`。图13.2是运行结果 158 | 159 | 清单13.3 简单的类似黑板的应用 160 | 161 | ```objective-c 162 | #import "DrawingView.h" 163 | #import 164 | #define BRUSH_SIZE 32 165 | 166 | @interface DrawingView () 167 | 168 | @property (nonatomic, strong) NSMutableArray *strokes; 169 | 170 | @end 171 | 172 | @implementation DrawingView 173 | 174 | - (void)awakeFromNib 175 | { 176 | //create array 177 | self.strokes = [NSMutableArray array]; 178 | } 179 | 180 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 181 | { 182 | //get the starting point 183 | CGPoint point = [[touches anyObject] locationInView:self]; 184 | 185 | //add brush stroke 186 | [self addBrushStrokeAtPoint:point]; 187 | } 188 | 189 | - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event 190 | { 191 | //get the touch point 192 | CGPoint point = [[touches anyObject] locationInView:self]; 193 | 194 | //add brush stroke 195 | [self addBrushStrokeAtPoint:point]; 196 | } 197 | 198 | - (void)addBrushStrokeAtPoint:(CGPoint)point 199 | { 200 | //add brush stroke to array 201 | [self.strokes addObject:[NSValue valueWithCGPoint:point]]; 202 | 203 | //needs redraw 204 | [self setNeedsDisplay]; 205 | } 206 | 207 | - (void)drawRect:(CGRect)rect 208 | { 209 | //redraw strokes 210 | for (NSValue *value in self.strokes) { 211 | //get point 212 | CGPoint point = [value CGPointValue]; 213 | 214 | //get brush rect 215 | CGRect brushRect = CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); 216 | 217 | //draw brush stroke  218 | [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect]; 219 | } 220 | } 221 | @end 222 | ``` 223 | 224 | ![图13.2](./13.2.png) 225 | 226 | 图13.2 用程序绘制一个简单的『素描』 227 | 228 | 这个实现在模拟器上表现还不错,但是在真实设备上就没那么好了。问题在于每次手指移动的时候我们就会重绘之前的线刷,即使场景的大部分并没有改变。我们绘制地越多,就会越慢。随着时间的增加每次重绘需要更多的时间,帧数也会下降(见图13.3),如何提高性能呢? 229 | 230 | ![图13.3](./13.3.png) 231 | 232 | 图13.3 帧率和线条质量会随时间下降。 233 | 234 | 为了减少不必要的绘制,Mac OS和iOS设备将会把屏幕区分为需要重绘的区域和不需要重绘的区域。那些需要重绘的部分被称作『脏区域』。在实际应用中,鉴于非矩形区域边界裁剪和混合的复杂性,通常会区分出包含指定视图的矩形位置,而这个位置就是『脏矩形』。 235 | 236 | 当一个视图被改动过了,TA可能需要重绘。但是很多情况下,只是这个视图的一部分被改变了,所以重绘整个寄宿图就太浪费了。但是Core Animation通常并不了解你的自定义绘图代码,它也不能自己计算出脏区域的位置。然而,你的确可以提供这些信息。 237 | 238 | 当你检测到指定视图或图层的指定部分需要被重绘,你直接调用`-setNeedsDisplayInRect:`来标记它,然后将影响到的矩形作为参数传入。这样就会在一次视图刷新时调用视图的`-drawRect:`(或图层代理的`-drawLayer:inContext:`方法)。 239 | 240 | 传入`-drawLayer:inContext:`的`CGContext`参数会自动被裁切以适应对应的矩形。为了确定矩形的尺寸大小,你可以用`CGContextGetClipBoundingBox()`方法来从上下文获得大小。调用`-drawRect()`会更简单,因为`CGRect`会作为参数直接传入。 241 | 242 | 你应该将你的绘制工作限制在这个矩形中。任何在此区域之外的绘制都将被自动无视,但是这样CPU花在计算和抛弃上的时间就浪费了,实在是太不值得了。 243 | 244 | 相比依赖于Core Graphics为你重绘,裁剪出自己的绘制区域可能会让你避免不必要的操作。那就是说,如果你的裁剪逻辑相当复杂,那还是让Core Graphics来代劳吧,记住:当你能高效完成的时候才这样做。 245 | 246 | 清单13.4 展示了一个`-addBrushStrokeAtPoint:`方法的升级版,它只重绘当前线刷的附近区域。另外也会刷新之前线刷的附近区域,我们也可以用`CGRectIntersectsRect()`来避免重绘任何旧的线刷以不至于覆盖已更新过的区域。这样做会显著地提高绘制效率(见图13.4) 247 | 248 | 清单13.4 用`-setNeedsDisplayInRect:`来减少不必要的绘制 249 | ```objective-c 250 | - (void)addBrushStrokeAtPoint:(CGPoint)point 251 | { 252 | //add brush stroke to array 253 | [self.strokes addObject:[NSValue valueWithCGPoint:point]]; 254 | 255 | //set dirty rect 256 | [self setNeedsDisplayInRect:[self brushRectForPoint:point]]; 257 | } 258 | 259 | - (CGRect)brushRectForPoint:(CGPoint)point 260 | { 261 | return CGRectMake(point.x - BRUSH_SIZE/2, point.y - BRUSH_SIZE/2, BRUSH_SIZE, BRUSH_SIZE); 262 | } 263 | 264 | - (void)drawRect:(CGRect)rect 265 | { 266 | //redraw strokes 267 | for (NSValue *value in self.strokes) { 268 | //get point 269 | CGPoint point = [value CGPointValue]; 270 | 271 | //get brush rect 272 | CGRect brushRect = [self brushRectForPoint:point]; 273 |  274 | //only draw brush stroke if it intersects dirty rect 275 | if (CGRectIntersectsRect(rect, brushRect)) { 276 | //draw brush stroke 277 | [[UIImage imageNamed:@"Chalk.png"] drawInRect:brushRect]; 278 | } 279 | } 280 | } 281 | ``` 282 | 283 | ![图13.4](./13.4.png) 284 | 285 | 图13.4 更好的帧率和顺滑线条 286 | 287 | ##异步绘制 288 | 289 | UIKit的单线程天性意味着寄宿图通常要在主线程上更新,这意味着绘制会打断用户交互,甚至让整个app看起来处于无响应状态。我们对此无能为力,但是如果能避免用户等待绘制完成就好多了。 290 | 291 | 针对这个问题,有一些方法可以用到:一些情况下,我们可以推测性地提前在另外一个线程上绘制内容,然后将由此绘出的图片直接设置为图层的内容。这实现起来可能不是很方便,但是在特定情况下是可行的。Core Animation提供了一些选择:`CATiledLayer`和`drawsAsynchronously`属性。 292 | 293 | ###CATiledLayer 294 | 295 | 我们在第六章简单探索了一下`CATiledLayer`。除了将图层再次分割成独立更新的小块(类似于脏矩形自动更新的概念),`CATiledLayer`还有一个有趣的特性:在多个线程中为每个小块同时调用`-drawLayer:inContext:`方法。这就避免了阻塞用户交互而且能够利用多核心新片来更快地绘制。只有一个小块的`CATiledLayer`是实现异步更新图片视图的简单方法。 296 | 297 | ###drawsAsynchronously 298 | 299 | iOS 6中,苹果为`CALayer`引入了这个令人好奇的属性,`drawsAsynchronously`属性对传入`-drawLayer:inContext:`的CGContext进行改动,允许CGContext延缓绘制命令的执行以至于不阻塞用户交互。 300 | 301 | 它与`CATiledLayer`使用的异步绘制并不相同。它自己的`-drawLayer:inContext:`方法只会在主线程调用,但是CGContext并不等待每个绘制命令的结束。相反地,它会将命令加入队列,当方法返回时,在后台线程逐个执行真正的绘制。 302 | 303 | 根据苹果的说法。这个特性在需要频繁重绘的视图上效果最好(比如我们的绘图应用,或者诸如`UITableViewCell`之类的),对那些只绘制一次或很少重绘的图层内容来说没什么太大的帮助。 304 | 305 | ##总结 306 | 307 | 本章我们主要围绕用Core Graphics软件绘制讨论了一些性能挑战,然后探索了一些改进方法:比如提高绘制性能或者减少需要绘制的数量。 308 | 309 | 第14章,『图像IO』,我们将讨论图片的载入性能。 310 | -------------------------------------------------------------------------------- /13-高效绘图/13.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/13-高效绘图/13.1.png -------------------------------------------------------------------------------- /13-高效绘图/13.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/13-高效绘图/13.2.png -------------------------------------------------------------------------------- /13-高效绘图/13.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/13-高效绘图/13.3.png -------------------------------------------------------------------------------- /13-高效绘图/13.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/13-高效绘图/13.4.png -------------------------------------------------------------------------------- /14-图像IO/14.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/14-图像IO/14.1.jpeg -------------------------------------------------------------------------------- /14-图像IO/14.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/14-图像IO/14.2.jpeg -------------------------------------------------------------------------------- /14-图像IO/14.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/14-图像IO/14.3.jpeg -------------------------------------------------------------------------------- /14-图像IO/14.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/14-图像IO/14.4.jpeg -------------------------------------------------------------------------------- /14-图像IO/14.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/14-图像IO/14.5.jpeg -------------------------------------------------------------------------------- /15-图层性能/15-图层性能.md: -------------------------------------------------------------------------------- 1 | #图层性能 2 | 3 | >要更快性能,也要做对正确的事情。 4 | >——Stephen R. Covey 5 | 6 | 在第14章『图像IO』讨论如何高效地载入和显示图像,通过视图来避免可能引起动画帧率下降的性能问题。在最后一章,我们将着重图层树本身,以发掘最好的性能。 7 | 8 | ##隐式绘制 9 | 10 | 寄宿图可以通过Core Graphics直接绘制,也可以直接载入一个图片文件并赋值给`contents`属性,或事先绘制一个屏幕之外的`CGContext`上下文。在之前的两章中我们讨论了这些场景下的优化。但是除了常见的显式创建寄宿图,你也可以通过以下三种方式创建隐式的:1,使用特性的图层属性。2,特定的视图。3,特定的图层子类。 11 | 12 | 了解这个情况为什么发生何时发生是很重要的,它能够让你避免引入不必要的软件绘制行为。 13 | 14 | ###文本 15 | 16 | `CATextLayer`和`UILabel`都是直接将文本绘制在图层的寄宿图中。事实上这两种方式用了完全不同的渲染方式:在iOS 6及之前,`UILabel`用WebKit的HTML渲染引擎来绘制文本,而`CATextLayer`用的是Core Text.后者渲染更迅速,所以在所有需要绘制大量文本的情形下都优先使用它吧。但是这两种方法都用了软件的方式绘制,因此他们实际上要比硬件加速合成方式要慢。 17 | 18 | 不论如何,尽可能地避免改变那些包含文本的视图的frame,因为这样做的话文本就需要重绘。例如,如果你想在图层的角落里显示一段静态的文本,但是这个图层经常改动,你就应该把文本放在一个子图层中。 19 | 20 | ###光栅化 21 | 22 | 在第四章『视觉效果』中我们提到了`CALayer`的`shouldRasterize`属性,它可以解决重叠透明图层的混合失灵问题。同样在第12章『速度的曲调』中,它也是作为绘制复杂图层树结构的优化方法。 23 | 24 | 启用`shouldRasterize`属性会将图层绘制到一个屏幕之外的图像。然后这个图像将会被缓存起来并绘制到实际图层的`contents`和子图层。如果有很多的子图层或者有复杂的效果应用,这样做就会比重绘所有事务的所有帧划得来得多。但是光栅化原始图像需要时间,而且还会消耗额外的内存。 25 | 26 | 当我们使用得当时,光栅化可以提供很大的性能优势(如你在第12章所见),但是一定要避免作用在内容不断变动的图层上,否则它缓存方面的好处就会消失,而且会让性能变的更糟。 27 | 28 | 为了检测你是否正确地使用了光栅化方式,用Instrument查看一下Color Hits Green和Misses Red项目,是否已光栅化图像被频繁地刷新(这样就说明图层并不是光栅化的好选择,或则你无意间触发了不必要的改变导致了重绘行为)。 29 | 30 | ##离屏渲染 31 | 32 | Offscreen rendering does not necessarily imply software drawing, but it means that the layer must first be rendered (either by the CPU or GPU) into an offscreen context before being displayed. The layer attributes that trigger offscreen rendering are as follows: 33 | 34 | 当图层属性的混合体被指定为在未预合成之前不能直接在屏幕中绘制时,屏幕外渲染就被唤起了。屏幕外渲染并不意味着软件绘制,但是它意味着图层必须在被显示之前在一个屏幕外上下文中被渲染(不论CPU还是GPU)。图层的以下属性将会触发屏幕外绘制: 35 | 36 | * 圆角(当和`maskToBounds`一起使用时) 37 | * 图层蒙板 38 | * 阴影 39 | 40 | 屏幕外渲染和我们启用光栅化时相似,除了它并没有像光栅化图层那么消耗大,子图层并没有被影响到,而且结果也没有被缓存,所以不会有长期的内存占用。但是,如果太多图层在屏幕外渲染依然会影响到性能。 41 | 42 | 有时候我们可以把那些需要屏幕外绘制的图层开启光栅化以作为一个优化方式,前提是这些图层并不会被频繁地重绘。 43 | 44 | 对于那些需要动画而且要在屏幕外渲染的图层来说,你可以用`CAShapeLayer`,`contentsCenter`或者`shadowPath`来获得同样的表现而且较少地影响到性能。 45 | 46 | ### CAShapeLayer 47 | 48 | `cornerRadius`和`maskToBounds`独立作用的时候都不会有太大的性能问题,但是当他俩结合在一起,就触发了屏幕外渲染。有时候你想显示圆角并沿着图层裁切子图层的时候,你可能会发现你并不需要沿着圆角裁切,这个情况下用`CAShapeLayer`就可以避免这个问题了。 49 | 50 | 你想要的只是圆角且沿着矩形边界裁切,同时还不希望引起性能问题。其实你可以用现成的`UIBezierPath`的构造器`+bezierPathWithRoundedRect:cornerRadius:`(见清单15.1).这样做并不会比直接用`cornerRadius`更快,但是它避免了性能问题。 51 | 52 | 清单15.1 用`CAShapeLayer`画一个圆角矩形 53 | 54 | ```objective-c 55 | #import "ViewController.h" 56 | #import 57 | 58 | @interface ViewController () 59 | 60 | @property (nonatomic, weak) IBOutlet UIView *layerView; 61 | 62 | @end 63 | 64 | @implementation ViewController 65 | 66 | - (void)viewDidLoad 67 | { 68 | [super viewDidLoad]; 69 | 70 | //create shape layer 71 | CAShapeLayer *blueLayer = [CAShapeLayer layer]; 72 | blueLayer.frame = CGRectMake(50, 50, 100, 100); 73 | blueLayer.fillColor = [UIColor blueColor].CGColor; 74 | blueLayer.path = [UIBezierPath bezierPathWithRoundedRect: 75 | CGRectMake(0, 0, 100, 100) cornerRadius:20].CGPath; 76 |  77 | //add it to our view 78 | [self.layerView.layer addSublayer:blueLayer]; 79 | } 80 | @end 81 | ``` 82 | 83 | ###可伸缩图片 84 | 85 | 另一个创建圆角矩形的方法就是用一个圆形内容图片并结合第二章『寄宿图』提到的`contensCenter`属性去创建一个可伸缩图片(见清单15.2).理论上来说,这个应该比用`CAShapeLayer`要快,因为一个可拉伸图片只需要18个三角形(一个图片是由一个3*3网格渲染而成),然而,许多都需要渲染成一个顺滑的曲线。在实际应用上,二者并没有太大的区别。 86 | 87 | 清单15.2 用可伸缩图片绘制圆角矩形 88 | 89 | ```objective-c 90 | @implementation ViewController 91 | 92 | - (void)viewDidLoad 93 | { 94 | [super viewDidLoad]; 95 | 96 | //create layer 97 | CALayer *blueLayer = [CALayer layer]; 98 | blueLayer.frame = CGRectMake(50, 50, 100, 100); 99 | blueLayer.contentsCenter = CGRectMake(0.5, 0.5, 0.0, 0.0); 100 | blueLayer.contentsScale = [UIScreen mainScreen].scale; 101 | blueLayer.contents = (__bridge id)[UIImage imageNamed:@"Circle.png"].CGImage; 102 | //add it to our view 103 | [self.layerView.layer addSublayer:blueLayer]; 104 | } 105 | @end 106 | ``` 107 | 108 | 使用可伸缩图片的优势在于它可以绘制成任意边框效果而不需要额外的性能消耗。举个例子,可伸缩图片甚至还可以显示出矩形阴影的效果。 109 | 110 | ###shadowPath 111 | 112 | 在第2章我们有提到`shadowPath`属性。如果图层是一个简单几何图形如矩形或者圆角矩形(假设不包含任何透明部分或者子图层),创建出一个对应形状的阴影路径就比较容易,而且Core Animation绘制这个阴影也相当简单,避免了屏幕外的图层部分的预排版需求。这对性能来说很有帮助。 113 | 114 | 如果你的图层是一个更复杂的图形,生成正确的阴影路径可能就比较难了,这样子的话你可以考虑用绘图软件预先生成一个阴影背景图。 115 | 116 | ##混合和过度绘制 117 | 118 | 在第12章有提到,GPU每一帧可以绘制的像素有一个最大限制(就是所谓的fill rate),这个情况下可以轻易地绘制整个屏幕的所有像素。但是如果由于重叠图层的关系需要不停地重绘同一区域的话,掉帧就可能发生了。 119 | 120 | GPU会放弃绘制那些完全被其他图层遮挡的像素,但是要计算出一个图层是否被遮挡也是相当复杂并且会消耗处理器资源。同样,合并不同图层的透明重叠像素(即混合)消耗的资源也是相当客观的。所以为了加速处理进程,不到必须时刻不要使用透明图层。任何情况下,你应该这样做: 121 | 122 | * 给视图的`backgroundColor`属性设置一个固定的,不透明的颜色 123 | * 设置`opaque`属性为YES 124 | 125 | 这样做减少了混合行为(因为编译器知道在图层之后的东西都不会对最终的像素颜色产生影响)并且计算得到了加速,避免了过度绘制行为因为Core Animation可以舍弃所有被完全遮盖住的图层,而不用每个像素都去计算一遍。 126 | 127 | 如果用到了图像,尽量避免透明除非非常必要。如果图像要显示在一个固定的背景颜色或是固定的背景图之前,你没必要相对前景移动,你只需要预填充背景图片就可以避免运行时混色了。 128 | 129 | 如果是文本的话,一个白色背景的`UILabel`(或者其他颜色)会比透明背景要更高效。 130 | 131 | 最后,明智地使用`shouldRasterize`属性,可以将一个固定的图层体系折叠成单张图片,这样就不需要每一帧重新合成了,也就不会有因为子图层之间的混合和过度绘制的性能问题了。 132 | 133 | ##减少图层数量 134 | 135 | 初始化图层,处理图层,打包通过IPC发给渲染引擎,转化成OpenGL几何图形,这些是一个图层的大致资源开销。事实上,一次性能够在屏幕上显示的最大图层数量也是有限的。 136 | 137 | 确切的限制数量取决于iOS设备,图层类型,图层内容和属性等。但是总得说来可以容纳上百或上千个,下面我们将演示即使图层本身并没有做什么也会遇到的性能问题。 138 | 139 | ###裁切 140 | 141 | 在对图层做任何优化之前,你需要确定你不是在创建一些不可见的图层,图层在以下几种情况下回事不可见的: 142 | 143 | * 图层在屏幕边界之外,或是在父图层边界之外。 144 | * 完全在一个不透明图层之后。 145 | * 完全透明 146 | 147 | Core Animation非常擅长处理对视觉效果无意义的图层。但是经常性地,你自己的代码会比Core Animation更早地想知道一个图层是否是有用的。理想状况下,在图层对象在创建之前就想知道,以避免创建和配置不必要图层的额外工作。 148 | 149 | 举个例子。清单15.3 的代码展示了一个简单的滚动3D图层矩阵。这看上去很酷,尤其是图层在移动的时候(见图15.1),但是绘制他们并不是很麻烦,因为这些图层就是一些简单的矩形色块。 150 | 151 | 清单15.3 绘制3D图层矩阵 152 | 153 | ```objective-c 154 | #import "ViewController.h" 155 | #import 156 | 157 | #define WIDTH 10 158 | #define HEIGHT 10 159 | #define DEPTH 10 160 | #define SIZE 100 161 | #define SPACING 150 162 | #define CAMERA_DISTANCE 500 163 | 164 | @interface ViewController () 165 |  166 | @property (nonatomic, strong) IBOutlet UIScrollView *scrollView; 167 | 168 | @end 169 | 170 | @implementation ViewController 171 | 172 | - (void)viewDidLoad 173 | { 174 | [super viewDidLoad]; 175 | 176 | //set content size 177 | self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); 178 | 179 | //set up perspective transform 180 | CATransform3D transform = CATransform3DIdentity; 181 | transform.m34 = -1.0 / CAMERA_DISTANCE; 182 | self.scrollView.layer.sublayerTransform = transform; 183 | 184 | //create layers 185 | for (int z = DEPTH - 1; z >= 0; z--) { 186 | for (int y = 0; y < HEIGHT; y++) { 187 | for (int x = 0; x < WIDTH; x++) { 188 | //create layer 189 | CALayer *layer = [CALayer layer]; 190 | layer.frame = CGRectMake(0, 0, SIZE, SIZE); 191 | layer.position = CGPointMake(x*SPACING, y*SPACING); 192 | layer.zPosition = -z*SPACING; 193 | //set background color 194 | layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; 195 | //attach to scroll view 196 | [self.scrollView.layer addSublayer:layer]; 197 | } 198 | } 199 | } 200 |  201 | //log 202 | NSLog(@"displayed: %i", DEPTH*HEIGHT*WIDTH); 203 | } 204 | @end 205 | ``` 206 | 207 | ![](./15.1.png) 208 | 209 | 图15.1 滚动的3D图层矩阵 210 | 211 | `WIDTH`,`HEIGHT`和`DEPTH`常量控制着图层的生成。在这个情况下,我们得到的是10\*10\*10个图层,总量为1000个,不过一次性显示在屏幕上的大约就几百个。 212 | 213 | 如果把`WIDTH`和`HEIGHT`常量增加到100,我们的程序就会慢得像龟爬了。这样我们有了100000个图层,性能下降一点儿也不奇怪。 214 | 215 | 但是显示在屏幕上的图层数量并没有增加,那么根本没有额外的东西需要绘制。程序慢下来的原因其实是因为在管理这些图层上花掉了不少功夫。他们大部分对渲染的最终结果没有贡献,但是在丢弃这么图层之前,Core Animation要强制计算每个图层的位置,就这样,我们的帧率就慢了下来。 216 | 217 | 我们的图层是被安排在一个均匀的栅格中,我们可以计算出哪些图层会被最终显示在屏幕上,根本不需要对每个图层的位置进行计算。这个计算并不简单,因为我们还要考虑到透视的问题。如果我们直接这样做了,Core Animation就不用费神了。 218 | 219 | 既然这样,让我们来重构我们的代码吧。改造后,随着视图的滚动动态地实例化图层而不是事先都分配好。这样,在创造他们之前,我们就可以计算出是否需要他。接着,我们增加一些代码去计算可视区域这样就可以排除区域之外的图层了。清单15.4是改造后的结果。 220 | 221 | 清单15.4 排除可视区域之外的图层 222 | 223 | ```objective-c 224 | #import "ViewController.h" 225 | #import 226 | 227 | #define WIDTH 100 228 | #define HEIGHT 100 229 | #define DEPTH 10 230 | #define SIZE 100 231 | #define SPACING 150 232 | #define CAMERA_DISTANCE 500 233 | #define PERSPECTIVE(z) (float)CAMERA_DISTANCE/(z + CAMERA_DISTANCE) 234 | 235 | @interface ViewController () 236 | 237 | @property (nonatomic, weak) IBOutlet UIScrollView *scrollView; 238 | 239 | @end 240 | 241 | @implementation ViewController 242 | 243 | - (void)viewDidLoad 244 | { 245 | [super viewDidLoad]; 246 | //set content size 247 | self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); 248 | //set up perspective transform 249 | CATransform3D transform = CATransform3DIdentity; 250 | transform.m34 = -1.0 / CAMERA_DISTANCE; 251 | self.scrollView.layer.sublayerTransform = transform; 252 | } 253 |  254 | - (void)viewDidLayoutSubviews 255 | { 256 | [self updateLayers]; 257 | } 258 | 259 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView 260 | { 261 | [self updateLayers]; 262 | } 263 | 264 | - (void)updateLayers 265 | { 266 | //calculate clipping bounds 267 | CGRect bounds = self.scrollView.bounds; 268 | bounds.origin = self.scrollView.contentOffset; 269 | bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); 270 | //create layers 271 | NSMutableArray *visibleLayers = [NSMutableArray array]; 272 | for (int z = DEPTH - 1; z >= 0; z--) 273 | { 274 | //increase bounds size to compensate for perspective 275 | CGRect adjusted = bounds; 276 | adjusted.size.width /= PERSPECTIVE(z*SPACING); 277 | adjusted.size.height /= PERSPECTIVE(z*SPACING); 278 | adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; 279 | adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; 280 | for (int y = 0; y < HEIGHT; y++) { 281 | //check if vertically outside visible rect 282 | if (y*SPACING < adjusted.origin.y || y*SPACING >= adjusted.origin.y + adjusted.size.height) 283 | { 284 | continue; 285 | } 286 | for (int x = 0; x < WIDTH; x++) { 287 | //check if horizontally outside visible rect 288 | if (x*SPACING < adjusted.origin.x ||x*SPACING >= adjusted.origin.x + adjusted.size.width) 289 | { 290 | continue; 291 | } 292 |  293 | //create layer 294 | CALayer *layer = [CALayer layer]; 295 | layer.frame = CGRectMake(0, 0, SIZE, SIZE); 296 | layer.position = CGPointMake(x*SPACING, y*SPACING); 297 | layer.zPosition = -z*SPACING; 298 | //set background color 299 | layer.backgroundColor = [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; 300 | //attach to scroll view 301 | [visibleLayers addObject:layer]; 302 | } 303 | } 304 | } 305 | //update layers 306 | self.scrollView.layer.sublayers = visibleLayers; 307 | //log 308 | NSLog(@"displayed: %i/%i", [visibleLayers count], DEPTH*HEIGHT*WIDTH); 309 | } 310 | @end 311 | ``` 312 | 313 | 这个计算机制并不具有普适性,但是原则上是一样。(当你用一个`UITableView`或者`UICollectionView`时,系统做了类似的事情)。这样做的结果?我们的程序可以处理成百上千个『虚拟』图层而且完全没有性能问题!因为它不需要一次性实例化几百个图层。 314 | 315 | ###对象回收 316 | 317 | 处理巨大数量的相似视图或图层时还有一个技巧就是回收他们。对象回收在iOS颇为常见;`UITableView`和`UICollectionView`都有用到,`MKMapView`中的动画pin码也有用到,还有其他很多例子。 318 | 319 | 对象回收的基础原则就是你需要创建一个相似对象池。当一个对象的指定实例(本例子中指的是图层)结束了使命,你把它添加到对象池中。每次当你需要一个实例时,你就从池中取出一个。当且仅当池中为空时再创建一个新的。 320 | 321 | 这样做的好处在于避免了不断创建和释放对象(相当消耗资源,因为涉及到内存的分配和销毁)而且也不必给相似实例重复赋值。 322 | 323 | 好了,让我们再次更新代码吧(见清单15.5) 324 | 325 | 清单15.5 通过回收减少不必要的分配 326 | 327 | ```objective-c 328 | @interface ViewController () 329 | 330 | @property (nonatomic, weak) IBOutlet UIScrollView *scrollView; 331 | @property (nonatomic, strong) NSMutableSet *recyclePool; 332 | 333 | 334 | @end 335 | 336 | @implementation ViewController 337 | 338 | - (void)viewDidLoad 339 | { 340 | [super viewDidLoad]; //create recycle pool 341 | self.recyclePool = [NSMutableSet set]; 342 | //set content size 343 | self.scrollView.contentSize = CGSizeMake((WIDTH - 1)*SPACING, (HEIGHT - 1)*SPACING); 344 | //set up perspective transform 345 | CATransform3D transform = CATransform3DIdentity; 346 | transform.m34 = -1.0 / CAMERA_DISTANCE; 347 | self.scrollView.layer.sublayerTransform = transform; 348 | } 349 | 350 | - (void)viewDidLayoutSubviews 351 | { 352 | [self updateLayers]; 353 | } 354 | 355 | - (void)scrollViewDidScroll:(UIScrollView *)scrollView 356 | { 357 | [self updateLayers]; 358 | } 359 | 360 | - (void)updateLayers { 361 |  362 | //calculate clipping bounds 363 | CGRect bounds = self.scrollView.bounds; 364 | bounds.origin = self.scrollView.contentOffset; 365 | bounds = CGRectInset(bounds, -SIZE/2, -SIZE/2); 366 | //add existing layers to pool 367 | [self.recyclePool addObjectsFromArray:self.scrollView.layer.sublayers]; 368 | //disable animation 369 | [CATransaction begin]; 370 | [CATransaction setDisableActions:YES]; 371 | //create layers 372 | NSInteger recycled = 0; 373 | NSMutableArray *visibleLayers = [NSMutableArray array]; 374 | for (int z = DEPTH - 1; z >= 0; z--) 375 | { 376 | //increase bounds size to compensate for perspective 377 | CGRect adjusted = bounds; 378 | adjusted.size.width /= PERSPECTIVE(z*SPACING); 379 | adjusted.size.height /= PERSPECTIVE(z*SPACING); 380 | adjusted.origin.x -= (adjusted.size.width - bounds.size.width) / 2; adjusted.origin.y -= (adjusted.size.height - bounds.size.height) / 2; 381 | for (int y = 0; y < HEIGHT; y++) { 382 | //check if vertically outside visible rect 383 | if (y*SPACING < adjusted.origin.y || 384 | y*SPACING >= adjusted.origin.y + adjusted.size.height) 385 | { 386 | continue; 387 | } 388 | for (int x = 0; x < WIDTH; x++) { 389 | //check if horizontally outside visible rect 390 | if (x*SPACING < adjusted.origin.x || 391 | x*SPACING >= adjusted.origin.x + adjusted.size.width) 392 | { 393 | continue; 394 | } 395 | //recycle layer if available 396 | CALayer *layer = [self.recyclePool anyObject]; if (layer) 397 | { 398 |  399 | recycled ++; 400 | [self.recyclePool removeObject:layer]; } 401 | else 402 | { 403 | layer = [CALayer layer]; 404 | layer.frame = CGRectMake(0, 0, SIZE, SIZE); } 405 | //set position 406 | layer.position = CGPointMake(x*SPACING, y*SPACING); layer.zPosition = -z*SPACING; 407 | //set background color 408 | layer.backgroundColor = 409 | [UIColor colorWithWhite:1-z*(1.0/DEPTH) alpha:1].CGColor; 410 | //attach to scroll view 411 | [visibleLayers addObject:layer]; } 412 | } } 413 | [CATransaction commit]; //update layers 414 | self.scrollView.layer.sublayers = visibleLayers; 415 | //log 416 | NSLog(@"displayed: %i/%i recycled: %i", 417 | [visibleLayers count], DEPTH*HEIGHT*WIDTH, recycled); 418 | } 419 | @end 420 | ``` 421 | 422 | 本例中,我们只有图层对象这一种类型,但是UIKit有时候用一个标识符字符串来区分存储在不同对象池中的不同的可回收对象类型。 423 | 424 | 你可能注意到当设置图层属性时我们用了一个`CATransaction`来抑制动画效果。在之前并不需要这样做,因为在显示之前我们给所有图层设置一次属性。但是既然图层正在被回收,禁止隐式动画就有必要了,不然当属性值改变时,图层的隐式动画就会被触发。 425 | 426 | ###Core Graphics绘制 427 | 428 | 当排除掉对屏幕显示没有任何贡献的图层或者视图之后,长远看来,你可能仍然需要减少图层的数量。例如,如果你正在使用多个`UILabel`或者`UIImageView`实例去显示固定内容,你可以把他们全部替换成一个单独的视图,然后用`-drawRect:`方法绘制出那些复杂的视图层级。 429 | 430 | 这个提议看上去并不合理因为大家都知道软件绘制行为要比GPU合成要慢而且还需要更多的内存空间,但是在因为图层数量而使得性能受限的情况下,软件绘制很可能提高性能呢,因为它避免了图层分配和操作问题。 431 | 432 | 你可以自己实验一下这个情况,它包含了性能和栅格化的权衡,但是意味着你可以从图层树上去掉子图层(用`shouldRasterize`,与完全遮挡图层相反)。 433 | 434 | ###-renderInContext: 方法 435 | 436 | 用Core Graphics去绘制一个静态布局有时候会比用层级的`UIView`实例来得快,但是使用`UIView`实例要简单得多而且比用手写代码写出相同效果要可靠得多,更边说Interface Builder来得直接明了。为了性能而舍弃这些便利实在是不应该。 437 | 438 | 幸好,你不必这样,如果大量的视图或者图层真的关联到了屏幕上将会是一个大问题。没有与图层树相关联的图层不会被送到渲染引擎,也没有性能问题(在他们被创建和配置之后)。 439 | 440 | 使用`CALayer`的`-renderInContext:`方法,你可以将图层及其子图层快照进一个Core Graphics上下文然后得到一个图片,它可以直接显示在`UIImageView`中,或者作为另一个图层的`contents`。不同于`shouldRasterize` —— 要求图层与图层树相关联 —— ,这个方法没有持续的性能消耗。 441 | 442 | 当图层内容改变时,刷新这张图片的机会取决于你(不同于`shouldRasterize`,它自动地处理缓存和缓存验证),但是一旦图片被生成,相比于让Core Animation处理一个复杂的图层树,你节省了相当客观的性能。 443 | 444 | ##总结 445 | 446 | 本章学习了使用Core Animation图层可能遇到的性能瓶颈,并讨论了如何避免或减小压力。你学习了如何管理包含上千虚拟图层的场景(事实上只创建了几百个)。同时也学习了一些有用的技巧,选择性地选取光栅化或者绘制图层内容在合适的时候重新分配给CPU和GPU。这些就是我们要讲的关于Core Animation的全部了(至少可以等到苹果发明什么新的玩意儿)。 447 | -------------------------------------------------------------------------------- /15-图层性能/15.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/15-图层性能/15.1.png -------------------------------------------------------------------------------- /2-寄宿图/2.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.1.png -------------------------------------------------------------------------------- /2-寄宿图/2.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.10.png -------------------------------------------------------------------------------- /2-寄宿图/2.11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.11.png -------------------------------------------------------------------------------- /2-寄宿图/2.12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.12.png -------------------------------------------------------------------------------- /2-寄宿图/2.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.2.png -------------------------------------------------------------------------------- /2-寄宿图/2.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.3.png -------------------------------------------------------------------------------- /2-寄宿图/2.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.4.png -------------------------------------------------------------------------------- /2-寄宿图/2.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.5.png -------------------------------------------------------------------------------- /2-寄宿图/2.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.6.png -------------------------------------------------------------------------------- /2-寄宿图/2.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.7.png -------------------------------------------------------------------------------- /2-寄宿图/2.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.8.png -------------------------------------------------------------------------------- /2-寄宿图/2.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/2-寄宿图/2.9.png -------------------------------------------------------------------------------- /2-寄宿图/寄宿图.md: -------------------------------------------------------------------------------- 1 | #寄宿图 2 | >图片胜过千言万语,界面抵得上千图片 ——Ben Shneiderman 3 | 4 | 我们在第一章『图层树』中介绍了CALayer类并创建了一个简单的有蓝色背景的图层。背景颜色还好啦,但是如果它仅仅是展现了一个单调的颜色未免也太无聊了。事实上CALayer类能够包含一张你喜欢的图片,这一章节我们将来探索CALayer的寄宿图(即图层中包含的图)。 5 | 6 | ##contents属性 7 | CALayer 有一个属性叫做`contents`,这个属性的类型被定义为id,意味着它可以是任何类型的对象。在这种情况下,你可以给`contents`属性赋任何值,你的app仍然能够编译通过。但是,在实践中,如果你给`contents`赋的不是CGImage,那么你得到的图层将是空白的。 8 | 9 | `contents`这个奇怪的表现是由Mac OS的历史原因造成的。它之所以被定义为id类型,是因为在Mac OS系统上,这个属性对CGImage和NSImage类型的值都起作用。如果你试图在iOS平台上将UIImage的值赋给它,只能得到一个空白的图层。一些初识Core Animation的iOS开发者可能会对这个感到困惑。 10 | 11 | 头疼的不仅仅是我们刚才提到的这个问题。事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个"CGImageRef",如果你想把这个值直接赋值给CALayer的`contents`,那你将会得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。 12 | 13 | 尽管Core Foundation类型跟Cocoa对象在运行时貌似很像(被称作toll-free bridging),它们并不是类型兼容的,不过你可以通过bridged关键字转换。如果要给图层的寄宿图赋值,你可以按照以下这个方法: 14 | 15 | ``` objective-c 16 | layer.contents = (__bridge id)image.CGImage; 17 | ``` 18 | 19 | 如果你没有使用ARC(自动引用计数),你就不需要__bridge这部分。但是,你干嘛不用ARC?! 20 | 21 | 让我们来继续修改我们在第一章新建的工程,以便能够展示一张图片而不仅仅是一个背景色。我们已经用代码的方式建立一个图层,那我们就不需要额外的图层了。那么我们就直接把layerView的宿主图层的`contents`属性设置成图片。 22 | 23 | 清单2.1 更新后的代码。 24 | 25 | ``` objective-c 26 | @implementation ViewController 27 | 28 | - (void)viewDidLoad 29 | { 30 | [super viewDidLoad]; //load an image 31 | UIImage *image = [UIImage imageNamed:@"Snowman.png"]; 32 | 33 | //add it directly to our view's layer 34 | self.layerView.layer.contents = (__bridge id)image.CGImage; 35 | } 36 | @end 37 | ``` 38 | 39 | 图表2.1 在UIView的宿主图层中显示一张图片 40 | 41 | ![图2.1](./2.1.png) 42 | 43 | 我们用这些简单的代码做了一件很有趣的事情:我们利用CALayer在一个普通的UIView中显示了一张图片。这不是一个UIImageView,它不是我们通常用来展示图片的方法。通过直接操作图层,我们使用了一些新的函数,使得UIView更加有趣了。 44 | 45 | **contentGravity** 46 | 47 | 你可能已经注意到了我们的雪人看起来有点。。。胖 ==! 我们加载的图片并不刚好是一个方的,为了适应这个视图,它有一点点被拉伸了。在使用UIImageView的时候遇到过同样的问题,解决方法就是把`contentMode`属性设置成更合适的值,像这样: 48 | 49 | ```objective-c 50 | view.contentMode = UIViewContentModeScaleAspectFit; 51 | ``` 52 | 这个方法基本和我们遇到的情况的解决方法已经接近了(你可以试一下 :) ),不过UIView大多数视觉相关的属性比如`contentMode`,对这些属性的操作其实是对对应图层的操作。 53 | 54 | CALayer与`contentMode`对应的属性叫做`contentsGravity`,但是它是一个NSString类型,而不是像对应的UIKit部分,那里面的值是枚举。`contentsGravity`可选的常量值有以下一些: 55 | 56 | * kCAGravityCenter 57 | * kCAGravityTop 58 | * kCAGravityBottom 59 | * kCAGravityLeft 60 | * kCAGravityRight 61 | * kCAGravityTopLeft 62 | * kCAGravityTopRight 63 | * kCAGravityBottomLeft 64 | * kCAGravityBottomRight 65 | * kCAGravityResize 66 | * kCAGravityResizeAspect 67 | * kCAGravityResizeAspectFill 68 | 69 | 和`cotentMode`一样,`contentsGravity`的目的是为了决定内容在图层的边界中怎么对齐,我们将使用kCAGravityResizeAspect,它的效果等同于UIViewContentModeScaleAspectFit, 同时它还能在图层中等比例拉伸以适应图层的边界。 70 | 71 | ```objective-c 72 | self.layerView.layer.contentsGravity = kCAGravityResizeAspect; 73 | ``` 74 | 75 | 图2.2 可以看到结果 76 | 77 | ![image](./2.2.png) 78 | 79 | 图2.2 正确地设置`contentsGravity`的值 80 | 81 | ##contentsScale 82 | 83 | `contentsScale`属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。 84 | 85 | `contentsScale`的目的并不是那么明显。它并不是总会对屏幕上的寄宿图有影响。如果你尝试对我们的例子设置不同的值,你就会发现根本没任何影响。因为`contents`由于设置了`contentsGravity`属性,所以它已经被拉伸以适应图层的边界。 86 | 87 | 如果你只是单纯地想放大图层的`contents`图片,你可以通过使用图层的`transform`和`affineTransform`属性来达到这个目的(见第五章『Transforms』,里面对此有解释),这(指放大)也不是`contentsScale`的目的所在. 88 | 89 | `contentsScale`属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置`contentsGravity`属性)。UIView有一个类似功能但是非常少用到的`contentScaleFactor`属性。 90 | 91 | 如果`contentsScale`设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。(如果你对像素和点的概念不是很清楚的话,这个章节的后面部分将会对此做出解释)。 92 | 93 | 这并不会对我们在使用kCAGravityResizeAspect时产生任何影响,因为它就是拉伸图片以适应图层而已,根本不会考虑到分辨率问题。但是如果我们把`contentsGravity`设置为kCAGravityCenter(这个值并不会拉伸图片),那将会有很明显的变化(如图2.3) 94 | 95 | ![图2.3](./2.3.png) 96 | 97 | 图2.3 用错误的`contentsScale`属性显示Retina图片 98 | 99 | 如你所见,我们的雪人不仅有点大还有点像素的颗粒感。那是因为和UIImage不同,CGImage没有拉伸的概念。当我们使用UIImage类去读取我们的雪人图片的时候,它读取了高质量的Retina版本的图片。但是当我们用CGImage来设置我们的图层的内容时,拉伸这个因素在转换的时候就丢失了。不过我们可以通过手动设置`contentsScale`来修复这个问题(如2.2清单),图2.4是结果 100 | 101 | ```objective-c 102 | @implementation ViewController 103 | 104 | - (void)viewDidLoad 105 | { 106 | [super viewDidLoad]; //load an image 107 | UIImage *image = [UIImage imageNamed:@"Snowman.png"]; //add it directly to our view's layer 108 | self.layerView.layer.contents = (__bridge id)image.CGImage; //center the image 109 | self.layerView.layer.contentsGravity = kCAGravityCenter; 110 | 111 | //set the contentsScale to match image 112 | self.layerView.layer.contentsScale = image.scale; 113 | } 114 | 115 | @end 116 | ``` 117 | 118 | ![图2.4](./2.4.png) 119 | 120 | 图2.4 同样的Retina图片设置了正确的`contentsScale`之后 121 | 122 | 当用代码的方式来处理寄宿图的时候,一定要记住要手动的设置图层的`contentsScale`属性,否则,你的图片在Retina设备上就显示得不正确啦。代码如下: 123 | 124 | ```objective-c 125 | layer.contentsScale = [UIScreen mainScreen].scale; 126 | ``` 127 | 128 | ##maskToBounds 129 | 130 | 现在我们的雪人总算是显示了正确的大小,不过你也许已经发现了另外一些事情:它超出了视图的边界。默认情况下,UIView仍然会绘制超过边界的内容或是子视图,在CALayer下也是这样的。 131 | 132 | UIView有一个叫做`clipsToBounds`的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做`masksToBounds`,把它设置为YES,雪人就在边界里啦~(如图2.5) 133 | 134 | ![图2.5](./2.5.png) 135 | 136 | 图2.5 使用`masksToBounds`来修建图层内容 137 | 138 | ##contentsRect 139 | 140 | CALayer的`contentsRect`属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比`contentsGravity`灵活多了 141 | 142 | 和`bounds`,`frame`不同,`contentsRect`不是按点来计算的,它使用了*单位坐标*,单位坐标指定在0到1之间,是一个相对值(像素和点就是绝对值)。所以它们是相对与寄宿图的尺寸的。iOS使用了以下的坐标系统: 143 | 144 | * 点 —— 在iOS和Mac OS中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina设备上,一个点等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。 145 | * 像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以你要清楚在Retina设备和普通设备上,它们表现出来了不同的大小。 146 | * 单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。 147 | 148 | 默认的`contentsRect`是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪(如图2.6) 149 | 150 | ![图2.6](./2.6.png) 151 | 152 | 图2.6 一个自定义的`contentsRect`(左)和之前显示的内容(右) 153 | 154 | 事实上给`contentsRect`设置一个负数的原点或是大于{1, 1}的尺寸也是可以的。这种情况下,最外面的像素会被拉伸以填充剩下的区域。 155 | 156 | `contentsRect`在app中最有趣的地方在于一个叫做*image sprites*(图片拼合)的用法。如果你有游戏编程的经验,那么你一定对图片拼合的概念很熟悉,图片能够在屏幕上独立地变更位置。抛开游戏编程不谈,这个技术常用来指代载入拼合的图片,跟移动图片一点关系也没有。 157 | 158 | 典型地,图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做能够带来很多方面的好处:内存使用,载入时间,渲染性能等等 159 | 160 | 2D游戏引擎入Cocos2D使用了拼合技术,它使用OpenGL来显示图片。不过我们可以使用拼合在一个普通的UIKit应用中,对!就是使用`contentsRect` 161 | 162 | 首先,我们需要一个拼合后的图表 —— 一个包含小一些的拼合图的大图片。如图2.7所示: 163 | 164 | ![图2.7](./2.7.png) 165 | 166 | 接下来,我们要在app中载入并显示这些拼合图。规则很简单:像平常一样载入我们的大图,然后把它赋值给四个独立的图层的`contents`,然后设置每个图层的`contentsRect`来去掉我们不想显示的部分。 167 | 168 | 我们的工程中需要一些额外的视图。(为了避免太多代码。我们将使用Interface Builder来访问它们的位置,如果你愿意还是可以用代码的方式来实现的)。清单2.3有需要的代码,图2.8展示了结果 169 | 170 | ```objective-c 171 | 172 | @interface ViewController () 173 | @property (nonatomic, weak) IBOutlet UIView *coneView; 174 | @property (nonatomic, weak) IBOutlet UIView *shipView; 175 | @property (nonatomic, weak) IBOutlet UIView *iglooView; 176 | @property (nonatomic, weak) IBOutlet UIView *anchorView; 177 | @end 178 | 179 | @implementation ViewController 180 | 181 | - (void)addSpriteImage:(UIImage *)image withContentRect:(CGRect)rect toLayer:(CALayer *)layer //set image 182 | { 183 | layer.contents = (__bridge id)image.CGImage; 184 | 185 | //scale contents to fit 186 | layer.contentsGravity = kCAGravityResizeAspect; 187 | 188 | //set contentsRect 189 | layer.contentsRect = rect; 190 | } 191 | 192 | - (void)viewDidLoad 193 | { 194 | [super viewDidLoad]; //load sprite sheet 195 | UIImage *image = [UIImage imageNamed:@"Sprites.png"]; 196 | //set igloo sprite 197 | [self addSpriteImage:image withContentRect:CGRectMake(0, 0, 0.5, 0.5) toLayer:self.iglooView.layer]; 198 | //set cone sprite 199 | [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0, 0.5, 0.5) toLayer:self.coneView.layer]; 200 | //set anchor sprite 201 | [self addSpriteImage:image withContentRect:CGRectMake(0, 0.5, 0.5, 0.5) toLayer:self.anchorView.layer]; 202 | //set spaceship sprite 203 | [self addSpriteImage:image withContentRect:CGRectMake(0.5, 0.5, 0.5, 0.5) toLayer:self.shipView.layer]; 204 | } 205 | @end 206 | ``` 207 | ![图2.8](./2.8.png) 208 | 209 | 拼合不仅给app提供了一个整洁的载入方式,还有效地提高了载入性能(单张大图比多张小图载入得更快),但是如果有手动安排的话,它们还是有一些不方便的,如果你需要在一个已经创建好的拼合图上做一些尺寸上的修改或者其他变动,无疑是比较麻烦的。 210 | 211 | Mac上有一些商业软件可以为你自动拼合图片,这些工具自动生成一个包含拼合后的坐标的XML或者plist文件,拼合图片的使用大大简化。这个文件可以和图片一同载入,并给每个拼合的图层设置`contentsRect`,这样开发者就不用手动写代码来摆放位置了。 212 | 213 | 这些文件通常在OpenGL游戏中使用,不过呢,你要是有兴趣在一些常见的app中使用拼合技术,那么一个叫做LayerSprites的开源库([https://github.com/nicklockwood/LayerSprites](https://github.com/nicklockwood/LayerSprites)),它能够读取Cocos2D格式中的拼合图并在普通的Core Animation层中显示出来。 214 | 215 | ##contentsCenter 216 | 217 | 本章我们介绍的最后一个和内容有关的属性是`contentsCenter`,看名字你可能会以为它可能跟图片的位置有关,不过这名字着实误导了你。`contentsCenter`其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。 改变`contentsCenter`的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。 218 | 219 | 默认情况下,`contentsCenter`是{0, 0, 1, 1},这意味着如果大小(由`conttensGravity`决定)改变了,那么寄宿图将会均匀地拉伸开。但是如果我们增加原点的值并减小尺寸。我们会在图片的周围创造一个边框。图2.9展示了`contentsCenter`设置为{0.25, 0.25, 0.5, 0.5}的效果。 220 | 221 | ![图2.9](./2.9.png) 222 | 223 | 图2.9 `contentsCenter`的例子 224 | 225 | 这意味着我们可以随意重设尺寸,边框仍然会是连续的。它工作起来的效果和UIImage里的-resizableImageWithCapInsets: 方法效果非常类似,只是它可以运用到任何寄宿图,甚至包括在Core Graphics运行时绘制的图形(本章稍后会讲到)。 226 | 227 | ![图2.10](./2.10.png) 228 | 229 | 图2.10 同一图片使用不同的`contentsCenter` 230 | 231 | 清单2.4 演示了如何编写这些可拉伸视图。不过,contentsCenter的另一个很酷的特性就是,它可以在Interface Builder里面配置,根本不用写代码。如图2.11 232 | 233 | 清单2.4 用`contentsCenter`设置可拉伸视图 234 | 235 | ```objective-c 236 | @interface ViewController () 237 | 238 | @property (nonatomic, weak) IBOutlet UIView *button1; 239 | @property (nonatomic, weak) IBOutlet UIView *button2; 240 | 241 | @end 242 | 243 | @implementation ViewController 244 | 245 | - (void)addStretchableImage:(UIImage *)image withContentCenter:(CGRect)rect toLayer:(CALayer *)layer 246 | { 247 | //set image 248 | layer.contents = (__bridge id)image.CGImage; 249 | 250 | //set contentsCenter 251 | layer.contentsCenter = rect; 252 | } 253 | 254 | - (void)viewDidLoad 255 | { 256 | [super viewDidLoad]; //load button image 257 | UIImage *image = [UIImage imageNamed:@"Button.png"]; 258 | 259 | //set button 1 260 | [self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button1.layer]; 261 | 262 | //set button 2 263 | [self addStretchableImage:image withContentCenter:CGRectMake(0.25, 0.25, 0.5, 0.5) toLayer:self.button2.layer]; 264 | } 265 | 266 | @end 267 | ``` 268 | ![图2.11](./2.11.png) 269 | 270 | 图2.11 用Interface Builder 探测窗口控制`contentsCenter`属性 271 | 272 | ##Custome Drawing 273 | 274 | 给`contents`赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现`-drawRect:`方法来自定义绘制。 275 | 276 | `-drawRect:` 方法没有默认的实现,因为对UIView来说,寄宿图并不是必须的,它不在意那到底是单调的颜色还是有一个图片的实例。如果UIView检测到`-drawRect:` 方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以 `contentsScale`的值。 277 | 278 | 如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。 279 | 280 | 当视图在屏幕上出现的时候 `-drawRect:`方法就会被自动调用。`-drawRect:`方法里面的代码利用Core Graphics去绘制一个寄宿图,然后内容就会被缓存起来直到它需要被更新(通常是因为开发者调用了`-setNeedsDisplay`方法,尽管影响到表现效果的属性值被更改时,一些视图类型会被自动重绘,如`bounds`属性)。虽然`-drawRect:`方法是一个UIView方法,事实上都是底层的CALayer安排了重绘工作和保存了因此产生的图片。 281 | 282 | CALayer有一个可选的`delegate`属性,实现了`CALayerDelegate`协议,当CALayer需要一个内容特定的信息时,就会从协议中请求。CALayerDelegate是一个非正式协议,其实就是说没有CALayerDelegate @protocol可以让你在类里面引用啦。你只需要调用你想调用的方法,CALayer会帮你做剩下的。(`delegate`属性被声明为id类型,所有的代理方法都是可选的)。 283 | 284 | 当需要被重绘时,CALayer会请求它的代理给它一个寄宿图来显示。它通过调用下面这个方法做到的: 285 | 286 | ```objective-c 287 | (void)displayLayer:(CALayer *)layer; 288 | ``` 289 | 290 | 趁着这个机会,如果代理想直接设置`contents`属性的话,它就可以这么做,不然没有别的方法可以调用了。如果代理不实现`-displayLayer:`方法,CALayer就会转而尝试调用下面这个方法: 291 | 292 | ```objective-c 293 | - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; 294 | ``` 295 | 296 | 在调用这个方法之前,CALayer创建了一个合适尺寸的空寄宿图(尺寸由`bounds`和`contentsScale`决定)和一个Core Graphics的绘制上下文环境,为绘制寄宿图做准备,它作为ctx参数传入。 297 | 298 | 让我们来继续第一章的项目让它实现CALayerDelegate并做一些绘图工作吧(见清单2.5).图2.12是它的结果 299 | 300 | 清单2.5 实现CALayerDelegate 301 | 302 | ```objective-c 303 | @implementation ViewController 304 | - (void)viewDidLoad 305 | { 306 | [super viewDidLoad]; 307 |  308 | //create sublayer 309 | CALayer *blueLayer = [CALayer layer]; 310 | blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); 311 | blueLayer.backgroundColor = [UIColor blueColor].CGColor; 312 | 313 | //set controller as layer delegate 314 | blueLayer.delegate = self; 315 | 316 | //ensure that layer backing image uses correct scale 317 | blueLayer.contentsScale = [UIScreen mainScreen].scale; //add layer to our view 318 | [self.layerView.layer addSublayer:blueLayer]; 319 | 320 | //force layer to redraw 321 | [blueLayer display]; 322 | } 323 | 324 | - (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx 325 | { 326 | //draw a thick red circle 327 | CGContextSetLineWidth(ctx, 10.0f); 328 | CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor); 329 | CGContextStrokeEllipseInRect(ctx, layer.bounds); 330 | } 331 | @end 332 | ``` 333 | 334 | ![图2.12](./2.12.png) 335 | 336 | 图2.12 实现CALayerDelegate来绘制图层 337 | 338 | 注意一下一些有趣的事情: 339 | 340 | * 我们在blueLayer上显式地调用了`-display`。不同于UIView,当图层显示在屏幕上时,CALayer不会自动重绘它的内容。它把重绘的决定权交给了开发者。 341 | * 尽管我们没有用`masksToBounds`属性,绘制的那个圆仍然沿边界被裁剪了。这是因为当你使用CALayerDelegate绘制寄宿图的时候,并没有对超出边界外的内容提供绘制支持。 342 | 343 | 现在你理解了CALayerDelegate,并知道怎么使用它。但是除非你创建了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。因为当UIView创建了它的宿主图层时,它就会自动地把图层的delegate设置为它自己,并提供了一个`-displayLayer:`的实现,那所有的问题就都没了。 344 | 345 | 当使用寄宿了视图的图层的时候,你也不必实现`-displayLayer:`和`-drawLayer:inContext:`方法来绘制你的寄宿图。通常做法是实现UIView的`-drawRect:`方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用`-display`方法。 346 | 347 | ##总结 348 | 349 | 本章介绍了寄宿图和一些相关的属性。你学到了如何显示和放置图片, 使用拼合技术来显示, 以及用CALayerDelegate和Core Graphics来绘制图层内容。 350 | 351 | 在第三章,"图层几何学"中,我们将会探讨一下图层的几何,观察它们是如何放置和改变相互的尺寸的。 352 | -------------------------------------------------------------------------------- /3-图层几何学/3.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.1.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.10.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.2.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.3.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.4.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.5.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.6.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.7.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.8.jpeg -------------------------------------------------------------------------------- /3-图层几何学/3.9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/3-图层几何学/3.9.jpeg -------------------------------------------------------------------------------- /3-图层几何学/图层几何学.md: -------------------------------------------------------------------------------- 1 | #图层几何学 2 | >*不熟悉几何学的人就不要来这里了* --柏拉图学院入口的签名 3 | 4 | 在第二章里面,我们介绍了图层背后的图片,和一些控制图层坐标和旋转的属性。在这一章中,我们将要看一看图层内部是如何根据父图层和兄弟图层来控制位置和尺寸的。另外我们也会涉及如何管理图层的几何结构,以及它是如何被自动调整和自动布局影响的。 5 | 6 | ##布局 7 | `UIView`有三个比较重要的布局属性:`frame`,`bounds`和`center`,`CALayer`对应地叫做`frame`,`bounds`和`position`。为了能清楚区分,图层用了“position”,视图用了“center”,但是他们都代表同样的值。 8 | 9 | `frame`代表了图层的外部坐标(也就是在父图层上占据的空间),`bounds`是内部坐标({0, 0}通常是图层的左上角),`center`和`position`都代表了相对于父图层`anchorPoint`所在的位置。`anchorPoint`的属性将会在后续介绍到,现在把它想成图层的中心点就好了。图3.1显示了这些属性是如何相互依赖的。 10 | 11 | 图3.1 12 | 13 | 图3.1 `UIView`和`CALayer`的坐标系 14 | 15 | 视图的`frame`,`bounds`和`center`属性仅仅是*存取方法*,当操纵视图的`frame`,实际上是在改变位于视图下方`CALayer`的`frame`,不能够独立于图层之外改变视图的`frame`。 16 | 17 | 对于视图或者图层来说,`frame`并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据`bounds`,`position`和`transform`计算而来,所以当其中任何一个值发生改变,frame都会变化。相反,改变frame的值同样会影响到他们当中的值 18 | 19 | 记住当对图层做变换的时候,比如旋转或者缩放,`frame`实际上代表了覆盖在图层旋转之后的整个轴对齐的矩形区域,也就是说`frame`的宽高可能和`bounds`的宽高不再一致了(图3.2) 20 | 21 | 图3.2 22 | 23 | 图3.2 旋转一个视图或者图层之后的`frame`属性 24 | 25 | 26 | ##锚点 27 | 之前提到过,视图的`center`属性和图层的`position`属性都指定了`anchorPoint`相对于父图层的位置。图层的`anchorPoint`通过`position`来控制它的`frame`的位置,你可以认为`anchorPoint`是用来移动图层的*把柄*。 28 | 29 | 默认来说,`anchorPoint`位于图层的中点,所以图层的将会以这个点为中心放置。`anchorPoint`属性并没有被`UIView`接口暴露出来,这也是视图的position属性被叫做“center”的原因。但是图层的`anchorPoint`可以被移动,比如你可以把它置于图层`frame`的左上角,于是图层的内容将会向右下角的`position`方向移动(图3.3),而不是居中了。 30 | 31 | 图3.3 32 | 33 | 图3.3 改变`anchorPoint`的效果 34 | 35 | 和第二章提到的`contentsRect`和`contentsCenter`属性类似,`anchorPoint`用*单位坐标*来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},因此默认坐标是{0.5, 0.5}。`anchorPoint`可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。 36 | 37 | 注意在图3.3中,当改变了`anchorPoint`,`position`属性保持固定的值并没有发生改变,但是`frame`却移动了。 38 | 39 | 那在什么场合需要改变`anchorPoint`呢?既然我们可以随意改变图层位置,那改变`anchorPoint`不会造成困惑么?为了举例说明,我们来举一个实用的例子,创建一个模拟闹钟的项目。 40 | 41 | 钟面和钟表由四张图片组成(图3.4),为了简单说明,我们还是用传统的方式来装载和加载图片,使用四个`UIImageView`实例(当然你也可以用正常的视图,设置他们图层的`contents`图片)。 42 | 43 | 图3.4 44 | 45 | 图3.4 组成钟面和钟表的四张图片 46 | 47 | 闹钟的组件通过IB来排列(图3.5),这些图片视图嵌套在一个容器视图之内,并且自动调整和自动布局都被禁用了。这是因为自动调整会影响到视图的`frame`,而根据图3.2的演示,当视图旋转的时候,`frame`是会发生改变的,这将会导致一些布局上的失灵。 48 | 49 | 我们用`NSTimer`来更新闹钟,使用视图的`transform`属性来旋转钟表(如果你对这个属性不太熟悉,不要着急,我们将会在第5章“变换”当中详细说明),具体代码见清单3.1 50 | 51 | 图3.5 52 | 53 | 图3.5 在Interface Builder中布局闹钟视图 54 | 55 | 清单3.1 **Clock** 56 | ```objective-c 57 | @interface ViewController () 58 | 59 | @property (nonatomic, weak) IBOutlet UIImageView *hourHand; 60 | @property (nonatomic, weak) IBOutlet UIImageView *minuteHand; 61 | @property (nonatomic, weak) IBOutlet UIImageView *secondHand; 62 | @property (nonatomic, weak) NSTimer *timer; 63 | 64 | @end 65 | 66 | @implementation ViewController 67 | 68 | - (void)viewDidLoad 69 | { 70 | [super viewDidLoad]; 71 | //start timer 72 | self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; 73 |  74 | //set initial hand positions 75 | [self tick]; 76 | } 77 | 78 | - (void)tick 79 | { 80 | //convert time to hours, minutes and seconds 81 | NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier:NSGregorianCalendar]; 82 | NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; 83 | NSDateComponents *components = [calendar components:units fromDate:[NSDate date]]; 84 | CGFloat hoursAngle = (components.hour / 12.0) * M_PI * 2.0; 85 | //calculate hour hand angle //calculate minute hand angle 86 | CGFloat minsAngle = (components.minute / 60.0) * M_PI * 2.0; 87 | //calculate second hand angle 88 | CGFloat secsAngle = (components.second / 60.0) * M_PI * 2.0; 89 | //rotate hands 90 | self.hourHand.transform = CGAffineTransformMakeRotation(hoursAngle); 91 | self.minuteHand.transform = CGAffineTransformMakeRotation(minsAngle); 92 | self.secondHand.transform = CGAffineTransformMakeRotation(secsAngle); 93 | } 94 | 95 | @end 96 | ``` 97 | 98 | 运行项目,看起来有点奇怪(图3.6),因为钟表的图片在围绕着中心旋转,这并不是我们期待的一个支点。 99 | 100 | 图3.6 101 | 102 | 图3.6 钟面,和不对齐的钟指针 103 | 104 | 你也许会认为可以在Interface Builder当中调整指针图片的位置来解决,但其实并不能达到目的,因为如果不放在钟面中间的话,同样不能正确的旋转。 105 | 106 | 也许在图片末尾添加一个透明空间也是个解决方案,但这样会让图片变大,也会消耗更多的内存,这样并不优雅。 107 | 108 | 更好的方案是使用`anchorPoint`属性,我们来在`-viewDidLoad`方法中添加几行代码来给每个钟指针的`anchorPoint`做一些平移(清单3.2),图3.7显示了正确的结果。 109 | 110 | 清单3.2 111 | ```objective-c 112 | - (void)viewDidLoad 113 | { 114 | [super viewDidLoad]; 115 | // adjust anchor points 116 | 117 | self.secondHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); 118 | self.minuteHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); 119 | self.hourHand.layer.anchorPoint = CGPointMake(0.5f, 0.9f); 120 | 121 | 122 | // start timer 123 | } 124 | ``` 125 | 126 | 图3.7 127 | 128 | 图3.7 钟面,和正确对齐的钟指针 129 | 130 | ##坐标系 131 | 和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的`position`依赖于它父图层的`bounds`,如果父图层发生了移动,它的所有子图层也会跟着移动。 132 | 133 | 这样对于放置图层会更加方便,因为你可以通过移动根图层来将它的子图层作为一个整体来移动,但是有时候你需要知道一个图层的*绝对*位置,或者是相对于另一个图层的位置,而不是它当前父图层的位置。 134 | 135 | `CALayer`给不同坐标系之间的图层转换提供了一些工具类方法: 136 | 137 | - (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer; 138 | - (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; 139 | - (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer; 140 | - (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer; 141 | 142 | 这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形 143 | 144 | ###翻转的几何结构 145 | 146 | 常规说来,在iOS上,一个图层的`position`位于父图层的左上角,但是在Mac OS上,通常是位于左下角。Core Animation可以通过`geometryFlipped`属性来适配这两种情况,它决定了一个图层的坐标是否相对于父图层垂直翻转,是一个`BOOL`类型。在iOS上通过设置它为`YES`意味着它的子图层将会被垂直翻转,也就是将会沿着底部排版而不是通常的顶部(它的所有子图层也同理,除非把它们的`geometryFlipped`属性也设为`YES`)。 147 | 148 | ###Z坐标轴 149 | 150 | 和`UIView`严格的二维坐标系不同,`CALayer`存在于一个三维空间当中。除了我们已经讨论过的`position`和`anchorPoint`属性之外,`CALayer`还有另外两个属性,`zPosition`和`anchorPointZ`,二者都是在Z轴上描述图层位置的浮点类型。 151 | 152 | 注意这里并没有更*深*的属性来描述由宽和高做成的`bounds`了,图层是一个完全扁平的对象,你可以把它们想象成类似于一页二维的坚硬的纸片,用胶水粘成一个空洞,就像三维结构的折纸一样。 153 | 154 | `zPosition`属性在大多数情况下其实并不常用。在第五章,我们将会涉及`CATransform3D`,你会知道如何在三维空间移动和旋转图层,除了做变换之外,`zPosition`最实用的功能就是改变图层的*显示顺序*了。 155 | 156 | 通常,图层是根据它们子图层的`sublayers`出现的顺序来类绘制的,这就是所谓的*画家的算法*--就像一个画家在墙上作画--后被绘制上的图层将会遮盖住之前的图层,但是通过增加图层的`zPosition`,就可以把图层向相机方向*前置*,于是它就在所有其他图层的*前面*了(或者至少是小于它的`zPosition`值的图层的前面)。 157 | 158 | 这里所谓的“相机”实际上是相对于用户是视角,这里和iPhone背后的内置相机没任何关系。 159 | 160 | 图3.8显示了在Interface Builder内的一对视图,正如你所见,首先出现在视图层级绿色的视图被绘制在红色视图的后面。 161 | 162 | 图3.8 163 | 164 | 图3.8 在视图层级中绿色视图被绘制在红色视图的后面 165 | 166 | 我们希望在真实的应用中也能显示出绘图的顺序,同样地,如果我们提高绿色视图的`zPosition`(清单3.3),我们会发现顺序就反了(图3.9)。其实并不需要增加太多,视图都非常地薄,所以给`zPosition`提高一个像素就可以让绿色视图前置,当然0.1或者0.0001也能够做到,但是最好不要这样,因为浮点类型四舍五入的计算可能会造成一些不便的麻烦。 167 | 168 | 清单3.3 169 | 170 | ```objective-c 171 | @interface ViewController () 172 | 173 | @property (nonatomic, weak) IBOutlet UIView *greenView; 174 | @property (nonatomic, weak) IBOutlet UIView *redView; 175 | 176 | @end 177 | 178 | @implementation ViewController 179 | 180 | - (void)viewDidLoad 181 | { 182 | [super viewDidLoad]; 183 |  184 | //move the green view zPosition nearer to the camera 185 | self.greenView.layer.zPosition = 1.0f; 186 | } 187 | @end 188 | ``` 189 | 190 | 图3.9 191 | 192 | 图3.9 绿色视图被绘制在红色视图的前面 193 | 194 | ##Hit Testing 195 | 第一章“图层树”证实了最好使用图层相关视图,而不是创建独立的图层关系。其中一个原因就是要处理额外复杂的触摸事件。 196 | 197 | `CALayer`并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:`-containsPoint:`和`-hitTest:`。 198 | 199 | ` -containsPoint: `接受一个在本图层坐标系下的`CGPoint`,如果这个点在图层`frame`范围内就返回`YES`。如清单3.4所示第一章的项目的另一个合适的版本,也就是使用`-containsPoint:`方法来判断到底是白色还是蓝色的图层被触摸了 200 | (图3.10)。这需要把触摸坐标转换成每个图层坐标系下的坐标,结果很不方便。 201 | 202 | 清单3.4 使用containsPoint判断被点击的图层 203 | 204 | ```objective-c 205 | @interface ViewController () 206 | 207 | @property (nonatomic, weak) IBOutlet UIView *layerView; 208 | @property (nonatomic, weak) CALayer *blueLayer; 209 | 210 | @end 211 | 212 | @implementation ViewController 213 | 214 | - (void)viewDidLoad 215 | { 216 | [super viewDidLoad]; 217 | //create sublayer 218 | self.blueLayer = [CALayer layer]; 219 | self.blueLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); 220 | self.blueLayer.backgroundColor = [UIColor blueColor].CGColor; 221 | //add it to our view 222 | [self.layerView.layer addSublayer:self.blueLayer]; 223 | } 224 | 225 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 226 | { 227 | //get touch position relative to main view 228 | CGPoint point = [[touches anyObject] locationInView:self.view]; 229 | //convert point to the white layer's coordinates 230 | point = [self.layerView.layer convertPoint:point fromLayer:self.view.layer]; 231 | //get layer using containsPoint: 232 | if ([self.layerView.layer containsPoint:point]) { 233 | //convert point to blueLayer’s coordinates 234 | point = [self.blueLayer convertPoint:point fromLayer:self.layerView.layer]; 235 | if ([self.blueLayer containsPoint:point]) { 236 | [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer" 237 | message:nil 238 | delegate:nil 239 | cancelButtonTitle:@"OK" 240 | otherButtonTitles:nil] show]; 241 | } else { 242 | [[[UIAlertView alloc] initWithTitle:@"Inside White Layer" 243 | message:nil 244 | delegate:nil 245 | cancelButtonTitle:@"OK" 246 | otherButtonTitles:nil] show]; 247 | } 248 | } 249 | } 250 | 251 | @end 252 | ``` 253 | 254 | 图3.10 255 | 256 | 图3.10 点击图层被正确标识 257 | 258 | `-hitTest:`方法同样接受一个`CGPoint`类型参数,而不是`BOOL`类型,它返回图层本身,或者包含这个坐标点的叶子节点图层。这意味着不再需要像使用`-containsPoint:`那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。具体使用`-hitTest:`方法被点击图层的代码如清单3.5所示。 259 | 260 | 清单3.5 使用hitTest判断被点击的图层 261 | 262 | ```objective-c 263 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 264 | { 265 | //get touch position 266 | CGPoint point = [[touches anyObject] locationInView:self.view]; 267 | //get touched layer 268 | CALayer *layer = [self.layerView.layer hitTest:point]; 269 | //get layer using hitTest 270 | if (layer == self.blueLayer) { 271 | [[[UIAlertView alloc] initWithTitle:@"Inside Blue Layer" 272 | message:nil 273 | delegate:nil 274 | cancelButtonTitle:@"OK" 275 | otherButtonTitles:nil] show]; 276 | } else if (layer == self.layerView.layer) { 277 | [[[UIAlertView alloc] initWithTitle:@"Inside White Layer" 278 | message:nil 279 | delegate:nil 280 | cancelButtonTitle:@"OK" 281 | otherButtonTitles:nil] show]; 282 | } 283 | } 284 | ``` 285 | 286 | 注意当调用图层的`-hitTest:`方法时,测算的顺序严格依赖于图层树当中的图层顺序(和UIView处理事件类似)。之前提到的`zPosition`属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。 287 | 288 | 这意味着如果改变了图层的z轴顺序,你会发现将不能够检测到最前方的视图点击事件,这是因为被另一个图层遮盖住了,虽然它的`zPosition`值较小,但是在图层树中的顺序靠前。我们将在第五章详细讨论这个问题。 289 | 290 | ##自动布局 291 | 292 | 你可能用过`UIViewAutoresizingMask`类型的一些常量,应用于当父视图改变尺寸的时候,相应`UIView`的`frame`也跟着更新的场景(通常用于横竖屏切换)。 293 | 294 | 在iOS6中,苹果介绍了*自动排版*机制,它和自动调整不同,并且更加复杂。 295 | 296 | 在Mac OS平台,`CALayer`有一个叫做`layoutManager`的属性可以通过`CALayoutManager`协议和`CAConstraintLayoutManager`类来实现自动排版的机制。但由于某些原因,这在iOS上并不适用。 297 | 298 | 当使用视图的时候,可以充分利用`UIView`类接口暴露出来的`UIViewAutoresizingMask`和`NSLayoutConstraint`API,但如果想随意控制`CALayer`的布局,就需要手工操作。最简单的方法就是使用`CALayerDelegate`如下函数: 299 | 300 | - (void)layoutSublayersOfLayer:(CALayer *)layer; 301 | 302 | 当图层的`bounds`发生改变,或者图层的`-setNeedsLayout`方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者重新调整子图层的大小,但是不能像`UIView`的`autoresizingMask`和`constraints`属性做到自适应屏幕旋转。 303 | 304 | 这也是为什么最好使用视图而不是单独的图层来构建应用程序的另一个重要原因之一。 305 | 306 | 307 | ##总结 308 | 309 | 本章涉及了`CALayer`的集合结构,包括它的`frame`,`position`和`bounds`,介绍了三维空间内图层的概念,以及如何在独立的图层内响应事件,最后简单说明了在iOS平台,Core Animation对自动调整和自动布局支持的缺乏。 310 | 311 | 在第四章“视觉效果”当中,我们接着介绍一些图层外表的特性。 312 | -------------------------------------------------------------------------------- /4-视觉效果/4-视觉效果.md: -------------------------------------------------------------------------------- 1 | #视觉效果 2 | 3 | >嗯,圆和椭圆还不错,但如果是带圆角的矩形呢? 4 | 5 | >我们现在能做到那样了么? 6 | 7 | >史蒂芬·乔布斯 8 | 9 | 我们在第三章『图层几何学』中讨论了图层的frame,第二章『寄宿图』则讨论了图层的寄宿图。但是图层不仅仅可以是图片或是颜色的容器;还有一系列内建的特性使得创造美丽优雅的令人深刻的界面元素成为可能。在这一章,我们将会探索一些能够通过使用CALayer属性实现的视觉效果。 10 | 11 | ##圆角 12 | 13 | 圆角矩形是iOS的一个标志性审美特性。这在iOS的每一个地方都得到了体现,不论是主屏幕图标,还是警告弹框,甚至是文本框。按照这流行程度,你可能会认为一定有不借助Photoshop就能轻易创建圆角矩形的方法。恭喜你,猜对了。 14 | 15 | CALayer有一个叫做`conrnerRadius`的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把`masksToBounds`设置成YES的话,图层里面的所有东西都会被截取。 16 | 17 | 我们可以通过一个简单的项目来演示这个效果。在Interface Builder中,我们放置一些视图,他们有一些子视图。而且这些子视图有一些超出了边界(如图4.1)。你可能无法看到他们超出了边界,因为在编辑界面的时候,超出的部分总是被Interface Builder裁切掉了。不过,你相信我就好了 :) 18 | 19 | ![图4.1](./4.1.png) 20 | 21 | 图4.1 两个白色的大视图,他们都包含了小一些的红色视图。 22 | 23 | 然后在代码中,我们设置角的半径为20个点,并裁剪掉第一个视图的超出部分(见清单4.1)。技术上来说,这些属性都可以在Interface Builder的探测板中分别通过『用户定义运行时属性』和勾选『裁剪子视图』(Clip Subviews)选择框来直接设置属性的值。不过,在这个示例中,代码能够表示得更清楚。图4.2是运行代码的结果 24 | 25 | 清单4.1 设置`cornerRadius`和`masksToBounds` 26 | 27 | ```objective-c 28 | @interface ViewController () 29 | 30 | @property (nonatomic, weak) IBOutlet UIView *layerView1; 31 | @property (nonatomic, weak) IBOutlet UIView *layerView2; 32 | 33 | @end 34 | 35 | @implementation ViewController 36 | - (void)viewDidLoad 37 | { 38 | [super viewDidLoad]; 39 | 40 | //set the corner radius on our layers 41 | self.layerView1.layer.cornerRadius = 20.0f; 42 | self.layerView2.layer.cornerRadius = 20.0f; 43 | 44 | //enable clipping on the second layer 45 | self.layerView2.layer.masksToBounds = YES; 46 | } 47 | @end 48 | ``` 49 | 50 | ![图4.2](./4.2.png) 51 | 52 | 右图中,红色的子视图沿角半径被裁剪了 53 | 54 | 如你所见,右边的子视图沿边界被裁剪了。 55 | 56 | 单独控制每个层的圆角曲率也不是不可能的。如果想创建有些圆角有些直角的图层或视图时,你可能需要一些不同的方法。比如使用一个图层蒙板(本章稍后会讲到)或者是CAShapeLayer(见第六章『专用图层』)。 57 | 58 | ##图层边框 59 | 60 | CALayer另外两个非常有用属性就是`borderWidth`和`borderColor`。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的`bounds`绘制,同时也包含图层的角。 61 | 62 | `borderWidth`是以点为单位的定义边框粗细的浮点数,默认为0.`borderColor`定义了边框的颜色,默认为黑色。 63 | 64 | `borderColor`是CGColorRef类型,而不是UIColor,所以它不是Cocoa的内置对象。不过呢,你肯定也清楚图层引用了`borderColor`,虽然属性声明并不能证明这一点。`CGColorRef`在引用/释放时候的行为表现得与`NSObject`极其相似。但是Objective-C语法并不支持这一做法,所以`CGColorRef`属性即便是强引用也只能通过assign关键字来声明。 65 | 66 | 边框是绘制在图层边界里面的,而且在所有子内容之前,也在子图层之前。如果我们在之前的示例中(清单4.2)加入图层的边框,你就能看到到底是怎么一回事了(如图4.3). 67 | 68 | 清单4.2 加上边框 69 | 70 | ```objective-c 71 | @implementation ViewController 72 | 73 | - (void)viewDidLoad 74 | { 75 | [super viewDidLoad]; 76 | 77 | //set the corner radius on our layers 78 | self.layerView1.layer.cornerRadius = 20.0f; 79 | self.layerView2.layer.cornerRadius = 20.0f; 80 | 81 | //add a border to our layers 82 | self.layerView1.layer.borderWidth = 5.0f; 83 | self.layerView2.layer.borderWidth = 5.0f; 84 | 85 | //enable clipping on the second layer 86 | self.layerView2.layer.masksToBounds = YES; 87 | } 88 | 89 | @end 90 | ``` 91 | 92 | ![图4.3](./4.3.png) 93 | 94 | 图4.3 给图层增加一个边框 95 | 96 | 仔细观察会发现边框并不会把寄宿图或子图层的形状计算进来,如果图层的子图层超过了边界,或者是寄宿图在透明区域有一个透明蒙板,边框仍然会沿着图层的边界绘制出来(如图4.4). 97 | 98 | ![图4.4](./4.4.png) 99 | 100 | 图4.4 边框是跟随图层的边界变化的,而不是图层里面的内容 101 | 102 | ##阴影 103 | 104 | iOS的另一个常见特性呢,就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候他们只是单纯的装饰目的。 105 | 106 | 给`shadowOpacity`属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。`shadowOpacity`是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:`shadowColor`,`shadowOffset`和`shadowRadius`。 107 | 108 | 显而易见,`shadowColor`属性控制着阴影的颜色,和`borderColor`和`backgroundColor`一样,它的类型也是`CGColorRef`。阴影默认是黑色,大多数时候你需要的阴影也是黑色的(其他颜色的阴影看起来是不是有一点点奇怪。。)。 109 | 110 | `shadowOffset`属性控制着阴影的方向和距离。它是一个`CGSize`的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。`shadowOffset`的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。 111 | 112 | 为什么要默认向上的阴影呢?尽管Core Animation是从图层套装演变而来(可以认为是为iOS创建的私有动画框架),但是呢,它却是在Mac OS上面世的,前面有提到,二者的Y轴是颠倒的。这就导致了默认的3个点位移的阴影是向上的。在Mac上,`shadowOffset`的默认值是阴影向下的,这样你就能理解为什么iOS上的阴影方向是向上的了(如图4.5). 113 | 114 | ![图4.5](./4.5.png) 115 | 116 | 图4.5 在iOS(左)和Mac OS(右)上`shadowOffset`的表现。 117 | 118 | 苹果更倾向于用户界面的阴影应该是垂直向下的,所以在iOS把阴影宽度设为0,然后高度设为一个正值不失为一个做法。 119 | 120 | `shadowRadius`属性控制着阴影的*模糊度*,当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然。苹果自家的应用设计更偏向于自然的阴影,所以一个非零值再合适不过了。 121 | 122 | 通常来讲,如果你想让视图或控件非常醒目独立于背景之外(比如弹出框遮罩层),你就应该给`shadowRadius`设置一个稍大的值。阴影越模糊,图层的深度看上去就会更明显(如图4.6). 123 | 124 | ![图4.6](./4.6.png) 125 | 126 | 图4.6 大一些的阴影位移和角半径会增加图层的深度即视感 127 | 128 | ##阴影裁剪 129 | 130 | 和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。为了计算出阴影的形状,Core Animation会将寄宿图(包括子视图,如果有的话)考虑在内,然后通过这些来完美搭配图层形状从而创建一个阴影(见图4.7)。 131 | 132 | ![图4.7](./4.7.png) 133 | 134 | 图4.7 阴影是根据寄宿图的轮廓来确定的 135 | 136 | 当阴影和裁剪扯上关系的时候就有一个头疼的限制:阴影通常就是在Layer的边界之外,如果你开启了`masksToBounds`属性,所有从图层中突出来的内容都会被才剪掉。如果我们在我们之前的边框示例项目中增加图层的阴影属性时,你就会发现问题所在(见图4.8). 137 | 138 | ![图4.8](./4.8.png) 139 | 140 | 图4.8 `maskToBounds`属性裁剪掉了阴影和内容 141 | 142 | 从技术角度来说,这个结果是可以是可以理解的,但确实又不是我们想要的效果。如果你想沿着内容裁切,你需要用到两个图层:一个只画阴影的空的外图层,和一个用`masksToBounds`裁剪内容的内图层。 143 | 144 | 如果我们把之前项目的右边用单独的视图把裁剪的视图包起来,我们就可以解决这个问题(如图4.9). 145 | 146 | ![图4.9](./4.9.png) 147 | 148 | 图4.9 右边,用额外的阴影转换视图包裹被裁剪的视图 149 | 150 | 我们只把阴影用在最外层的视图上,内层视图进行裁剪。清单4.3是代码实现,图4.10是运行结果。 151 | 152 | 清单4.3 用一个额外的视图来解决阴影裁切的问题 153 | 154 | ```objective-c 155 | @interface ViewController () 156 | 157 | @property (nonatomic, weak) IBOutlet UIView *layerView1; 158 | @property (nonatomic, weak) IBOutlet UIView *layerView2; 159 | @property (nonatomic, weak) IBOutlet UIView *shadowView; 160 | 161 | @end 162 | 163 | @implementation ViewController 164 |  165 | - (void)viewDidLoad 166 | { 167 | [super viewDidLoad]; 168 | 169 | //set the corner radius on our layers 170 | self.layerView1.layer.cornerRadius = 20.0f; 171 | self.layerView2.layer.cornerRadius = 20.0f; 172 | 173 | //add a border to our layers 174 | self.layerView1.layer.borderWidth = 5.0f; 175 | self.layerView2.layer.borderWidth = 5.0f; 176 | 177 | //add a shadow to layerView1 178 | self.layerView1.layer.shadowOpacity = 0.5f; 179 | self.layerView1.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); 180 | self.layerView1.layer.shadowRadius = 5.0f; 181 | 182 | //add same shadow to shadowView (not layerView2) 183 | self.shadowView.layer.shadowOpacity = 0.5f; 184 | self.shadowView.layer.shadowOffset = CGSizeMake(0.0f, 5.0f); 185 | self.shadowView.layer.shadowRadius = 5.0f; 186 | 187 | //enable clipping on the second layer 188 | self.layerView2.layer.masksToBounds = YES; 189 | } 190 | 191 | @end 192 | ``` 193 | 194 | ![图4.10](./4.10.png) 195 | 196 | 图4.10 右边视图,不受裁切阴影的阴影视图。 197 | 198 | ## `shadowPath`属性 199 | 200 | 我们已经知道图层阴影并不总是方的,而是从图层内容的形状继承而来。这看上去不错,但是实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。 201 | 202 | 如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个`shadowPath`来提高性能。`shadowPath`是一个`CGPathRef`类型(一个指向`CGPath`的指针)。`CGPath`是一个Core Graphics对象,用来指定任意的一个矢量图形。我们可以通过这个属性单独于图层形状之外指定阴影的形状。 203 | 204 | 图4.11 展示了同一寄宿图的不同阴影设定。如你所见,我们使用的图形很简单,但是它的阴影可以是你想要的任何形状。清单4.4是代码实现。 205 | 206 | ![图4.11](./4.11.png) 207 | 208 | 图4.11 用`shadowPath`指定任意阴影形状 209 | 210 | 清单4.4 创建简单的阴影形状 211 | 212 | ```objective-c 213 | @interface ViewController () 214 | 215 | @property (nonatomic, weak) IBOutlet UIView *layerView1; 216 | @property (nonatomic, weak) IBOutlet UIView *layerView2; 217 | @end 218 | 219 | @implementation ViewController 220 | 221 | - (void)viewDidLoad 222 | { 223 | [super viewDidLoad]; 224 | 225 | //enable layer shadows 226 | self.layerView1.layer.shadowOpacity = 0.5f; 227 | self.layerView2.layer.shadowOpacity = 0.5f; 228 | 229 | //create a square shadow 230 | CGMutablePathRef squarePath = CGPathCreateMutable(); 231 | CGPathAddRect(squarePath, NULL, self.layerView1.bounds); 232 | self.layerView1.layer.shadowPath = squarePath; CGPathRelease(squarePath); 233 | 234 | //create a circular shadow 235 | CGMutablePathRef circlePath = CGPathCreateMutable(); 236 | CGPathAddEllipseInRect(circlePath, NULL, self.layerView2.bounds); 237 | self.layerView2.layer.shadowPath = circlePath; CGPathRelease(circlePath); 238 | } 239 | @end 240 | ``` 241 | 242 | 如果是一个矩形或者是圆,用`CGPath`会相当简单明了。但是如果是更加复杂一点的图形,`UIBezierPath`类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。 243 | 244 | ##图层蒙板 245 | 246 | 通过`masksToBounds`属性,我们可以沿边界裁剪图形;通过`cornerRadius`属性,我们还可以设定一个圆角。但是有时候你希望展现的内容不是在一个矩形或圆角矩形。比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。 247 | 248 | 使用一个32位有alpha通道的png图片通常是创建一个无矩形视图最方便的方法,你可以给它指定一个透明蒙板来实现。但是这个方法不能让你以编码的方式动态地生成蒙板,也不能让子图层或子视图裁剪成同样的形状。 249 | 250 | CALayer有一个属性叫做`mask`可以解决这个问题。这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,`mask`图层定义了父图层的部分可见区域。 251 | 252 | `mask`图层的`Color`属性是无关紧要的,真正重要的是图层的轮廓。`mask`属性就像是一个饼干切割机,`mask`图层实心的部分会被保留下来,其他的则会被抛弃。(如图4.12) 253 | 254 | 如果`mask`图层比父图层要小,只有在`mask`图层里面的内容才是它关心的,除此以外的一切都会被隐藏起来。 255 | 256 | ![图4.12](./4.12.png) 257 | 258 | 图4.12 把图片和蒙板图层作用在一起的效果 259 | 260 | 我们将代码演示一下这个过程,创建一个简单的项目,通过图层的`mask`属性来作用于图片之上。为了简便一些,我们用Interface Builder来创建一个包含UIImageView的图片图层。这样我们就只要代码实现蒙板图层了。清单4.5是最终的代码,图4.13是运行后的结果。 261 | 262 | 清单4.5 应用蒙板图层 263 | 264 | ```objective-c 265 | @interface ViewController () 266 | 267 | @property (nonatomic, weak) IBOutlet UIImageView *imageView; 268 | @end 269 | 270 | @implementation ViewController 271 | 272 | - (void)viewDidLoad 273 | { 274 | [super viewDidLoad]; 275 | 276 | //create mask layer 277 | CALayer *maskLayer = [CALayer layer]; 278 | maskLayer.frame = self.imageView.bounds; 279 | UIImage *maskImage = [UIImage imageNamed:@"Cone.png"]; 280 | maskLayer.contents = (__bridge id)maskImage.CGImage; 281 | 282 | //apply mask to image layer 283 | self.imageView.layer.mask = maskLayer; 284 | } 285 | @end 286 | ``` 287 | 288 | ![图4.13](./4.13.png) 289 | 290 | 图4.13 使用了`mask`之后的UIImageView 291 | 292 | CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为`mask`属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。 293 | 294 | ##拉伸过滤 295 | 296 | 最后我们再来谈谈`minificationFilter`和`magnificationFilter`属性。总得来讲,当我们视图显示一个图片的时候,都应该正确地显示这个图片(意即:以正确的比例和正确的1:1像素显示在屏幕上)。原因如下: 297 | 298 | * 能够显示最好的画质,像素既没有被压缩也没有被拉伸。 299 | * 能更好的使用内存,因为这就是所有你要存储的东西。 300 | * 最好的性能表现,CPU不需要为此额外的计算。 301 | 302 | 不过有时候,显示一个非真实大小的图片确实是我们需要的效果。比如说一个头像或是图片的缩略图,再比如说一个可以被拖拽和伸缩的大图。这些情况下,为同一图片的不同大小存储不同的图片显得又不切实际。 303 | 304 | 当图片需要显示不同的大小的时候,有一种叫做*拉伸过滤*的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。 305 | 306 | 事实上,重绘图片大小也没有一个统一的通用算法。这取决于需要拉伸的内容,放大或是缩小的需求等这些因素。`CALayer`为此提供了三种拉伸过滤方法,他们是: 307 | 308 | * kCAFilterLinear 309 | * kCAFilterNearest 310 | * kCAFilterTrilinear 311 | 312 | minification(缩小图片)和magnification(放大图片)默认的过滤器都是`kCAFilterLinear`,这个过滤器采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。 313 | 314 | `kCAFilterTrilinear`和`kCAFilterLinear`非常相似,大部分情况下二者都看不出来有什么差别。但是,较双线性滤波算法而言,三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。 315 | 316 | 这个方法的好处在于算法能够从一系列已经接近于最终大小的图片中得到想要的结果,也就是说不要对很多像素同步取样。这不仅提高了性能,也避免了小概率因舍入错误引起的取样失灵的问题 317 | 318 | ![图4.14](./4.14.png) 319 | 320 | 图4.14 对于大图来说,双线性滤波和三线性滤波表现得更出色 321 | 322 | `kCAFilterNearest`是一种比较武断的方法。从名字不难看出,这个算法(也叫最近过滤)就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。 323 | 324 | ![图4.15](./4.15.png) 325 | 326 | 图4.15 对于没有斜线的小图来说,最近过滤算法要好很多 327 | 328 | 总的来说,对于比较小的图或者是差异特别明显,极少斜线的大图,最近过滤算法会保留这种差异明显的特质以呈现更好的结果。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差异。 329 | 330 | 让我们来实验一下。我们对第三章的时钟项目改动一下,用LCD风格的数字方式显示。我们用简单的像素字体(一种用像素构成字符的字体,而非矢量图形)创造数字显示方式,用图片存储起来,而且用第二章介绍过的拼合技术来显示(如图4.16)。 331 | 332 | ![图4.16](./4.16.png) 333 | 334 | 图4.16 一个简单的运用拼合技术显示的LCD数字风格的像素字体 335 | 336 | 我们在Interface Builder中放置了六个视图,小时、分钟、秒钟各两个,图4.17显示了这六个视图是如何在Interface Builder中放置的。如果每个都用一个淡出的outlets对象就会显得太多了,所以我们就用了一个`IBOutletCollection`对象把他们和控制器联系起来,这样我们就可以以数组的方式访问视图了。清单4.6是代码实现。 337 | 338 | 339 | ![图4.17](./4.17.png) 340 | 341 | 图4.17 在Interface Builder中放置的六个视图 342 | 343 | 清单4.6 显示一个LCD风格的时钟 344 | 345 | ```objective-c 346 | @interface ViewController () 347 | 348 | @property (nonatomic, strong) IBOutletCollection(UIView) NSArray *digitViews; 349 | @property (nonatomic, weak) NSTimer *timer; 350 |  351 | @end 352 | 353 | @implementation ViewController 354 | 355 | - (void)viewDidLoad 356 | { 357 | [super viewDidLoad]; //get spritesheet image 358 | UIImage *digits = [UIImage imageNamed:@"Digits.png"]; 359 | 360 | //set up digit views 361 | for (UIView *view in self.digitViews) { 362 | //set contents 363 | view.layer.contents = (__bridge id)digits.CGImage; 364 | view.layer.contentsRect = CGRectMake(0, 0, 0.1, 1.0); 365 | view.layer.contentsGravity = kCAGravityResizeAspect; 366 | } 367 | 368 | //start timer 369 | self.timer = [NSTimer scheduledTimerWithTimeInterval:1.0 target:self selector:@selector(tick) userInfo:nil repeats:YES]; 370 | 371 | //set initial clock time 372 | [self tick]; 373 | } 374 | 375 | - (void)setDigit:(NSInteger)digit forView:(UIView *)view 376 | { 377 | //adjust contentsRect to select correct digit 378 | view.layer.contentsRect = CGRectMake(digit * 0.1, 0, 0.1, 1.0); 379 | } 380 | 381 | - (void)tick 382 | { 383 | //convert time to hours, minutes and seconds 384 | NSCalendar *calendar = [[NSCalendar alloc] initWithCalendarIdentifier: NSGregorianCalendar]; 385 | NSUInteger units = NSHourCalendarUnit | NSMinuteCalendarUnit | NSSecondCalendarUnit; 386 |  387 | NSDateComponents *components = [calendar components:units fromDate:[NSDate date]]; 388 | 389 | //set hours 390 | [self setDigit:components.hour / 10 forView:self.digitViews[0]]; 391 | [self setDigit:components.hour % 10 forView:self.digitViews[1]]; 392 | 393 | //set minutes 394 | [self setDigit:components.minute / 10 forView:self.digitViews[2]]; 395 | [self setDigit:components.minute % 10 forView:self.digitViews[3]]; 396 | 397 | //set seconds 398 | [self setDigit:components.second / 10 forView:self.digitViews[4]]; 399 | [self setDigit:components.second % 10 forView:self.digitViews[5]]; 400 | } 401 | @end 402 | ``` 403 | 404 | 如图4.18,这样做的确起了效果,但是图片看起来模糊了。看起来默认的`kCAFilterLinear`选项让我们失望了。 405 | 406 | ![图4.18](./4.18.png) 407 | 408 | 图4.18 一个模糊的时钟,由默认的`kCAFilterLinear`引起 409 | 410 | 为了能像图4.19中那样,我们需要在for循环中加入如下代码: 411 | 412 | ```objective-c 413 | view.layer.magnificationFilter = kCAFilterNearest; 414 | ``` 415 | 416 | ![图4.19](./4.19.png) 417 | 418 | 图4.19 设置了最近过滤之后的清晰显示 419 | 420 | ##组透明 421 | 422 | UIView有一个叫做`alpha`的属性来确定视图的透明度。CALayer有一个等同的属性叫做`opacity`,这两个属性都是影响子层级的。也就是说,如果你给一个图层设置了`opacity`属性,那它的子图层都会受此影响。 423 | 424 | iOS常见的做法是把一个空间的alpha值设置为0.5(50%)以使其看上去呈现为不可用状态。对于独立的视图来说还不错,但是当一个控件有子视图的时候就有点奇怪了,图4.20展示了一个内嵌了UILabel的自定义UIButton;左边是一个不透明的按钮,右边是50%透明度的相同按钮。我们可以注意到,里面的标签的轮廓跟按钮的背景很不搭调。 425 | 426 | ![图4.20](./4.20.png) 427 | 428 | 图4.20 右边的渐隐按钮中,里面的标签清晰可见 429 | 430 | 这是由透明度的混合叠加造成的,当你显示一个50%透明度的图层时,图层的每个像素都会一般显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层本身的颜色,另外的25%则来自背景色。 431 | 432 | 在我们的示例中,按钮和表情都是白色背景。虽然他们都是50%的可见度,但是合起来的可见度是75%,所以标签所在的区域看上去就没有周围的部分那么透明。所以看上去子视图就高亮了,使得这个显示效果都糟透了。 433 | 434 | 理想状况下,当你设置了一个图层的透明度,你希望它包含的整个图层树像一个整体一样的透明效果。你可以通过设置Info.plist文件中的`UIViewGroupOpacity`为YES来达到这个效果,但是这个设置会影响到这个应用,整个app可能会受到不良影响。如果`UIViewGroupOpacity`并未设置,iOS 6和以前的版本会默认为NO(也许以后的版本会有一些改变)。 435 | 436 | 另一个方法就是,你可以设置CALayer的一个叫做`shouldRasterize`属性(见清单4.7)来实现组透明的效果,如果它被设置为YES,在应用透明度之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了(如图4.21)。 437 | 438 | 为了启用`shouldRasterize`属性,我们设置了图层的`rasterizationScale`属性。默认情况下,所有图层拉伸都是1.0, 所以如果你使用了`shouldRasterize`属性,你就要确保你设置了`rasterizationScale`属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。 439 | 440 | 当`shouldRasterize`和`UIViewGroupOpacity`一起的时候,性能问题就出现了(我们在第12章『速度』和第15章『图层性能』将做出介绍),但是性能碰撞都本地化了(译者注:这句话需要再翻译)。 441 | 442 | 清单4.7 使用`shouldRasterize`属性解决组透明问题 443 | 444 | ```objective-c 445 | @interface ViewController () 446 | @property (nonatomic, weak) IBOutlet UIView *containerView; 447 | @end 448 | 449 | @implementation ViewController 450 | 451 | - (UIButton *)customButton 452 | { 453 | //create button 454 | CGRect frame = CGRectMake(0, 0, 150, 50); 455 | UIButton *button = [[UIButton alloc] initWithFrame:frame]; 456 | button.backgroundColor = [UIColor whiteColor]; 457 | button.layer.cornerRadius = 10; 458 | 459 | //add label 460 | frame = CGRectMake(20, 10, 110, 30); 461 | UILabel *label = [[UILabel alloc] initWithFrame:frame]; 462 | label.text = @"Hello World"; 463 | label.textAlignment = NSTextAlignmentCenter; 464 | [button addSubview:label]; 465 | return button; 466 | } 467 | 468 | - (void)viewDidLoad 469 | { 470 | [super viewDidLoad]; 471 | 472 | //create opaque button 473 | UIButton *button1 = [self customButton]; 474 | button1.center = CGPointMake(50, 150); 475 | [self.containerView addSubview:button1]; 476 | 477 | //create translucent button 478 | UIButton *button2 = [self customButton]; 479 |  480 | button2.center = CGPointMake(250, 150); 481 | button2.alpha = 0.5; 482 | [self.containerView addSubview:button2]; 483 | 484 | //enable rasterization for the translucent button 485 | button2.layer.shouldRasterize = YES; 486 | button2.layer.rasterizationScale = [UIScreen mainScreen].scale; 487 | } 488 | @end 489 | ``` 490 | 491 | ![图4.12](./4.21.png) 492 | 493 | 图4.21 修正后的图 494 | 495 | ##总结 496 | 497 | 这一章介绍了一些可以通过代码应用到图层上的视觉效果,比如圆角,阴影和蒙板。我们也了解了拉伸过滤器和组透明。 498 | 499 | 在第五章,『变换』中,我们将会研究图层变化和3D转换。 500 | -------------------------------------------------------------------------------- /4-视觉效果/4.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.1.png -------------------------------------------------------------------------------- /4-视觉效果/4.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.10.png -------------------------------------------------------------------------------- /4-视觉效果/4.11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.11.png -------------------------------------------------------------------------------- /4-视觉效果/4.12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.12.png -------------------------------------------------------------------------------- /4-视觉效果/4.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.13.png -------------------------------------------------------------------------------- /4-视觉效果/4.14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.14.png -------------------------------------------------------------------------------- /4-视觉效果/4.15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.15.png -------------------------------------------------------------------------------- /4-视觉效果/4.16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.16.png -------------------------------------------------------------------------------- /4-视觉效果/4.17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.17.png -------------------------------------------------------------------------------- /4-视觉效果/4.18.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.18.png -------------------------------------------------------------------------------- /4-视觉效果/4.19.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.19.png -------------------------------------------------------------------------------- /4-视觉效果/4.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.2.png -------------------------------------------------------------------------------- /4-视觉效果/4.20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.20.png -------------------------------------------------------------------------------- /4-视觉效果/4.21.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.21.png -------------------------------------------------------------------------------- /4-视觉效果/4.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.3.png -------------------------------------------------------------------------------- /4-视觉效果/4.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.4.png -------------------------------------------------------------------------------- /4-视觉效果/4.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.5.png -------------------------------------------------------------------------------- /4-视觉效果/4.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.6.png -------------------------------------------------------------------------------- /4-视觉效果/4.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.7.png -------------------------------------------------------------------------------- /4-视觉效果/4.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.8.png -------------------------------------------------------------------------------- /4-视觉效果/4.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/4-视觉效果/4.9.png -------------------------------------------------------------------------------- /5-变换/5.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.1.jpeg -------------------------------------------------------------------------------- /5-变换/5.10.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.10.jpeg -------------------------------------------------------------------------------- /5-变换/5.11.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.11.jpeg -------------------------------------------------------------------------------- /5-变换/5.12.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.12.jpeg -------------------------------------------------------------------------------- /5-变换/5.13.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.13.jpeg -------------------------------------------------------------------------------- /5-变换/5.14.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.14.jpeg -------------------------------------------------------------------------------- /5-变换/5.15.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.15.jpeg -------------------------------------------------------------------------------- /5-变换/5.16.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.16.jpeg -------------------------------------------------------------------------------- /5-变换/5.17.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.17.jpeg -------------------------------------------------------------------------------- /5-变换/5.18.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.18.jpeg -------------------------------------------------------------------------------- /5-变换/5.19.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.19.jpeg -------------------------------------------------------------------------------- /5-变换/5.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.2.jpeg -------------------------------------------------------------------------------- /5-变换/5.20.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.20.jpeg -------------------------------------------------------------------------------- /5-变换/5.21.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.21.jpeg -------------------------------------------------------------------------------- /5-变换/5.22.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.22.jpeg -------------------------------------------------------------------------------- /5-变换/5.23.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.23.jpeg -------------------------------------------------------------------------------- /5-变换/5.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.3.jpeg -------------------------------------------------------------------------------- /5-变换/5.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.4.jpeg -------------------------------------------------------------------------------- /5-变换/5.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.5.jpeg -------------------------------------------------------------------------------- /5-变换/5.6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.6.jpeg -------------------------------------------------------------------------------- /5-变换/5.7.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.7.jpeg -------------------------------------------------------------------------------- /5-变换/5.8.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.8.jpeg -------------------------------------------------------------------------------- /5-变换/5.9.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/5-变换/5.9.jpeg -------------------------------------------------------------------------------- /5-变换/变换.md: -------------------------------------------------------------------------------- 1 | #变换 2 | 3 | >*很不幸,没人能告诉你母体是什么,你只能自己体会* -- 骇客帝国 4 | 5 | 在第四章“可视效果”中,我们研究了一些增强图层和它的内容显示效果的一些技术,在这一章中,我们将要研究可以用来对图层旋转,摆放或者扭曲的`CGAffineTransform`,以及可以将扁平物体转换成三维空间对象的`CATransform3D`(而不是仅仅对圆角矩形添加下沉阴影)。 6 | 7 | ##仿射变换 8 | 9 | 在第三章“图层几何学”中,我们使用了`UIView`的`transform`属性旋转了钟的指针,但并没有解释背后运作的原理,实际上`UIView`的`transform`属性是一个`CGAffineTransform`类型,用于在二维空间做旋转,缩放和平移。`CGAffineTransform`是一个可以和二维空间向量(例如`CGPoint`)做乘法的3X2的矩阵(见图5.1)。 10 | 11 | 图5.1 12 | 13 | 图5.1 用矩阵表示的`CGAffineTransform`和`CGPoint` 14 | 15 | 用`CGPoint`的每一列和`CGAffineTransform`矩阵的每一行对应元素相乘再求和,就形成了一个新的`CGPoint`类型的结果。要解释一下图中显示的灰色元素,为了能让矩阵做乘法,左边矩阵的列数一定要和右边矩阵的行数个数相同,所以要给矩阵填充一些标志值,使得既可以让矩阵做乘法,又不改变运算结果,并且没必要存储这些添加的值,因为它们的值不会发生变化,但是要用来做运算。 16 | 17 | 因此,通常会用3×3(而不是2×3)的矩阵来做二维变换,你可能会见到3行2列格式的矩阵,这是所谓的以列为主的格式,图5.1所示的是以行为主的格式,只要能保持一致,用哪种格式都无所谓。 18 | 19 | 当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。`CGAffineTransform`中的“仿射”的意思是无论变换矩阵用什么值,图层中平行的两条线在变换之后任然保持平行,`CGAffineTransform`可以做出任意符合上述标注的变换,图5.2显示了一些仿射的和非仿射的变换: 20 | 21 | 图5.2 22 | 23 | 图5.2 仿射和非仿射变换 24 | 25 | ###创建一个`CGAffineTransform` 26 | 27 | 对矩阵数学做一个全面的阐述就超出本书的讨论范围了,不过如果你对矩阵完全不熟悉的话,矩阵变换可能会使你感到畏惧。幸运的是,Core Graphics提供了一系列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个`CGAffineTransform`实例: 28 | 29 | CGAffineTransformMakeRotation(CGFloat angle) 30 | CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) 31 | CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty) 32 | 33 | 旋转和缩放变换都可以很好解释--分别旋转或者缩放一个向量的值。平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。 34 | 35 | 我们用一个很简单的项目来做个demo,把一个原始视图旋转45度角度(图5.3) 36 | 37 | 图5.3 38 | 39 | 图5.3 使用仿射变换旋转45度角之后的视图 40 | 41 | `UIView`可以通过设置`transform`属性做变换,但实际上它只是封装了内部图层的变换。 42 | 43 | `CALayer`同样也有一个`transform`属性,但它的类型是`CATransform3D`,而不是`CGAffineTransform`,本章后续将会详细解释。`CALayer`对应于`UIView`的`transform`属性叫做`affineTransform`,清单5.1的例子就是使用`affineTransform`对图层做了45度顺时针旋转。 44 | 45 | 清单5.1 使用`affineTransform`对图层旋转45度 46 | ```objective-c 47 | @interface ViewController () 48 | 49 | @property (nonatomic, weak) IBOutlet UIView *layerView; 50 | 51 | @end 52 | 53 | @implementation ViewController 54 | 55 | - (void)viewDidLoad 56 | { 57 | [super viewDidLoad]; 58 | //rotate the layer 45 degrees 59 | CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4); 60 | self.layerView.layer.affineTransform = transform; 61 | } 62 | 63 | @end 64 | ``` 65 | 66 | 注意我们使用的旋转常量是`M_PI_4`,而不是你想象的45,因为iOS的变换函数使用弧度而不是角度作为单位。弧度用数学常量pi的倍数表示,一个pi代表180度,所以四分之一的pi就是45度。 67 | 68 | C的数学函数库(iOS会自动引入)提供了pi的一些简便的换算,`M_PI_4`于是就是pi的四分之一,如果对换算不太清楚的话,可以用如下的宏做换算: 69 | 70 | #define RADIANS_TO_DEGREES(x) ((x)/M_PI*180.0) 71 | #define DEGREES_TO_RADIANS(x) ((x)/180.0*M_PI) 72 | 73 | 74 | ###混合变换 75 | 76 | Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个既要*缩放*又要*旋转*的变换,这就会非常有用了。例如下面几个函数: 77 | 78 | CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) 79 | CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) 80 | CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty) 81 | 82 | 当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一个`CGAffineTransform`类型的空值,矩阵论中称作*单位矩阵*,Core Graphics同样也提供了一个方便的常量: 83 | 84 | CGAffineTransformIdentity 85 | 86 | 最后,如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换: 87 | 88 | CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2); 89 | 90 | 我们来用这些函数组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右移动200个像素(清单5.2)。图5.4显示了图层变换最后的结果。 91 | 92 | 清单5.2 使用若干方法创建一个复合变换 93 | 94 | ```objective-c 95 | - (void)viewDidLoad 96 | { 97 | [super viewDidLoad]; 98 | CGAffineTransform transform = CGAffineTransformIdentity; //create a new transform 99 | transform = CGAffineTransformScale(transform, 0.5, 0.5); //scale by 50% 100 | transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0); //rotate by 30 degrees 101 | transform = CGAffineTransformTranslate(transform, 200, 0); //translate by 200 points 102 | //apply transform to layer 103 | self.layerView.layer.affineTransform = transform; 104 | } 105 | ``` 106 | 107 | 图5.4 108 | 109 | 图5.4 顺序应用多个仿射变换之后的结果 110 | 111 | 图5.4中有些需要注意的地方:图片向右边发生了平移,但并没有指定距离那么远(200像素),另外它还有点向下发生了平移。原因在于当你按顺序做了变换,上一个变换的结果将会影响之后的变换,所以200像素的向右平移同样也被旋转了30度,缩小了50%,所以它实际上是斜向移动了100像素。 112 | 113 | 这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。 114 | 115 | ###剪切变换 116 | 117 | Core Graphics为你提供了计算变换矩阵的一些方法,所以很少需要直接设置`CGAffineTransform`的值。除非需要创建一个*斜切*的变换,Core Graphics并没有提供直接的函数。 118 | 119 | 斜切变换是放射变换的第四种类型,较于平移,旋转和缩放并不常用(这也是Core Graphics没有提供相应函数的原因),但有些时候也会很有用。我们用一张图片可以很直接的说明效果(图5.5)。也许用“倾斜”描述更加恰当,具体做变换的代码见清单5.3。 120 | 121 | 图5.5 122 | 123 | 图5.5 水平方向的斜切变换 124 | 125 | 清单5.3 实现一个斜切变换 126 | 127 | ```objective-c 128 | @implementation ViewController 129 | 130 | CGAffineTransform CGAffineTransformMakeShear(CGFloat x, CGFloat y) 131 | { 132 | CGAffineTransform transform = CGAffineTransformIdentity; 133 | transform.c = -x; 134 | transform.b = y; 135 | return transform; 136 | } 137 | 138 | - (void)viewDidLoad 139 | { 140 | [super viewDidLoad]; 141 | //shear the layer at a 45-degree angle 142 | self.layerView.layer.affineTransform = CGAffineTransformMakeShear(1, 0); 143 | } 144 | 145 | @end 146 | ``` 147 | 148 | ##3D变换 149 | 150 | CG的前缀告诉我们,`CGAffineTransform`类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且`CGAffineTransform`仅仅对2D变换有效。 151 | 152 | 在第三章中,我们提到了`zPosition`属性,可以用来让图层靠近或者远离相机(用户视角),`transform`属性(`CATransform3D`类型)可以真正做到这点,即让图层在3D空间内移动或者旋转。 153 | 154 | 和`CGAffineTransform`类似,`CATransform3D`也是一个矩阵,但是和2x3的矩阵不同,`CATransform3D`是一个可以在3维空间内做变换的4x4的矩阵(图5.6)。 155 | 156 | 图5.6 157 | 158 | 图5.6 对一个3D像素点做`CATransform3D`矩阵变换 159 | 160 | 和`CGAffineTransform`矩阵类似,Core Animation提供了一系列的方法用来创建和组合`CATransform3D`类型的矩阵,和Core Graphics的函数类似,但是3D的平移和旋转多处了一个`z`参数,并且旋转函数除了`angle`之外多出了`x`,`y`,`z`三个参数,分别决定了每个坐标轴方向上的旋转: 161 | 162 | CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z) 163 | CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) 164 | CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz) 165 | 166 | 你应该对X轴和Y轴比较熟悉了,分别以右和下为正方向(回忆第三章,这是iOS上的标准结构,在Mac OS,Y轴朝上为正方向),Z轴和这两个轴分别垂直,指向视角外为正方向(图5.7)。 167 | 168 | 图5.7 169 | 170 | 图5.7 X,Y,Z轴,以及围绕它们旋转的方向 171 | 172 | 由图所见,绕Z轴的旋转等同于之前二维空间的仿射旋转,但是绕X轴和Y轴的旋转就突破了屏幕的二维空间,并且在用户视角看来发生了倾斜。 173 | 174 | 举个例子:清单5.4的代码使用了`CATransform3DMakeRotation`对视图内的图层绕Y轴做了45度角的旋转,我们可以把视图向右倾斜,这样会看得更清晰。 175 | 176 | 结果见图5.8,但并不像我们期待的那样。 177 | 178 | 清单5.4 绕Y轴旋转图层 179 | 180 | ```objective-c 181 | @implementation ViewController 182 | 183 | - (void)viewDidLoad 184 | { 185 | [super viewDidLoad]; 186 | //rotate the layer 45 degrees along the Y axis 187 | CATransform3D transform = CATransform3DMakeRotation(M_PI_4, 0, 1, 0); 188 | self.layerView.layer.transform = transform; 189 | } 190 | 191 | @end 192 | ``` 193 | 194 | 图5.8 195 | 196 | 图5.8 绕y轴旋转45度的视图 197 | 198 | 看起来图层并没有被旋转,而是仅仅在水平方向上的一个压缩,是哪里出了问题呢? 199 | 200 | 其实完全没错,视图看起来更窄实际上是因为我们在用一个斜向的视角看它,而不是*透视*。 201 | 202 | ###透视投影 203 | 204 | 在真实世界中,当物体远离我们的时候,由于视角的原因看起来会变小,理论上说远离我们的视图的边要比靠近视角的边跟短,但实际上并没有发生,而我们当前的视角是等距离的,也就是在3D变换中任然保持平行,和之前提到的仿射变换类似。 205 | 206 | 在等距投影中,远处的物体和近处的物体保持同样的缩放比例,这种投影也有它自己的用处(例如建筑绘图,颠倒,和伪3D视频),但当前我们并不需要。 207 | 208 | 为了做一些修正,我们需要引入*投影变换*(又称作*z变换*)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,幸运的是,很简单: 209 | 210 | `CATransform3D`的透视效果通过一个矩阵中一个很简单的元素来控制:`m34`。`m34`(图5.9)用于按比例缩放X和Y的值来计算到底要离视角多远。 211 | 212 | 图5.9 213 | 214 | 图5.9 `CATransform3D`的`m34`元素,用来做透视 215 | 216 | `m34`的默认值是0,我们可以通过设置`m34`为-1.0 / `d`来应用透视效果,`d`代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。 217 | 218 | 因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它的放置的位置。通常500-1000就已经很好了,但对于特定的图层有时候更小或者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果,对视图应用透视的代码见清单5.5,结果见图5.10。 219 | 220 | 清单5.5 对变换应用透视效果 221 | 222 | ```objective-c 223 | @implementation ViewController 224 | 225 | - (void)viewDidLoad 226 | { 227 | [super viewDidLoad]; 228 | //create a new transform 229 | CATransform3D transform = CATransform3DIdentity; 230 | //apply perspective 231 | transform.m34 = - 1.0 / 500.0; 232 | //rotate by 45 degrees along the Y axis 233 | transform = CATransform3DRotate(transform, M_PI_4, 0, 1, 0); 234 | //apply to layer 235 | self.layerView.layer.transform = transform; 236 | } 237 | 238 | @end 239 | ``` 240 | 241 | 图5.10 242 | 243 | 图5.10 应用透视效果之后再次对图层做旋转 244 | 245 | ###灭点 246 | 247 | 当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。 248 | 249 | 在现实中,这个点通常是视图的中心(图5.11),于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D对象的视图中点。 250 | 251 | 图5.11 252 | 253 | 图5.11 灭点 254 | 255 | Core Animation定义了这个点位于变换图层的`anchorPoint`(通常位于图层中心,但也有例外,见第三章)。这就是说,当图层发生变换时,这个点永远位于图层变换之前`anchorPoint`的位置。 256 | 257 | 当改变一个图层的`position`,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整`m34`来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的`position`),这样所有的3D图层都共享一个灭点。 258 | 259 | ###`sublayerTransform`属性 260 | 261 | 如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个`position`,如果用一个函数封装这些操作的确会更加方便,但仍然有限制(例如,你不能在Interface Builder中摆放视图),这里有一个更好的方法。 262 | 263 | `CALayer`有一个属性叫做`sublayerTransform`。它也是`CATransform3D`类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。 264 | 265 | 相较而言,通过在一个地方设置透视变换会很方便,同时它会带来另一个显著的优势:灭点被设置在*容器图层*的中点,从而不需要再对子图层分别设置了。这意味着你可以随意使用`position`和`frame`来放置子图层,而不需要把它们放置在屏幕中点,然后为了保证统一的灭点用变换来做平移。 266 | 267 | 我们来用一个demo举例说明。这里用Interface Builder并排放置两个视图(图5.12),然后通过设置它们容器视图的透视变换,我们可以保证它们有相同的透视和灭点,代码见清单5.6,结果见图5.13。 268 | 269 | 图5.12 270 | 271 | 图5.12 在一个视图容器内并排放置两个视图 272 | 273 | 清单5.6 应用`sublayerTransform` 274 | 275 | ```objective-c 276 | @interface ViewController () 277 | 278 | @property (nonatomic, weak) IBOutlet UIView *containerView; 279 | @property (nonatomic, weak) IBOutlet UIView *layerView1; 280 | @property (nonatomic, weak) IBOutlet UIView *layerView2; 281 | 282 | @end 283 | 284 | @implementation ViewController 285 | 286 | - (void)viewDidLoad 287 | { 288 | [super viewDidLoad]; 289 | //apply perspective transform to container 290 | CATransform3D perspective = CATransform3DIdentity; 291 | perspective.m34 = - 1.0 / 500.0; 292 | self.containerView.layer.sublayerTransform = perspective; 293 | //rotate layerView1 by 45 degrees along the Y axis 294 | CATransform3D transform1 = CATransform3DMakeRotation(M_PI_4, 0, 1, 0); 295 | self.layerView1.layer.transform = transform1; 296 | //rotate layerView2 by 45 degrees along the Y axis 297 | CATransform3D transform2 = CATransform3DMakeRotation(-M_PI_4, 0, 1, 0); 298 | self.layerView2.layer.transform = transform2; 299 | } 300 | ``` 301 | 302 | 图5.13 303 | 304 | 图5.13 通过相同的透视效果分别对视图做变换 305 | 306 | ###背面 307 | 308 | 我们既然可以在3D场景下旋转图层,那么也可以从*背面*去观察它。如果我们在清单5.4中把角度修改为`M_PI`(180度)而不是当前的` M_PI_4`(45度),那么将会把图层完全旋转一个半圈,于是完全背对了相机视角。 309 | 310 | 那么从背部看图层是什么样的呢,见图5.14 311 | 312 | 图5.14 313 | 314 | 图5.14 视图的背面,一个镜像对称的图片 315 | 316 | 如你所见,图层是双面绘制的,反面显示的是正面的一个镜像图片。 317 | 318 | 但这并不是一个很好的特性,因为如果图层包含文本或者其他控件,那用户看到这些内容的镜像图片当然会感到困惑。另外也有可能造成资源的浪费:想象用这些图层形成一个不透明的固态立方体,既然永远都看不见这些图层的背面,那为什么浪费GPU来绘制它们呢? 319 | 320 | `CALayer`有一个叫做`doubleSided`的属性来控制图层的背面是否要被绘制。这是一个`BOOL`类型,默认为`YES`,如果设置为`NO`,那么当图层正面从相机视角消失的时候,它将不会被绘制。 321 | 322 | ###扁平化图层 323 | 324 | 如果对包含已经做过变换的图层的图层做反方向的变换将会发什么什么呢?是不是有点困惑?见图5.15 325 | 326 | 图5.15 327 | 328 | 图5.15 反方向变换的嵌套图层 329 | 330 | 注意做了-45度旋转的内部图层是怎样抵消旋转45度的图层,从而恢复正常状态的。 331 | 332 | 如果内部图层相对外部图层做了相反的变换(这里是绕Z轴的旋转),那么按照逻辑这两个变换将被相互抵消。 333 | 334 | 验证一下,相应代码见清单5.7,结果见5.16 335 | 336 | 清单5.7 绕Z轴做相反的旋转变换 337 | 338 | ```objective-c 339 | @interface ViewController () 340 | 341 | @property (nonatomic, weak) IBOutlet UIView *outerView; 342 | @property (nonatomic, weak) IBOutlet UIView *innerView; 343 | 344 | @end 345 | 346 | @implementation ViewController 347 | 348 | - (void)viewDidLoad 349 | { 350 | [super viewDidLoad]; 351 | //rotate the outer layer 45 degrees 352 | CATransform3D outer = CATransform3DMakeRotation(M_PI_4, 0, 0, 1); 353 | self.outerView.layer.transform = outer; 354 | //rotate the inner layer -45 degrees 355 | CATransform3D inner = CATransform3DMakeRotation(-M_PI_4, 0, 0, 1); 356 | self.innerView.layer.transform = inner; 357 | } 358 | 359 | @end 360 | ``` 361 | 362 | 图5.16 363 | 364 | 图5.16 旋转后的视图 365 | 366 | 运行结果和我们预期的一致。现在在3D情况下再试一次。修改代码,让内外两个视图绕Y轴旋转而不是Z轴,再加上透视效果,以便我们观察。注意不能用`sublayerTransform`属性,因为内部的图层并不直接是容器图层的子图层,所以这里分别对图层设置透视变换(清单5.8)。 367 | 368 | 清单5.8 绕Y轴相反的旋转变换 369 | 370 | ```objective-c 371 | - (void)viewDidLoad 372 | { 373 | [super viewDidLoad]; 374 | //rotate the outer layer 45 degrees 375 | CATransform3D outer = CATransform3DIdentity; 376 | outer.m34 = -1.0 / 500.0; 377 | outer = CATransform3DRotate(outer, M_PI_4, 0, 1, 0); 378 | self.outerView.layer.transform = outer; 379 | //rotate the inner layer -45 degrees 380 | CATransform3D inner = CATransform3DIdentity; 381 | inner.m34 = -1.0 / 500.0; 382 | inner = CATransform3DRotate(inner, -M_PI_4, 0, 1, 0); 383 | self.innerView.layer.transform = inner; 384 | } 385 | ``` 386 | 387 | 预期的效果应该如图5.17所示。 388 | 389 | 图5.17 390 | 391 | 图5.17 绕Y轴做相反旋转的预期结果。 392 | 393 | 但其实这并不是我们所看到的,相反,我们看到的结果如图5.18所示。发什么了什么呢?内部的图层仍然向左侧旋转,并且发生了扭曲,但按道理说它应该保持正面朝上,并且显示正常的方块。 394 | 395 | 这是由于尽管Core Animation图层存在于3D空间之内,但它们并不都存在*同一个*3D空间。每个图层的3D场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。 396 | 397 | 图5.18 398 | 399 | 图5.18 绕Y轴做相反旋转的真实结果 400 | 401 | 类似的,当你在玩一个3D游戏,实际上仅仅是把屏幕做了一次倾斜,或许在游戏中可以看见有一面墙在你面前,但是倾斜屏幕并不能够看见墙里面的东西。所有场景里面绘制的东西并不会随着你观察它的角度改变而发生变化;图层也是同样的道理。 402 | 403 | 这使得用Core Animation创建非常复杂的3D场景变得十分困难。你不能够使用图层树去创建一个3D结构的层级关系--在相同场景下的任何3D表面必须和同样的图层保持一致,这是因为每个的父视图都把它的子视图扁平化了。 404 | 405 | 至少当你用正常的`CALayer`的时候是这样,`CALayer`有一个叫做`CATransformLayer`的子类来解决这个问题。具体在第六章“特殊的图层”中将会具体讨论。 406 | 407 | ##固体对象 408 | 409 | 现在你懂得了在3D空间的一些图层布局的基础,我们来试着创建一个固态的3D对象(实际上是一个技术上所谓的*空洞*对象,但它以固态呈现)。我们用六个独立的视图来构建一个立方体的各个面。 410 | 411 | 在这个例子中,我们用Interface Builder来构建立方体的面(图5.19),我们当然可以用代码来写,但是用Interface Builder的好处是可以方便的在每一个面上添加子视图。记住这些面仅仅是包含视图和控件的普通的用户界面元素,它们完全是我们界面交互的部分,并且当把它折成一个立方体之后也不会改变这个性质。 412 | 413 | 图5.19 414 | 415 | 图5.19 用Interface Builder对立方体的六个面进行布局 416 | 417 | 这些面视图并没有放置在主视图当中,而是松散地排列在根nib文件里面。我们并不关心在这个容器中如何摆放它们的位置,因为后续将会用图层的`transform`对它们进行重新布局,并且用Interface Builder在容器视图之外摆放他们可以让我们容易看清楚它们的内容,如果把它们一个叠着一个都塞进主视图,将会变得很难看。 418 | 419 | 我们把一个有颜色的`UILabel`放置在视图内部,是为了清楚的辨别它们之间的关系,并且`UIButton`被放置在第三个面视图里面,后面会做简单的解释。 420 | 421 | 具体把视图组织成立方体的代码见清单5.9,结果见图5.20 422 | 423 | 清单5.9 创建一个立方体 424 | 425 | ```objective-c 426 | @interface ViewController () 427 | 428 | @property (nonatomic, weak) IBOutlet UIView *containerView; 429 | @property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces; 430 | 431 | @end 432 | 433 | @implementation ViewController 434 | 435 | - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform 436 | { 437 | //get the face view and add it to the container 438 | UIView *face = self.faces[index]; 439 | [self.containerView addSubview:face]; 440 | //center the face view within the container 441 | CGSize containerSize = self.containerView.bounds.size; 442 | face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0); 443 | // apply the transform 444 | face.layer.transform = transform; 445 | } 446 | 447 | - (void)viewDidLoad 448 | { 449 | [super viewDidLoad]; 450 | //set up the container sublayer transform 451 | CATransform3D perspective = CATransform3DIdentity; 452 | perspective.m34 = -1.0 / 500.0; 453 | self.containerView.layer.sublayerTransform = perspective; 454 | //add cube face 1 455 | CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100); 456 | [self addFace:0 withTransform:transform]; 457 | //add cube face 2 458 | transform = CATransform3DMakeTranslation(100, 0, 0); 459 | transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); 460 | [self addFace:1 withTransform:transform]; 461 | //add cube face 3 462 | transform = CATransform3DMakeTranslation(0, -100, 0); 463 | transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0); 464 | [self addFace:2 withTransform:transform]; 465 | //add cube face 4 466 | transform = CATransform3DMakeTranslation(0, 100, 0); 467 | transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0); 468 | [self addFace:3 withTransform:transform]; 469 | //add cube face 5 470 | transform = CATransform3DMakeTranslation(-100, 0, 0); 471 | transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0); 472 | [self addFace:4 withTransform:transform]; 473 | //add cube face 6 474 | transform = CATransform3DMakeTranslation(0, 0, -100); 475 | transform = CATransform3DRotate(transform, M_PI, 0, 1, 0); 476 | [self addFace:5 withTransform:transform]; 477 | } 478 | 479 | @end 480 | ``` 481 | 482 | 图5.20 483 | 484 | 图5.20 正面朝上的立方体 485 | 486 | 从这个角度看立方体并不是很明显;看起来只是一个方块,为了更好地欣赏它,我们将更换一个*不同的视角*。 487 | 488 | 旋转这个立方体将会显得很笨重,因为我们要单独对每个面做旋转。另一个简单的方案是通过调整容器视图的`sublayerTransform`去旋转*照相机*。 489 | 490 | 添加如下几行去旋转`containerView`图层的`perspective`变换矩阵: 491 | 492 | perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); 493 | perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0); 494 | 495 | 这就对相机(或者相对相机的整个场景,你也可以这么认为)绕Y轴旋转45度,并且绕X轴旋转45度。现在从另一个角度去观察立方体,就能看出它的真实面貌(图5.21)。 496 | 497 | 图5.21 498 | 499 | 图5.21 从一个边角观察的立方体 500 | 501 | ###光亮和阴影 502 | 503 | 现在它看起来更像是一个立方体没错了,但是对每个面之间的连接还是很难分辨。Core Animation可以用3D显示图层,但是它对*光线*并没有概念。如果想让立方体看起来更加真实,需要自己做一个阴影效果。你可以通过改变每个面的背景颜色或者直接用带光亮效果的图片来调整。 504 | 505 | 如果需要*动态*地创建光线效果,你可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但为了计算阴影图层的不透明度,你需要得到每个面的*正太向量*(垂直于表面的向量),然后根据一个想象的光源计算出两个向量*叉乘*结果。叉乘代表了光源和图层之间的角度,从而决定了它有多大程度上的光亮。 506 | 507 | 清单5.10实现了这样一个结果,我们用GLKit框架来做向量的计算(你需要引入GLKit库来运行代码),每个面的`CATransform3D`都被转换成`GLKMatrix4`,然后通过`GLKMatrix4GetMatrix3`函数得出一个3×3的*旋转矩阵*。这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。 508 | 509 | 结果如图5.22所示,试着调整`LIGHT_DIRECTION`和`AMBIENT_LIGHT`的值来切换光线效果 510 | 511 | 清单5.10 对立方体的表面应用动态的光线效果 512 | 513 | ```objective-c 514 | #import "ViewController.h" 515 | #import 516 | #import 517 | 518 | #define LIGHT_DIRECTION 0, 1, -0.5 519 | #define AMBIENT_LIGHT 0.5 520 | 521 | @interface ViewController () 522 | 523 | @property (nonatomic, weak) IBOutlet UIView *containerView; 524 | @property (nonatomic, strong) IBOutletCollection(UIView) NSArray *faces; 525 | 526 | @end 527 | 528 | @implementation ViewController 529 | 530 | - (void)applyLightingToFace:(CALayer *)face 531 | { 532 | //add lighting layer 533 | CALayer *layer = [CALayer layer]; 534 | layer.frame = face.bounds; 535 | [face addSublayer:layer]; 536 | //convert the face transform to matrix 537 | //(GLKMatrix4 has the same structure as CATransform3D) 538 | //译者注:GLKMatrix4和CATransform3D内存结构一致,但坐标类型有长度区别,所以理论上应该做一次float到CGFloat的转换,感谢[@zihuyishi](https://github.com/zihuyishi)同学~ 539 | CATransform3D transform = face.transform; 540 | GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform; 541 | GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4); 542 | //get face normal 543 | GLKVector3 normal = GLKVector3Make(0, 0, 1); 544 | normal = GLKMatrix3MultiplyVector3(matrix3, normal); 545 | normal = GLKVector3Normalize(normal); 546 | //get dot product with light direction 547 | GLKVector3 light = GLKVector3Normalize(GLKVector3Make(LIGHT_DIRECTION)); 548 | float dotProduct = GLKVector3DotProduct(light, normal); 549 | //set lighting layer opacity 550 | CGFloat shadow = 1 + dotProduct - AMBIENT_LIGHT; 551 | UIColor *color = [UIColor colorWithWhite:0 alpha:shadow]; 552 | layer.backgroundColor = color.CGColor; 553 | } 554 | 555 | - (void)addFace:(NSInteger)index withTransform:(CATransform3D)transform 556 | { 557 | //get the face view and add it to the container 558 | UIView *face = self.faces[index]; 559 | [self.containerView addSubview:face]; 560 | //center the face view within the container 561 | CGSize containerSize = self.containerView.bounds.size; 562 | face.center = CGPointMake(containerSize.width / 2.0, containerSize.height / 2.0); 563 | // apply the transform 564 | face.layer.transform = transform; 565 | //apply lighting 566 | [self applyLightingToFace:face.layer]; 567 | } 568 | 569 | - (void)viewDidLoad 570 | { 571 | [super viewDidLoad]; 572 | //set up the container sublayer transform 573 | CATransform3D perspective = CATransform3DIdentity; 574 | perspective.m34 = -1.0 / 500.0; 575 | perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); 576 | perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0); 577 | self.containerView.layer.sublayerTransform = perspective; 578 | //add cube face 1 579 | CATransform3D transform = CATransform3DMakeTranslation(0, 0, 100); 580 | [self addFace:0 withTransform:transform]; 581 | //add cube face 2 582 | transform = CATransform3DMakeTranslation(100, 0, 0); 583 | transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); 584 | [self addFace:1 withTransform:transform]; 585 | //add cube face 3 586 | transform = CATransform3DMakeTranslation(0, -100, 0); 587 | transform = CATransform3DRotate(transform, M_PI_2, 1, 0, 0); 588 | [self addFace:2 withTransform:transform]; 589 | //add cube face 4 590 | transform = CATransform3DMakeTranslation(0, 100, 0); 591 | transform = CATransform3DRotate(transform, -M_PI_2, 1, 0, 0); 592 | [self addFace:3 withTransform:transform]; 593 | //add cube face 5 594 | transform = CATransform3DMakeTranslation(-100, 0, 0); 595 | transform = CATransform3DRotate(transform, -M_PI_2, 0, 1, 0); 596 | [self addFace:4 withTransform:transform]; 597 | //add cube face 6 598 | transform = CATransform3DMakeTranslation(0, 0, -100); 599 | transform = CATransform3DRotate(transform, M_PI, 0, 1, 0); 600 | [self addFace:5 withTransform:transform]; 601 | } 602 | 603 | @end 604 | ``` 605 | 606 | 图5.22 607 | 608 | 图5.22 动态计算光线效果之后的立方体 609 | 610 | ###点击事件 611 | 612 | 你应该能注意到现在可以在第三个表面的顶部看见按钮了,点击它,什么都没发生,为什么呢? 613 | 614 | 这并不是因为iOS在3D场景下正确地处理响应事件,实际上是可以做到的。问题在于*视图顺序*。在第三章中我们简要提到过,点击事件的处理由视图在父视图中的顺序决定的,并不是3D空间中的Z轴顺序。当给立方体添加视图的时候,我们实际上是按照一个顺序添加,所以按照视图/图层顺序来说,4,5,6在3的前面。 615 | 616 | 即使我们看不见4,5,6的表面(因为被1,2,3遮住了),iOS在事件响应上仍然保持之前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体一样。 617 | 618 | 你也许认为把`doubleSided`设置成`NO`可以解决这个问题,因为它不再渲染视图后面的内容,但实际上并不起作用。因为背对相机而隐藏的视图仍然会响应点击事件(这和通过设置`hidden`属性或者设置`alpha`为0而隐藏的视图不同,那两种方式将不会响应事件)。所以即使禁止了双面渲染仍然不能解决这个问题(虽然由于性能问题,还是需要把它设置成`NO`)。 619 | 620 | 这里有几种正确的方案:把除了表面3的其他视图`userInteractionEnabled`属性都设置成`NO`来禁止事件传递。或者简单通过代码把视图3覆盖在视图6上。无论怎样都可以点击按钮了(图5.23)。 621 | 622 | 图5.23 623 | 624 | 图5.23 背景视图不再阻碍按钮,我们可以点击它了 625 | 626 | 627 | ##总结 628 | 629 | 这一章涉及了一些2D和3D的变换。你学习了一些矩阵计算的基础,以及如何用Core Animation创建3D场景。你看到了图层背后到底是如何呈现的,并且知道了不能把扁平的图片做成真实的立体效果,最后我们用demo说明了触摸事件的处理,视图中图层添加的层级顺序会比屏幕上显示的顺序更有意义。 630 | 631 | 第六章我们会研究一些Core Animation提供不同功能的具体的`CALayer`子类。 632 | -------------------------------------------------------------------------------- /6-专有图层/6.1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.1.png -------------------------------------------------------------------------------- /6-专有图层/6.10.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.10.png -------------------------------------------------------------------------------- /6-专有图层/6.11.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.11.png -------------------------------------------------------------------------------- /6-专有图层/6.12.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.12.png -------------------------------------------------------------------------------- /6-专有图层/6.13.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.13.png -------------------------------------------------------------------------------- /6-专有图层/6.14.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.14.png -------------------------------------------------------------------------------- /6-专有图层/6.15.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.15.png -------------------------------------------------------------------------------- /6-专有图层/6.16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.16.png -------------------------------------------------------------------------------- /6-专有图层/6.17.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.17.png -------------------------------------------------------------------------------- /6-专有图层/6.2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.2.png -------------------------------------------------------------------------------- /6-专有图层/6.3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.3.png -------------------------------------------------------------------------------- /6-专有图层/6.4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.4.png -------------------------------------------------------------------------------- /6-专有图层/6.5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.5.png -------------------------------------------------------------------------------- /6-专有图层/6.6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.6.png -------------------------------------------------------------------------------- /6-专有图层/6.7.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.7.png -------------------------------------------------------------------------------- /6-专有图层/6.8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.8.png -------------------------------------------------------------------------------- /6-专有图层/6.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/6-专有图层/6.9.png -------------------------------------------------------------------------------- /7-隐式动画/7.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/7-隐式动画/7.1.jpeg -------------------------------------------------------------------------------- /7-隐式动画/7.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/7-隐式动画/7.2.jpeg -------------------------------------------------------------------------------- /7-隐式动画/7.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/7-隐式动画/7.3.jpeg -------------------------------------------------------------------------------- /7-隐式动画/7.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/7-隐式动画/7.4.jpeg -------------------------------------------------------------------------------- /7-隐式动画/隐式动画.md: -------------------------------------------------------------------------------- 1 | #隐式动画 2 | 3 | >*按照我的意思去做,而不是我说的。* -- 埃德娜,辛普森 4 | 5 | 我们在第一部分讨论了Core Animation除了动画之外可以做到的任何事情。但是动画是Core Animation库一个非常显著的特性。这一章我们来看看它是怎么做到的。具体来说,我们先来讨论框架自动完成的*隐式动画*(除非你明确禁用了这个功能)。 6 | 7 | ##事务 8 | 9 | Core Animation基于一个假设,说屏幕上的任何东西都可以(或者可能)做动画。动画并不需要你在Core Animation中手动打开,相反需要明确地关闭,否则他会一直存在。 10 | 11 | 当你改变`CALayer`的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。 12 | 13 | 这看起来这太棒了,似乎不太真实,我们来用一个demo解释一下:首先和第一章“图层树”一样创建一个蓝色的方块,然后添加一个按钮,随机改变它的颜色。代码见清单7.1。点击按钮,你会发现图层的颜色平滑过渡到一个新值,而不是跳变(图7.1)。 14 | 15 | 清单7.1 随机改变图层颜色 16 | 17 | ```objective-c 18 | @interface ViewController () 19 | 20 | @property (nonatomic, weak) IBOutlet UIView *layerView; 21 | @property (nonatomic, weak) IBOutlet CALayer *colorLayer; 22 | 23 | @end 24 | 25 | @implementation ViewController 26 | 27 | - (void)viewDidLoad 28 | { 29 | [super viewDidLoad]; 30 | //create sublayer 31 | self.colorLayer = [CALayer layer]; 32 | self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); 33 | self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; 34 | //add it to our view 35 | [self.layerView.layer addSublayer:self.colorLayer]; 36 | } 37 | 38 | - (IBAction)changeColor 39 | { 40 | //randomize the layer background color 41 | CGFloat red = arc4random() / (CGFloat)INT_MAX; 42 | CGFloat green = arc4random() / (CGFloat)INT_MAX; 43 | CGFloat blue = arc4random() / (CGFloat)INT_MAX; 44 | self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor;  45 | } 46 | 47 | @end 48 | ``` 49 | 50 | 图7.1 51 | 52 | 图7.1 添加一个按钮来控制图层颜色 53 | 54 | 这其实就是所谓的*隐式*动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。Core Animaiton同样支持*显式*动画,下章详细说明。 55 | 56 | 但当你改变一个属性,Core Animation是如何判断动画类型和持续时间的呢?实际上动画执行的时间取决于当前*事务*的设置,动画类型取决于*图层行为*。 57 | 58 | 事务实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦*提交*的时候开始用一个动画过渡到新值。 59 | 60 | 事务是通过`CATransaction`类来做管理,这个类的设计有些奇怪,不像你从它的命名预期的那样去管理一个简单的事务,而是管理了一叠你不能访问的事务。`CATransaction`没有属性或者实例方法,并且也不能用`+alloc`和`-init`方法创建它。但是可以用`+begin`和`+commit`分别来入栈或者出栈。 61 | 62 | 任何可以做动画的图层属性都会被添加到栈顶的事务,你可以通过`+setAnimationDuration:`方法设置当前事务的动画时间,或者通过`+animationDuration`方法来获取值(默认0.25秒)。 63 | 64 | Core Animation在每个*run loop*周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用`[CATransaction begin]`开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。 65 | 66 | 明白这些之后,我们就可以轻松修改变色动画的时间了。我们当然可以用当前事务的`+setAnimationDuration:`方法来修改动画时间,但在这里我们首先起一个新的事务,于是修改时间就不会有别的副作用。因为修改当前事务的时间可能会导致同一时刻别的动画(如屏幕旋转),所以最好还是在调整动画之前压入一个新的事务。 67 | 68 | 修改后的代码见清单7.2。运行程序,你会发现色块颜色比之前变得更慢了。 69 | 70 | 清单7.2 使用`CATransaction`控制动画时间 71 | 72 | ```objective-c 73 | - (IBAction)changeColor 74 | { 75 | //begin a new transaction 76 | [CATransaction begin]; 77 | //set the animation duration to 1 second 78 | [CATransaction setAnimationDuration:1.0]; 79 | //randomize the layer background color 80 | CGFloat red = arc4random() / (CGFloat)INT_MAX; 81 | CGFloat green = arc4random() / (CGFloat)INT_MAX; 82 | CGFloat blue = arc4random() / (CGFloat)INT_MAX; 83 | self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 84 | //commit the transaction 85 | [CATransaction commit]; 86 | } 87 | ``` 88 | 89 | 如果你用过`UIView`的动画方法做过一些动画效果,那么应该对这个模式不陌生。`UIView`有两个方法,`+beginAnimations:context:`和`+commitAnimations`,和`CATransaction`的`+begin`和`+commit`方法类似。实际上在`+beginAnimations:context:`和`+commitAnimations`之间所有视图或者图层属性的改变而做的动画都是由于设置了`CATransaction`的原因。 90 | 91 | 在iOS4中,苹果对UIView添加了一种基于block的动画方法:`+animateWithDuration:animations:`。这样写对做一堆的属性动画在语法上会更加简单,但实质上它们都是在做同样的事情。 92 | 93 | `CATransaction`的`+begin`和`+commit`方法在`+animateWithDuration:animations:`内部自动调用,这样block中所有属性的改变都会被事务所包含。这样也可以避免开发者由于对`+begin`和`+commit`匹配的失误造成的风险。 94 | 95 | ##完成块 96 | 97 | 基于`UIView`的block的动画允许你在动画结束的时候提供一个完成的动作。`CATranscation`接口提供的`+setCompletionBlock:`方法也有同样的功能。我们来调整上个例子,在颜色变化结束之后执行一些操作。我们来添加一个完成之后的block,用来在每次颜色变化结束之后切换到另一个旋转90的动画。代码见清单7.3,运行结果见图7.2。 98 | 99 | 清单7.3 在颜色动画完成之后添加一个回调 100 | 101 | ```objective-c 102 | - (IBAction)changeColor 103 | { 104 | //begin a new transaction 105 | [CATransaction begin]; 106 | //set the animation duration to 1 second 107 | [CATransaction setAnimationDuration:1.0]; 108 | //add the spin animation on completion 109 | [CATransaction setCompletionBlock:^{ 110 | //rotate the layer 90 degrees 111 | CGAffineTransform transform = self.colorLayer.affineTransform; 112 | transform = CGAffineTransformRotate(transform, M_PI_2); 113 | self.colorLayer.affineTransform = transform; 114 | }]; 115 | //randomize the layer background color 116 | CGFloat red = arc4random() / (CGFloat)INT_MAX; 117 | CGFloat green = arc4random() / (CGFloat)INT_MAX; 118 | CGFloat blue = arc4random() / (CGFloat)INT_MAX; 119 | self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 120 | //commit the transaction 121 | [CATransaction commit]; 122 | } 123 | ``` 124 | 125 | 图7.2 126 | 127 | 图7.2 颜色渐变之完成之后再做一次旋转 128 | 129 | 注意旋转动画要比颜色渐变快得多,这是因为完成块是在颜色渐变的事务提交并出栈之后才被执行,于是,用默认的事务做变换,默认的时间也就变成了0.25秒。 130 | 131 | ##图层行为 132 | 133 | 现在来做个实验,试着直接对UIView关联的图层做动画而不是一个单独的图层。清单7.4是对清单7.2代码的一点修改,移除了`colorLayer`,并且直接设置`layerView`关联图层的背景色。 134 | 135 | 清单7.4 直接设置图层的属性 136 | 137 | ```objective-c 138 | @interface ViewController () 139 | 140 | @property (nonatomic, weak) IBOutlet UIView *layerView; 141 | 142 | @end 143 | 144 | @implementation ViewController 145 | 146 | - (void)viewDidLoad 147 | { 148 | [super viewDidLoad]; 149 | //set the color of our layerView backing layer directly 150 | self.layerView.layer.backgroundColor = [UIColor blueColor].CGColor; 151 | } 152 | 153 | - (IBAction)changeColor 154 | { 155 | //begin a new transaction 156 | [CATransaction begin]; 157 | //set the animation duration to 1 second 158 | [CATransaction setAnimationDuration:1.0]; 159 | //randomize the layer background color 160 | CGFloat red = arc4random() / (CGFloat)INT_MAX; 161 | CGFloat green = arc4random() / (CGFloat)INT_MAX; 162 | CGFloat blue = arc4random() / (CGFloat)INT_MAX; 163 | self.layerView.layer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 164 | //commit the transaction 165 | [CATransaction commit]; 166 | } 167 | ``` 168 | 169 | 运行程序,你会发现当按下按钮,图层颜色瞬间切换到新的值,而不是之前平滑过渡的动画。发生了什么呢?隐式动画好像被`UIView`关联图层给禁用了。 170 | 171 | 试想一下,如果`UIView`的属性都有动画特性的话,那么无论在什么时候修改它,我们都应该能注意到的。所以,如果说UIKit建立在Core Animation(默认对所有东西都做动画)之上,那么隐式动画是如何被UIKit禁用掉呢? 172 | 173 | 我们知道Core Animation通常对`CALayer`的所有属性(可动画的属性)做动画,但是`UIView`把它关联的图层的这个特性关闭了。为了更好说明这一点,我们需要知道隐式动画是如何实现的。 174 | 175 | 我们把改变属性时`CALayer`自动应用的动画称作*行为*,当`CALayer`的属性被修改时候,它会调用`-actionForKey:`方法,传递属性的名称。剩下的操作都在`CALayer`的头文件中有详细的说明,实质上是如下几步: 176 | 177 | * 图层首先检测它是否有委托,并且是否实现`CALayerDelegate`协议指定的`-actionForLayer:forKey`方法。如果有,直接调用并返回结果。 178 | * 如果没有委托,或者委托没有实现`-actionForLayer:forKey`方法,图层接着检查包含属性名称对应行为映射的`actions`字典。 179 | * 如果`actions字典`没有包含对应的属性,那么图层接着在它的`style`字典接着搜索属性名。 180 | * 最后,如果在`style`里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的`-defaultActionForKey:`方法。 181 | 182 | 所以一轮完整的搜索结束之后,`-actionForKey:`要么返回空(这种情况下将不会有动画发生),要么是`CAAction`协议对应的对象,最后`CALayer`拿这个结果去对先前和当前的值做动画。 183 | 184 | 于是这就解释了UIKit是如何禁用隐式动画的:每个`UIView`对它关联的图层都扮演了一个委托,并且提供了`-actionForLayer:forKey`的实现方法。当不在一个动画块的实现中,`UIView`对所有图层行为返回`nil`,但是在动画block范围之内,它就返回了一个非空值。我们可以用一个demo做个简单的实验(清单7.5) 185 | 186 | 清单7.5 测试UIView的`actionForLayer:forKey:`实现 187 | 188 | ```objective-c 189 | @interface ViewController () 190 | 191 | @property (nonatomic, weak) IBOutlet UIView *layerView; 192 | 193 | @end 194 | 195 | @implementation ViewController 196 | 197 | - (void)viewDidLoad 198 | { 199 | [super viewDidLoad]; 200 | //test layer action when outside of animation block 201 | NSLog(@"Outside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]); 202 | //begin animation block 203 | [UIView beginAnimations:nil context:nil]; 204 | //test layer action when inside of animation block 205 | NSLog(@"Inside: %@", [self.layerView actionForLayer:self.layerView.layer forKey:@"backgroundColor"]); 206 | //end animation block 207 | [UIView commitAnimations]; 208 | } 209 | 210 | @end 211 | ``` 212 | 213 | 运行程序,控制台显示结果如下: 214 | 215 | $ LayerTest[21215:c07] Outside: 216 | $ LayerTest[21215:c07] Inside: 217 | 218 | 于是我们可以预言,当属性在动画块之外发生改变,`UIView`直接通过返回`nil`来禁用隐式动画。但如果在动画块范围之内,根据动画具体类型返回相应的属性,在这个例子就是`CABasicAnimation`(第八章“显式动画”将会提到)。 219 | 220 | 当然返回`nil`并不是禁用隐式动画唯一的办法,`CATransaction`有个方法叫做`+setDisableActions:`,可以用来对所有属性打开或者关闭隐式动画。如果在清单7.2的`[CATransaction begin]`之后添加下面的代码,同样也会阻止动画的发生: 221 | 222 | [CATransaction setDisableActions:YES]; 223 | 224 | 总结一下,我们知道了如下几点 225 | 226 | * `UIView`关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用`UIView`的动画函数(而不是依赖`CATransaction`),或者继承`UIView`,并覆盖`-actionForLayer:forKey:`方法,或者直接创建一个显式动画(具体细节见第八章)。 227 | * 对于单独存在的图层,我们可以通过实现图层的`-actionForLayer:forKey:`委托方法,或者提供一个`actions`字典来控制隐式动画。 228 | 229 | 我们来对颜色渐变的例子使用一个不同的行为,通过给`colorLayer`设置一个自定义的`actions`字典。我们也可以使用委托来实现,但是`actions`字典可以写更少的代码。那么到底改如何创建一个合适的行为对象呢? 230 | 231 | 行为通常是一个被Core Animation*隐式*调用的*显式*动画对象。这里我们使用的是一个实现了`CATransition`的实例,叫做*推进过渡*。 232 | 233 | 第八章中将会详细解释过渡,不过对于现在,知道`CATransition`响应`CAAction`协议,并且可以当做一个图层行为就足够了。结果很赞,不论在什么时候改变背景颜色,新的色块都是从左侧滑入,而不是默认的渐变效果。 234 | 235 | 清单7.6 实现自定义行为 236 | 237 | ```objective-c 238 | @interface ViewController () 239 | 240 | @property (nonatomic, weak) IBOutlet UIView *layerView; 241 | @property (nonatomic, weak) IBOutlet CALayer *colorLayer; 242 | 243 | @end 244 | 245 | @implementation ViewController 246 | 247 | - (void)viewDidLoad 248 | { 249 | [super viewDidLoad]; 250 | 251 | //create sublayer 252 | self.colorLayer = [CALayer layer]; 253 | self.colorLayer.frame = CGRectMake(50.0f, 50.0f, 100.0f, 100.0f); 254 | self.colorLayer.backgroundColor = [UIColor blueColor].CGColor; 255 | //add a custom action 256 | CATransition *transition = [CATransition animation]; 257 | transition.type = kCATransitionPush; 258 | transition.subtype = kCATransitionFromLeft; 259 | self.colorLayer.actions = @{@"backgroundColor": transition}; 260 | //add it to our view 261 | [self.layerView.layer addSublayer:self.colorLayer]; 262 | } 263 | 264 | - (IBAction)changeColor 265 | { 266 | //randomize the layer background color 267 | CGFloat red = arc4random() / (CGFloat)INT_MAX; 268 | CGFloat green = arc4random() / (CGFloat)INT_MAX; 269 | CGFloat blue = arc4random() / (CGFloat)INT_MAX; 270 | self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 271 | } 272 | 273 | @end 274 | ``` 275 | 276 | 图7.3 277 | 278 | 图7.3 使用推进过渡的色值动画 279 | 280 | ##呈现与模型 281 | 282 | `CALayer`的属性行为其实很不正常,因为改变一个图层的属性并没有立刻生效,而是通过一段时间渐变更新。这是怎么做到的呢? 283 | 284 | 当你改变一个图层的属性,属性值的确是立刻更新的(如果你读取它的数据,你会发现它的值在你设置它的那一刻就已经生效了),但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。 285 | 286 | 当设置`CALayer`的属性,实际上是在定义当前事务结束之后图层如何显示的*模型*。Core Animation扮演了一个*控制器*的角色,并且负责根据图层行为和事务设置去不断更新*视图*的这些属性在屏幕上的状态。 287 | 288 | 我们讨论的就是一个典型的*微型MVC模式*。`CALayer`是一个连接用户界面(就是MVC中的*view*)虚构的类,但是在界面本身这个场景下,`CALayer`的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。 289 | 290 | 在iOS中,屏幕每秒钟重绘60次。如果动画时长比60分之一秒要长,Core Animation就需要在设置一次新值和新值生效之间,对屏幕上的图层进行重新组织。这意味着`CALayer`除了“真实”值(就是你设置的值)之外,必须要知道当前*显示*在屏幕上的属性值的记录。 291 | 292 | 每个图层属性的显示值都被存储在一个叫做*呈现图层*的独立图层当中,他可以通过`-presentationLayer`方法来访问。这个呈现图层实际上是模型图层的复制,但是它的属性值代表了在任何指定时刻当前外观效果。换句话说,你可以通过呈现图层的值来获取当前屏幕上真正显示出来的值(图7.4)。 293 | 294 | 我们在第一章中提到除了图层树,另外还有*呈现树*。呈现树通过图层树中所有图层的呈现图层所形成。注意呈现图层仅仅当图层首次被*提交*(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用`-presentationLayer`将会返回`nil`。 295 | 296 | 你可能注意到有一个叫做`–modelLayer`的方法。在呈现图层上调用`–modelLayer`将会返回它正在呈现所依赖的`CALayer`。通常在一个图层上调用`-modelLayer`会返回`–self`(实际上我们已经创建的原始图层就是一种数据模型)。 297 | 298 | 图7.4 299 | 300 | 图7.4 一个移动的图层是如何通过数据模型呈现的 301 | 302 | 大多数情况下,你不需要直接访问呈现图层,你可以通过和模型图层的交互,来让Core Animation更新显示。两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。 303 | 304 | * 如果你在实现一个基于定时器的动画(见第11章“基于定时器的动画”),而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。 305 | * 如果你想让你做动画的图层响应用户输入,你可以使用`-hitTest:`方法(见第三章“图层几何学”)来判断指定图层是否被触摸,这时候对*呈现*图层而不是*模型*图层调用`-hitTest:`会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。 306 | 307 | 我们可以用一个简单的案例来证明后者(见清单7.7)。在这个例子中,点击屏幕上的任意位置将会让图层平移到那里。点击图层本身可以随机改变它的颜色。我们通过对呈现图层调用`-hitTest:`来判断是否被点击。 308 | 309 | 如果修改代码让`-hitTest:`直接作用于*colorLayer*而不是呈现图层,你会发现当图层移动的时候它并不能正确显示。这时候你就需要点击图层将要移动到的位置而不是图层本身来响应点击(这就是为什么用呈现图层来响应交互的原因)。 310 | 311 | 清单7.7 使用`presentationLayer`图层来判断当前图层位置 312 | 313 | ```objective-c 314 | @interface ViewController () 315 | 316 | @property (nonatomic, strong) CALayer *colorLayer; 317 | 318 | @end 319 | 320 | @implementation ViewController 321 | 322 | - (void)viewDidLoad 323 | { 324 | [super viewDidLoad]; 325 | //create a red layer 326 | self.colorLayer = [CALayer layer]; 327 | self.colorLayer.frame = CGRectMake(0, 0, 100, 100); 328 | self.colorLayer.position = CGPointMake(self.view.bounds.size.width / 2, self.view.bounds.size.height / 2); 329 | self.colorLayer.backgroundColor = [UIColor redColor].CGColor; 330 | [self.view.layer addSublayer:self.colorLayer]; 331 | } 332 | 333 | - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event 334 | { 335 | //get the touch point 336 | CGPoint point = [[touches anyObject] locationInView:self.view]; 337 | //check if we've tapped the moving layer 338 | if ([self.colorLayer.presentationLayer hitTest:point]) { 339 | //randomize the layer background color 340 | CGFloat red = arc4random() / (CGFloat)INT_MAX; 341 | CGFloat green = arc4random() / (CGFloat)INT_MAX; 342 | CGFloat blue = arc4random() / (CGFloat)INT_MAX; 343 | self.colorLayer.backgroundColor = [UIColor colorWithRed:red green:green blue:blue alpha:1.0].CGColor; 344 | } else { 345 | //otherwise (slowly) move the layer to new position 346 | [CATransaction begin]; 347 | [CATransaction setAnimationDuration:4.0]; 348 | self.colorLayer.position = point; 349 | [CATransaction commit]; 350 | } 351 | } 352 | ``` 353 | @end 354 | 355 | ##总结 356 | 357 | 这一章讨论了隐式动画,还有Core Animation对指定属性选择合适的动画行为的机制。同时你知道了UIKit是如何充分利用Core Animation的隐式动画机制来强化它的显式系统,以及动画是如何被默认禁用并且当需要的时候启用的。最后,你了解了呈现和模型图层,以及Core Animation是如何通过它们来判断出图层当前位置以及将要到达的位置。 358 | 359 | 在下一章中,我们将研究Core Animation提供的*显式*动画类型,既可以直接对图层属性做动画,也可以覆盖默认的图层行为。 360 | -------------------------------------------------------------------------------- /8-显式动画/8.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/8-显式动画/8.1.jpeg -------------------------------------------------------------------------------- /8-显式动画/8.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/8-显式动画/8.2.jpeg -------------------------------------------------------------------------------- /8-显式动画/8.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/8-显式动画/8.3.jpeg -------------------------------------------------------------------------------- /8-显式动画/8.4.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/8-显式动画/8.4.jpeg -------------------------------------------------------------------------------- /8-显式动画/8.5.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/8-显式动画/8.5.jpeg -------------------------------------------------------------------------------- /8-显式动画/8.6.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/8-显式动画/8.6.jpeg -------------------------------------------------------------------------------- /9-图层时间/9.1.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/9-图层时间/9.1.jpeg -------------------------------------------------------------------------------- /9-图层时间/9.2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/9-图层时间/9.2.jpeg -------------------------------------------------------------------------------- /9-图层时间/9.3.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZsIsMe/iOS-Core-Animation-Advanced-Techniques/47b444c40ac62bf5d72215751eb8e517ee477d39/9-图层时间/9.3.jpeg -------------------------------------------------------------------------------- /9-图层时间/图层时间.md: -------------------------------------------------------------------------------- 1 | #图层时间 2 | 3 | >*时间和空间最大的区别在于,时间不能被复用* -- 弗斯特梅里克 4 | 5 | 在上面两章中,我们探讨了可以用`CAAnimation`和它的子类实现的多种图层动画。动画的发生是需要持续一段时间的,所以*计时*对整个概念来说至关重要。在这一章中,我们来看看`CAMediaTiming`,看看Core Animation是如何跟踪时间的。 6 | 7 | ##`CAMediaTiming`协议 8 | 9 | `CAMediaTiming`协议定义了在一段动画内用来控制逝去时间的属性的集合,`CALayer`和`CAAnimation`都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。 10 | 11 | ###持续和重复 12 | 13 | 我们在第八章“显式动画”中简单提到过`duration`(`CAMediaTiming`的属性之一),`duration`是一个`CFTimeInterval`的类型(类似于`NSTimeInterval`的一种双精度浮点类型),对将要进行的动画的一次迭代指定了时间。 14 | 15 | 这里的*一次迭代*是什么意思呢?`CAMediaTiming`另外还有一个属性叫做`repeatCount`,代表动画重复的迭代次数。如果`duration`是2,`repeatCount`设为3.5(三个半迭代),那么完整的动画时长将是7秒。 16 | 17 | `duration`和`repeatCount`默认都是0。但这不意味着动画时长为0秒,或者0次,这里的0仅仅代表了“默认”,也就是0.25秒和1次,你可以用一个简单的测试来尝试为这两个属性赋多个值,如清单9.1,图9.1展示了程序的结果。 18 | 19 | 清单9.1 测试`duration`和`repeatCount` 20 | 21 | ```objective-c 22 | @interface ViewController () 23 | 24 | @property (nonatomic, weak) IBOutlet UIView *containerView; 25 | @property (nonatomic, weak) IBOutlet UITextField *durationField; 26 | @property (nonatomic, weak) IBOutlet UITextField *repeatField; 27 | @property (nonatomic, weak) IBOutlet UIButton *startButton; 28 | @property (nonatomic, strong) CALayer *shipLayer; 29 | 30 | @end 31 | 32 | @implementation ViewController 33 | 34 | - (void)viewDidLoad 35 | { 36 | [super viewDidLoad]; 37 | //add the ship 38 | self.shipLayer = [CALayer layer]; 39 | self.shipLayer.frame = CGRectMake(0, 0, 128, 128); 40 | self.shipLayer.position = CGPointMake(150, 150); 41 | self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; 42 | [self.containerView.layer addSublayer:self.shipLayer]; 43 | } 44 | 45 | - (void)setControlsEnabled:(BOOL)enabled 46 | { 47 | for (UIControl *control in @[self.durationField, self.repeatField, self.startButton]) { 48 | control.enabled = enabled; 49 | control.alpha = enabled? 1.0f: 0.25f; 50 | } 51 | } 52 | 53 | - (IBAction)hideKeyboard 54 | { 55 | [self.durationField resignFirstResponder]; 56 | [self.repeatField resignFirstResponder]; 57 | } 58 | 59 | - (IBAction)start 60 | { 61 | CFTimeInterval duration = [self.durationField.text doubleValue]; 62 | float repeatCount = [self.repeatField.text floatValue]; 63 | //animate the ship rotation 64 | CABasicAnimation *animation = [CABasicAnimation animation]; 65 | animation.keyPath = @"transform.rotation"; 66 | animation.duration = duration; 67 | animation.repeatCount = repeatCount; 68 | animation.byValue = @(M_PI * 2); 69 | animation.delegate = self; 70 | [self.shipLayer addAnimation:animation forKey:@"rotateAnimation"]; 71 | //disable controls 72 | [self setControlsEnabled:NO]; 73 | } 74 | 75 | - (void)animationDidStop:(CAAnimation *)anim finished:(BOOL)flag 76 | { 77 | //reenable controls 78 | [self setControlsEnabled:YES]; 79 | } 80 | 81 | @end 82 | ``` 83 | 84 | 图9.1 85 | 86 | 图9.1 演示`duration`和`repeatCount`的测试程序 87 | 88 | 创建重复动画的另一种方式是使用`repeatDuration`属性,它让动画重复一个指定的时间,而不是指定次数。你甚至设置一个叫做`autoreverses`的属性(BOOL类型)在每次间隔交替循环过程中自动回放。这对于播放一段连续非循环的动画很有用,例如打开一扇门,然后关上它(图9.2)。 89 | 90 | 图9.2 91 | 92 | 图9.2 摆动门的动画 93 | 94 | 对门进行摆动的代码见清单9.2。我们用了`autoreverses`来使门在打开后自动关闭,在这里我们把`repeatDuration`设置为`INFINITY`,于是动画无限循环播放,设置`repeatCount`为`INFINITY`也有同样的效果。注意`repeatCount`和`repeatDuration`可能会相互冲突,所以你只要对其中一个指定非零值。对两个属性都设置非0值的行为没有被定义。 95 | 96 | 清单9.2 使用`autoreverses`属性实现门的摇摆 97 | 98 | ```objective-c 99 | @interface ViewController () 100 | 101 | @property (nonatomic, weak) UIView *containerView; 102 | 103 | @end 104 | 105 | @implementation ViewController 106 | 107 | - (void)viewDidLoad 108 | { 109 | [super viewDidLoad]; 110 | //add the door 111 | CALayer *doorLayer = [CALayer layer]; 112 | doorLayer.frame = CGRectMake(0, 0, 128, 256); 113 | doorLayer.position = CGPointMake(150 - 64, 150); 114 | doorLayer.anchorPoint = CGPointMake(0, 0.5); 115 | doorLayer.contents = (__bridge id)[UIImage imageNamed: @"Door.png"].CGImage; 116 | [self.containerView.layer addSublayer:doorLayer]; 117 | //apply perspective transform 118 | CATransform3D perspective = CATransform3DIdentity; 119 | perspective.m34 = -1.0 / 500.0; 120 | self.containerView.layer.sublayerTransform = perspective; 121 | //apply swinging animation 122 | CABasicAnimation *animation = [CABasicAnimation animation]; 123 | animation.keyPath = @"transform.rotation.y"; 124 | animation.toValue = @(-M_PI_2); 125 | animation.duration = 2.0; 126 | animation.repeatDuration = INFINITY; 127 | animation.autoreverses = YES; 128 | [doorLayer addAnimation:animation forKey:nil]; 129 | } 130 | 131 | @end 132 | ``` 133 | 134 | ###相对时间 135 | 136 | 每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。 137 | 138 | `beginTime`指定了动画开始之前的的延迟时间。这里的延迟从动画添加到可见图层的那一刻开始测量,默认是0(就是说动画会立刻执行)。 139 | 140 | `speed`是一个时间的倍数,默认1.0,减少它会减慢图层/动画的时间,增加它会加快速度。如果2.0的速度,那么对于一个`duration`为1的动画,实际上在0.5秒的时候就已经完成了。 141 | 142 | `timeOffset`和`beginTime`类似,但是和增加`beginTime`导致的延迟动画不同,增加`timeOffset`只是让动画快进到某一点,例如,对于一个持续1秒的动画来说,设置`timeOffset`为0.5意味着动画将从一半的地方开始。 143 | 144 | 和`beginTime`不同的是,`timeOffset`并不受`speed`的影响。所以如果你把`speed`设为2.0,把`timeOffset`设置为0.5,那么你的动画将从动画最后结束的地方开始,因为1秒的动画实际上被缩短到了0.5秒。然而即使使用了`timeOffset`让动画从结束的地方开始,它仍然播放了一个完整的时长,这个动画仅仅是循环了一圈,然后从头开始播放。 145 | 146 | 可以用清单9.3的测试程序验证一下,设置`speed`和`timeOffset`滑块到随意的值,然后点击播放来观察效果(见图9.3) 147 | 148 | 清单9.3 测试`timeOffset`和`speed`属性 149 | 150 | ```objective-c 151 | @interface ViewController () 152 | 153 | @property (nonatomic, weak) IBOutlet UIView *containerView; 154 | @property (nonatomic, weak) IBOutlet UILabel *speedLabel; 155 | @property (nonatomic, weak) IBOutlet UILabel *timeOffsetLabel; 156 | @property (nonatomic, weak) IBOutlet UISlider *speedSlider; 157 | @property (nonatomic, weak) IBOutlet UISlider *timeOffsetSlider; 158 | @property (nonatomic, strong) UIBezierPath *bezierPath; 159 | @property (nonatomic, strong) CALayer *shipLayer; 160 | 161 | @end 162 | 163 | @implementation ViewController 164 | 165 | - (void)viewDidLoad 166 | { 167 | [super viewDidLoad]; 168 | //create a path 169 | self.bezierPath = [[UIBezierPath alloc] init]; 170 | [self.bezierPath moveToPoint:CGPointMake(0, 150)]; 171 | [self.bezierPath addCurveToPoint:CGPointMake(300, 150) controlPoint1:CGPointMake(75, 0) controlPoint2:CGPointMake(225, 300)]; 172 | //draw the path using a CAShapeLayer 173 | CAShapeLayer *pathLayer = [CAShapeLayer layer]; 174 | pathLayer.path = self.bezierPath.CGPath; 175 | pathLayer.fillColor = [UIColor clearColor].CGColor; 176 | pathLayer.strokeColor = [UIColor redColor].CGColor; 177 | pathLayer.lineWidth = 3.0f; 178 | [self.containerView.layer addSublayer:pathLayer]; 179 | //add the ship 180 | self.shipLayer = [CALayer layer]; 181 | self.shipLayer.frame = CGRectMake(0, 0, 64, 64); 182 | self.shipLayer.position = CGPointMake(0, 150); 183 | self.shipLayer.contents = (__bridge id)[UIImage imageNamed: @"Ship.png"].CGImage; 184 | [self.containerView.layer addSublayer:self.shipLayer]; 185 | //set initial values 186 | [self updateSliders]; 187 | } 188 | 189 | - (IBAction)updateSliders 190 | { 191 | CFTimeInterval timeOffset = self.timeOffsetSlider.value; 192 | self.timeOffsetLabel.text = [NSString stringWithFormat:@"%0.2f", timeOffset]; 193 | float speed = self.speedSlider.value; 194 | self.speedLabel.text = [NSString stringWithFormat:@"%0.2f", speed]; 195 | } 196 | 197 | - (IBAction)play 198 | { 199 | //create the keyframe animation 200 | CAKeyframeAnimation *animation = [CAKeyframeAnimation animation]; 201 | animation.keyPath = @"position"; 202 | animation.timeOffset = self.timeOffsetSlider.value; 203 | animation.speed = self.speedSlider.value; 204 | animation.duration = 1.0; 205 | animation.path = self.bezierPath.CGPath; 206 | animation.rotationMode = kCAAnimationRotateAuto; 207 | animation.removedOnCompletion = NO; 208 | [self.shipLayer addAnimation:animation forKey:@"slide"]; 209 | } 210 | 211 | @end 212 | ``` 213 | 214 | 图9.3 215 | 216 | 图9.3 测试时间偏移和速度的简单的应用程序 217 | 218 | ###`fillMode` 219 | 220 | 对于`beginTime`非0的一段动画来说,会出现一个当动画添加到图层上但什么也没发生的状态。类似的,`removeOnCompletion`被设置为`NO`的动画将会在动画结束的时候仍然保持之前的状态。这就产生了一个问题,当动画开始之前和动画结束之后,被设置动画的属性将会是什么值呢? 221 | 222 | 一种可能是属性和动画没被添加之前保持一致,也就是在模型图层定义的值(见第七章“隐式动画”,模型图层和呈现图层的解释)。 223 | 224 | 另一种可能是保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的*填充*,因为动画开始和结束的值用来填充开始之前和结束之后的时间。 225 | 226 | 这种行为就交给开发者了,它可以被`CAMediaTiming`的`fillMode`来控制。`fillMode`是一个`NSString`类型,可以接受如下四种常量: 227 | 228 | kCAFillModeForwards 229 | kCAFillModeBackwards 230 | kCAFillModeBoth 231 | kCAFillModeRemoved 232 | 233 | 默认是`kCAFillModeRemoved`,当动画不再播放的时候就显示图层模型指定的值剩下的三种类型向前,向后或者即向前又向后去填充动画状态,使得动画在开始前或者结束后仍然保持开始和结束那一刻的值。 234 | 235 | 这就对避免在动画结束的时候急速返回提供另一种方案(见第八章)。但是记住了,当用它来解决这个问题的时候,需要把`removeOnCompletion`设置为`NO`,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。 236 | 237 | ##层级关系时间 238 | 239 | 在第三章“图层几何学”中,你已经了解到每个图层是如何相对在图层树中的父图层定义它的坐标系的。动画时间和它类似,每个动画和图层在时间上都有它自己的层级概念,相对于它的父亲来测量。对图层调整时间将会影响到它本身和子图层的动画,但不会影响到父图层。另一个相似点是所有的动画都被按照层级组合(使用`CAAnimationGroup`实例)。 240 | 241 | 对`CALayer`或者`CAGroupAnimation`调整`duration`和`repeatCount`/`repeatDuration`属性并不会影响到子动画。但是`beginTime`,`timeOffset`和`speed`属性将会影响到子动画。然而在层级关系中,`beginTime`指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。类似的,调整`CALayer`和`CAGroupAnimation`的`speed`属性将会对动画以及子动画速度应用一个缩放的因子。 242 | 243 | ###全局时间和本地时间 244 | 245 | CoreAnimation有一个*全局时间*的概念,也就是所谓的*马赫时间*(“马赫”实际上是iOS和Mac OS系统内核的命名)。马赫时间在设备上所有进程都是全局的--但是在不同设备上并不是全局的--不过这已经足够对动画的参考点提供便利了,你可以使用`CACurrentMediaTime`函数来访问马赫时间: 246 | 247 | CFTimeInterval time = CACurrentMediaTime(); 248 | 249 | 250 | 这个函数返回的值其实无关紧要(它返回了设备自从上次启动后的秒数,并不是你所关心的),它真实的作用在于对动画的时间测量提供了一个相对值。注意当设备休眠的时候马赫时间会暂停,也就是所有的`CAAnimations`(基于马赫时间)同样也会暂停。 251 | 252 | 253 | 因此马赫时间对长时间测量并不有用。比如用`CACurrentMediaTime`去更新一个实时闹钟并不明智。(可以用`[NSDate date]`代替,就像第三章例子所示)。 254 | 255 | 256 | 每个`CALayer`和`CAAnimation`实例都有自己*本地*时间的概念,是根据父图层/动画层级关系中的`beginTime`,`timeOffset`和`speed`属性计算。就和转换不同图层之间坐标关系一样,`CALayer`同样也提供了方法来转换不同图层之间的*本地时间*。如下: 257 | 258 | - (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l; 259 | - (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l; 260 | 261 | 当用来同步不同图层之间有不同的`speed`,`timeOffset`和`beginTime`的动画,这些方法会很有用。 262 | 263 | 264 | ###暂停,倒回和快进 265 | 266 | 设置动画的`speed`属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个`CAAnimation`实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用`-animationForKey:`来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。 267 | 268 | 如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。 269 | 270 | 一个简单的方法是可以利用`CAMediaTiming`来暂停*图层*本身。如果把图层的`speed`设置成0,它会暂停任何添加到图层上的动画。类似的,设置`speed`大于1.0将会快进,设置成一个负值将会倒回动画。 271 | 272 | 273 | 通过增加主窗口图层的`speed`,可以暂停整个应用程序的动画。这对UI自动化提供了好处,我们可以加速所有的视图动画来进行自动化测试(注意对于在主窗口之外的视图并不会被影响,比如`UIAlertview`)。可以在app delegate设置如下进行验证: 274 | 275 | self.window.layer.speed = 100; 276 | 277 | 你也可以通过这种方式来*减速*,但其实也可以在模拟器通过切换慢速动画来实现。 278 | 279 | ##手动动画 280 | 281 | `timeOffset`一个很有用的功能在于你可以它可以让你手动控制动画进程,通过设置`speed`为0,可以禁用动画的自动播放,然后来使用`timeOffset`来来回显示动画序列。这可以使得运用手势来手动控制动画变得很简单。 282 | 283 | 举个简单的例子:还是之前关门的动画,修改代码来用手势控制动画。我们给视图添加一个`UIPanGestureRecognizer`,然后用`timeOffset`左右摇晃。 284 | 285 | 因为在动画添加到图层之后不能再做修改了,我们来通过调整`layer`的`timeOffset`达到同样的效果(清单9.4)。 286 | 287 | 清单9.4 通过触摸手势手动控制动画 288 | 289 | ```objective-c 290 | @interface ViewController () 291 | 292 | @property (nonatomic, weak) UIView *containerView; 293 | @property (nonatomic, strong) CALayer *doorLayer; 294 | 295 | @end 296 | 297 | @implementation ViewController 298 | 299 | - (void)viewDidLoad 300 | { 301 | [super viewDidLoad]; 302 | //add the door 303 | self.doorLayer = [CALayer layer]; 304 | self.doorLayer.frame = CGRectMake(0, 0, 128, 256); 305 | self.doorLayer.position = CGPointMake(150 - 64, 150); 306 | self.doorLayer.anchorPoint = CGPointMake(0, 0.5); 307 | self.doorLayer.contents = (__bridge id)[UIImage imageNamed:@"Door.png"].CGImage; 308 | [self.containerView.layer addSublayer:self.doorLayer]; 309 | //apply perspective transform 310 | CATransform3D perspective = CATransform3DIdentity; 311 | perspective.m34 = -1.0 / 500.0; 312 | self.containerView.layer.sublayerTransform = perspective; 313 | //add pan gesture recognizer to handle swipes 314 | UIPanGestureRecognizer *pan = [[UIPanGestureRecognizer alloc] init]; 315 | [pan addTarget:self action:@selector(pan:)]; 316 | [self.view addGestureRecognizer:pan]; 317 | //pause all layer animations 318 | self.doorLayer.speed = 0.0; 319 | //apply swinging animation (which won't play because layer is paused) 320 | CABasicAnimation *animation = [CABasicAnimation animation]; 321 | animation.keyPath = @"transform.rotation.y"; 322 | animation.toValue = @(-M_PI_2); 323 | animation.duration = 1.0; 324 | [self.doorLayer addAnimation:animation forKey:nil]; 325 | } 326 | 327 | - (void)pan:(UIPanGestureRecognizer *)pan 328 | { 329 | //get horizontal component of pan gesture 330 | CGFloat x = [pan translationInView:self.view].x; 331 | //convert from points to animation duration //using a reasonable scale factor 332 | x /= 200.0f; 333 | //update timeOffset and clamp result 334 | CFTimeInterval timeOffset = self.doorLayer.timeOffset; 335 | timeOffset = MIN(0.999, MAX(0.0, timeOffset - x)); 336 | self.doorLayer.timeOffset = timeOffset; 337 | //reset pan gesture 338 | [pan setTranslation:CGPointZero inView:self.view]; 339 | } 340 | 341 | @end 342 | ``` 343 | 344 | 这其实是个小诡计,也许相对于设置个动画然后每次显示一帧而言,用移动手势来直接设置门的`transform`会更简单。 345 | 346 | 在这个例子中的确是这样,但是对于比如说关键这这样更加复杂的情况,或者有多个图层的动画组,相对于实时计算每个图层的属性而言,这就显得方便的多了。 347 | 348 | ##总结 349 | 350 | 在这一章,我们了解了`CAMediaTiming`协议,以及Core Animation用来操作时间控制动画的机制。在下一章,我们将要接触`缓冲`,另一个用来使动画更加真实的操作时间的技术。 351 | 352 | 353 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | iOS-Core-Animation-Advanced-Techniques 2 | ====================================== 3 | 4 | 翻译,喵~ 5 | 6 | >知识是人类进步的阶梯 7 | 8 | * [1-图层树](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/1-图层树/图层树.md) 9 | 10 | * [2-寄宿图](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/2-寄宿图/寄宿图.md) 11 | 12 | * [3-图层几何学](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/3-图层几何学/图层几何学.md) 13 | 14 | * [4-视觉效果](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/4-视觉效果/4-视觉效果.md) 15 | 16 | * [5-变换](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/5-变换/变换.md) 17 | 18 | * [6-专有图层](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/6-专有图层/6-专有图层.md) 19 | 20 | * [7-隐式动画](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/7-隐式动画/隐式动画.md) 21 | 22 | * [8-显式动画](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/8-显式动画/显式动画.md) 23 | 24 | * [9-图层时间](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/9-图层时间/图层时间.md) 25 | 26 | * [10-缓冲](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/10-缓冲/缓冲.md) 27 | 28 | * [11-基于定时器的动画](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/11-基于定时器的动画/基于定时器的动画.md) 29 | 30 | * [12-性能调优](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/12-性能调优/性能调优.md) 31 | 32 | * [13-高效绘图](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/13-高效绘图/13-高效绘图.md) 33 | 34 | * [14-图像IO](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/14-图像IO/图像IO.md) 35 | 36 | * [15-图层性能](https://github.com/AttackOnDobby/iOS-Core-Animation-Advanced-Techniques/blob/master/15-图层性能/15-图层性能.md) 37 | 38 | 39 | 附上[ZsIsMe](https://github.com/ZsIsMe)同学制作的[gitbook排版](http://zsisme.gitbooks.io/ios-/),非常感谢大家的辛勤劳动 --------------------------------------------------------------------------------