iOS Core Animation: Advanced Techniques中文译本
原文链接:https://zsisme.gitbooks.io/ios-/content/index.html
1. 图层树
● Core Animation是一个复合引擎,它的职责就是尽可能快地组合屏幕上不同的 “可视内容” ,这个 “内容” 是 ”被分解“ 成独立的图层,存储在一个叫做 “图层树” 的体系之中。
1.1 图层与视图
● 一个“视图”就是在屏幕上显示的一个矩形块(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。
● 在iOS当中,”所有“ 的视图都从一个叫做
UIVIew
的基类派生而来,UIView
可以处理触摸事件,可以支持基于Core Graphics绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。
CALayer
● 和
UIView
最大的不同是CALayer
不处理用户的交互● 每一个
UIview
都有一个CALayer
实例的图层属性,也就是所谓的backing layer,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作● 实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,
UIView
仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation底层方法的高级接口。● 在iOS和Mac OS两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS有UIKit和
UIView
,但是Mac OS有AppKit和NSView
的原因。● 这里并不是两个层级关系,而是四个,每一个都扮演不同的角色,除了视图层级和图层树之外,还存在呈现树和渲染树。视图层次是UIView,图层树是CALayer
1.2 图层的能力
UIView
没有暴露出来的CALayer的功能:
- 阴影,圆角,带颜色的边框
- 3D变换
- 非矩形范围
- 透明遮罩
- 多级非线性动画
1.3 使用图层
● 一个 “视图” 只有一个相关联的 “图层”(自动创建),同时它也可以支持添加无数多个子图层。
● 当满足以下条件的时候,你可能更需要使用
CALayer
而不是UIView
:
- 开发同时可以在Mac OS上运行的跨平台应用
- 使用多种
CALayer
的子类(见第六章,“特殊的图层“),并且不想创建额外的UIView
去包封装它们所有- 做一些对性能特别挑剔的工作,比如对
UIView
一些可忽略不计的操作都会引起显著的不同(尽管如此,你可能会直接想使用OpenGL绘图)但是这些例子都很少见,总的来说,处理视图会比单独处理图层更加方便。
2. 寄宿图
● CALayer的寄宿图(即图层中包含的图)
2.1 contents属性
● CALayer 有一个属性叫做
contents
,这个属性的类型被定义为id,意味着它可以是任何类型的对象。但是,在实践中,如果你给contents
赋的不是CGImage,那么你得到的图层将是空白的。● 事实上,你真正要赋值的类型应该是CGImageRef,它是一个指向CGImage结构的指针。UIImage有一个CGImage属性,它返回一个"CGImageRef",如果你想把这个值直接赋值给CALayer的
contents
,那你将会得到一个编译错误。因为CGImageRef并不是一个真正的Cocoa对象,而是一个Core Foundation类型。不过你可以通过bridged关键字转换。
contentGravity
● 不过UIView大多数视觉相关的属性比如
contentMode
,对这些属性的操作其实是对对应 图层 的操作。CALayer与contentMode
对应的属性叫做contentsGravity
,但是它是一个NSString类型。●
contentsGravity
可选的常量值有以下一些:
- kCAGravityCenter
- kCAGravityTop
- kCAGravityBottom
- kCAGravityLeft
- kCAGravityRight
- kCAGravityTopLeft
- kCAGravityTopRight
- kCAGravityBottomLeft
- kCAGravityBottomRight
- kCAGravityResize
- kCAGravityResizeAspect
- kCAGravityResizeAspectFill
和
cotentMode
一样,contentsGravity
的目的是为了决定内容在图层的边界中怎么对齐,我们将使用kCAGravityResizeAspect,它的效果等同于UIViewContentModeScaleAspectFit, 同时它还能在图层中等比例拉伸以适应图层的边界。
contentsScale
●
contentsScale
属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。contentsScale
的目的并不是那么明显。它并不是总会对屏幕上的寄宿图有影响。因为contents
由于设置了contentsGravity
属性,所以它已经被拉伸以适应图层的边界。●
contentsScale
属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity
属性)。UIView有一个类似功能但是非常少用到的contentScaleFactor
属性。● 如果
contentsScale
设置为1.0,将会以每个点1个像素绘制图片,如果设置为2.0,则会以每个点2个像素绘制图片,这就是我们熟知的Retina屏幕。● 这并不会对我们在使用kCAGravityResizeAspect时产生任何影响,因为它就是拉伸图片以适应图层而已,根本不会考虑到分辨率问题。
● 那是因为和UIImage不同,GImage没有拉伸的概念。当我们使用UIImage类去读取我们的雪人图片的时候,他读取了高质量的Retina版本的图片。但是当我们用CGImage来设置我们的图层的内容时,拉伸这个因素在转换的时候就丢失了。
contentsRect
● CALayer的
contentsRect
属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比contentsGravity
灵活多了● 和
bounds
,frame
不同,contentsRect
不是按点来计算的,它使用了单位坐标,单位坐标指定在0到1之间,是一个相对值(像素和点就是绝对值)。★ iOS使用了以下的坐标系统:
- 点 —— 在iOS和Mac OS中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina设备上,一个点等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。
- 像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,使用“点”来度量大小。但是一些底层的图片表示如CGImage就会使用“像素”,所以你要清楚在Retina设备和普通设备上,他们表现出来了不同的大小。
- 单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,★ Core Animation中也用到了“单位坐标”。
默认的
contentsRect
是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪(如图2.6)
● 事实上给
contentsRect
设置一个负数的原点或是大于{1, 1}的尺寸也是可以的。这种情况下,最外面的像素会被拉伸以填充剩下的区域。●
contentsRect
在app中最有趣的地方在于一个叫做image sprites(图片拼合)的用法。典型地,图片拼合后可以打包整合到一张大图上一次性载入。相比多次载入不同的图片,这样做能够带来很多方面的好处:内存使用,载入时间,渲染性能等等。是通过创建多个layer对它的content的位置和contentsRect进行设置达到拼合的效果的。
contentsCenter
●
contentsCenter
与位置没有啥关系,它其实是一个CGRect,它定义了 “一个” 固定的边框 和“一个” 在图层上可拉伸的区域。 改变contentsCenter
的值并不会影响到寄宿图的显示,除非这个图层的大小改变了,你才看得到效果。● 默认情况下,
contentsCenter
是{0, 0, 1, 1},这意味着如果大小(由conttensGravity
决定)改变了,那么寄宿图将会均匀地拉伸开。但是如果我们增加原点的值并减小尺寸。我们会在图片的周围创造一个边框。图2.9展示了contentsCenter
设置为{0.25, 0.25, 0.5, 0.5}的效果。是运行在寄宿图里面的。
2.2 Custom Drawing
● 给
contents
赋CGImage的值不是唯一的设置寄宿图的方法。我们也可以直接用Core Graphics直接绘制寄宿图。能够通过继承UIView并实现-drawRect:
方法来自定义绘制。● 如果UIView检测到
-drawRect:
方法被调用了,它就会为视图分配一个寄宿图,这个寄宿图的像素尺寸等于视图大小乘以contentsScale
的值。● 如果你不需要寄宿图,那就不要创建这个方法了,这会造成CPU资源和内存的浪费,这也是为什么苹果建议:如果没有自定义绘制的任务就不要在子类中写一个空的-drawRect:方法。当视图在屏幕上出现的时候
-drawRect:
方法就会被自动调用。● CALayer有一个可选的
delegate
属性,实现了CALayerDelegate
协议,当CALayer需要一个内容的 特定的信息时,就会从协议中请求。例如寄宿图● 除非你创建了一个单独的图层,你几乎没有机会用到CALayerDelegate协议。因为当UIView创建了它的宿主图层时,它就会自动地把图层的delegate设置为它自己,并提供了一个
-displayLayer:
的实现,那所有的问题就都没了。● 当使用寄宿了视图的图层的时候,你也不必实现
-displayLayer:
和-drawLayer:inContext:
方法来绘制你的寄宿图。通常做法是实现UIView的-drawRect:
方法,UIView就会帮你做完剩下的工作,包括在需要重绘的时候调用-display
方法。
3. 图层几何学
● 看一看图层内部是如何根据父图层和兄弟图层来控制 位置和尺寸 的。另外我们也会涉及如何管理图层的几何结构,以及它是如何“被”自动调整和自动布局 影响 的。
3.1 布局
●
UIView
有三个比较重要的布局属性:frame
,bounds
和center
,
CALayer
对应地叫做frame
,bounds
和position
。为了能清楚区分,“图层” 用了“position”,“视图” 用了“center”,但是他们都代表同样的值。
●
frame
代表了图层的外部坐标(也就是在父图层上占据的空间),bounds
是内部坐标({0, 0}通常是图层的左上角),center
和position
都代表了相对于父图层anchorPoint
所在的位置。anchorPoint
的属性将会在后续介绍到,现在把它想成图层的中心点就好了。● ★ 视图的
frame
,bounds
和center
属性仅仅是存取方法,当操纵视图的frame
,实际上是在改变位于视图下方CALayer
的frame
,不能够独立于图层之外改变视图的frame
。★ 对于视图或者图层来说,
frame
并不是一个非常清晰的属性,它其实是一个虚拟属性,是根据bounds
,position
和transform
计算而来,所以当其中任何一个值发生改变,frame都会变化。相反,改变frame的值同样会影响到他们当中的值★ 记住当对图层做变换的时候,比如旋转或者缩放,
frame
实际上代表了覆盖在图层旋转之后的整个“轴对齐”的矩形区域,也就是说frame
的宽高可能和bounds
的宽高不再一致了
也就是说bounds是内容矩形的属性,frame的视图框架的属性。注意:UIView和CALayer都有自己的frame和bounds属性。
3.2 锚点(是图层的)
● 默认来说,
anchorPoint
位于图层的中点,所以图层的将会以这个点为中心放置。所谓锚点,就是和船只抛锚的点一样,所谓的设置锚点,就是把视图的自身某一点作为锚点,也就是你把视图的某一点抛锚到默认的固定的点上。我们就叫这一固定的点为基准点吧。所以你设置锚点的效果就相当于,把视图自身坐标系的某一点抛锚到这一点上了。默认的固定的点是图层的中点,也就是你所有抛锚的点都是相对于这一点而言。
注意:是layer的frame坐标发生了改变,bounds坐标没有改变(自身坐标),position也没有改变(自身坐标)。
●
anchorPoint
可以通过指定x和y值小于0或者大于1,使它放置在图层范围之外。
● 视图的
transform
属性来旋转钟表,是根据UIview的frame的中心点进行旋转,所以你可以通过设置anchorPoint的方式实现时钟表针的旋转。
3.3坐标系
● 和视图一样,图层在图层树当中也是相对于父图层按层级关系放置,一个图层的
position
依赖于它父图层的bounds
,如果父图层发生了移动,它的所有子图层也会跟着移动。● 为了知道一个图层的绝对位置,
CALayer
给不同坐标系之间的图层转换提供了一些工具类方法:- (CGPoint)convertPoint:(CGPoint)point fromLayer:(CALayer *)layer; - (CGPoint)convertPoint:(CGPoint)point toLayer:(CALayer *)layer; - (CGRect)convertRect:(CGRect)rect fromLayer:(CALayer *)layer; - (CGRect)convertRect:(CGRect)rect toLayer:(CALayer *)layer;
这些方法可以把定义在一个图层坐标系下的点或者矩形转换成另一个图层坐标系下的点或者矩形.
Z坐标轴
●
zPosition
属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。● 和
UIView
严格的二维坐标系不同,CALayer
存在于一个三维空间当中。除了我们已经讨论过的position
和anchorPoint
属性之外,CALayer
还有另外两个属性,zPosition
和anchorPointZ
,二者都是在Z轴上描述图层位置的浮点类型。●
zPosition
属性在大多数情况下其实并不常用。在第五章,我们将会涉及CATransform3D
,你会知道如何在三维空间移动和旋转图层,除了做变换之外,zPosition
最实用的功能就是改变图层的显示顺序了。
● 我们希望在真实的应用中也能显示出绘图的顺序,同样地,如果我们提高绿色视图的
zPosition
(清单3.3),我们会发现顺序就反了(图3.9)。其实并不需要增加太多,视图都非常地薄,所以给zPosition
提高一个像素就可以让绿色视图前置,当然0.1或者0.0001也能够做到,但是最好不要这样,因为浮点类型四舍五入的计算可能会造成一些不便的麻烦。
self.greenView.layer.zPosition = 1.0f;
3.4 Hit Testing -- 点击检验
●
CALayer
并不关心任何响应链事件,所以不能直接处理触摸事件或者手势。但是它有一系列的方法帮你处理事件:-containsPoint:
和-hitTest:
。●
-containsPoint:
接受一个在本图层坐标系下的CGPoint
,如果这个点在图层frame
范围内就返回YES
。●
-hitTest:
方法同样接受一个CGPoint
类型参数,而不是BOOL
类型,它返回图层本身,或者包含这个坐标点的叶子节点图层。这意味着不再需要像使用-containsPoint:
那样,人工地在每个子图层变换或者测试点击的坐标。如果这个点在最外面图层的范围之外,则返回nil。
3.5自动布局
● 你可能用过
UIViewAutoresizingMask
类型的一些常量,应用于当父视图改变尺寸的时候,相应UIView
的frame
也跟着更新的场景(通常用于横竖屏切换)。我没用过。自动排版 机制● 在Mac OS平台,
CALayer
有一个叫做layoutManager
的属性可以通过CALayoutManager
协议和CAConstraintLayoutManager
类来实现自动排版的机制。但由于某些原因,这在iOS上并不适用。● 当使用视图的时候,可以充分利用
UIView
类接口暴露出来的UIViewAutoresizingMask
和NSLayoutConstraint
API,但如果想随意控制CALayer
的布局,就需要手工操作。最简单的方法就是使用CALayerDelegate
的如下函数:- (void)layoutSublayersOfLayer:(CALayer *)layer;
当图层的
bounds
发生改变,或者图层的-setNeedsLayout
方法被调用的时候,这个函数将会被执行。这使得你可以手动地重新摆放或者重新调整子图层的大小,但是不能像UIView
的autoresizingMask
和constraints
属性做到自适应屏幕旋转。
4. 视觉效果
● 使用CALayer属性实现的视觉效果。
4.1 圆角
● CALayer有一个叫做
conrnerRadius
的属性控制着图层角的曲率。它是一个浮点数,默认为0(为0的时候就是直角),但是你可以把它设置成任意值。默认情况下,这个曲率值只影响背景颜色 而不影响 背景图片或是子图层。不过,如果把masksToBounds
设置成YES的话,图层里面的所有东西都会被截取。
上图中,红色的子视图沿角半径被裁剪了
如你所见,右边的子视图沿边界被裁剪了。
单独控制每个层的圆角曲率也不是不可能的。如果想创建有些圆角有些直角的图层或视图时,你可能需要一些不同的方法。比如使用一个图层蒙板(本章稍后会讲到)或者是CAShapeLayer(见第六章『专用图层』)。
4.2 图层边框
● CALayer另外两个非常有用属性就是
borderWidth
和borderColor
。二者共同定义了图层边的绘制样式。这条线(也被称作stroke)沿着图层的bounds
绘制,同时也包含图层的角。●
borderWidth
是以点为单位的定义边框粗细的浮点数,默认为0.borderColor
定义了边框的颜色,默认为黑色。borderColor
是CGColorRef类型,而不是UIColor,所以它不是Cocoa的内置对象。● 边框是绘制在图层边界里面的,而且在所有子内容绘制 之前,也在子图层之前。边框是跟随图层的边界变化的,而不是图层里面的内容
4.3 阴影
● iOS的另一个常见特性呢,就是阴影。阴影往往可以达到图层深度暗示的效果。也能够用来强调正在显示的图层和优先级(比如说一个在其他视图之前的弹出框),不过有时候他们只是单纯的装饰目的。
● 给
shadowOpacity
属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。
shadowOpacity
是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。若要改动阴影的表现,你可以使用CALayer的另外三个属性:
shadowColor
,shadowOffset
和shadowRadius
,类型也是CGColorRef
。
shadowOffset
属性控制着阴影的方向和距离。它是一个CGSize
的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。shadowOffset
的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。
shadowRadius
属性控制着阴影的模糊度,当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然。通常来讲,如果你想让视图或控件非常醒目独立于背景之外(比如弹出框遮罩层),你就应该给
shadowRadius
设置一个稍大的值。阴影越模糊,图层的深度看上去就会更明显(如下图)
阴影裁剪
● 和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。
为了计算出阴影的形状,Core Animation会将寄宿图(包括子视图,如果有的话)考虑在内,然后通过这些来完美搭配图层形状从而创建一个阴影(见下图)。
● 当阴影和裁剪扯上关系的时候,
maskToBounds
属性会裁剪掉了阴影和内容。如果你想沿着内容裁切,你需要用到两个图层:一个只画阴影的空的外图层,和一个用masksToBounds
裁剪内容的内图层。
shadowPath属性
● 如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个
shadowPath
(layer的属性)来提高性能。shadowPath
是一个CGPathRef
类型(一个指向CGPath
的指针)。CGPath
是一个Core Graphics对象,用来指定任意的一个矢量图形。我们可以使这个属性 单独于 图层形状之外,从而指定阴影的形状。
● 但是如果是更加复杂一点的图形,
UIBezierPath
类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。
4.4 图层蒙板
● 有时候你希望展现的内容不是在一个矩形或圆角矩形。比如,你想展示一个有星形框架的图片,又或者想让一些古卷文字慢慢渐变成背景色,而不是一个突兀的边界。
CALayer有一个属性叫做
mask
可以解决这个问题。这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层, ★mask
图层定义了父图层的部分可见区域。
mask
图层的Color
属性是无关紧要的,真正重要的是图层的轮廓。mask
属性就像是一个饼干切割机,mask
图层实心的部分会被保留下来,其他的则会被抛弃。(如下图)
● CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为
mask
属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。
4.5 拉伸过滤
● 当图片需要显示不同的大小的时候,有一种叫做 “拉伸过滤” 的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。
使用的是CALayer中的minificationFilter
和magnificationFilter
属性●
CALayer
为此提供了三种拉伸过滤方法,他们是:
- kCAFilterLinear
- kCAFilterNearest
- kCAFilterTrilinear
● minification(缩小图片)和magnification(放大图片)默认的过滤器都是
kCAFilterLinear
,这个过滤器采用 双线性滤波 算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。
kCAFilterNearest
是一种比较武断的方法。从名字不难看出,这个算法(也叫最近过滤)就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。但是对于大多数的图尤其是有很多斜线或是曲线轮廓的图片来说,最近过滤算法会导致更差的结果。换句话说,线性过滤保留了形状,最近过滤则保留了像素的差异。
对于大图来说,双线性滤波和三线性滤波表现得更出色。对于没有斜线的小图来说,最近过滤算法要好很多
view.layer.magnificationFilter = kCAFilterNearest;
● 也就是说,你的layer如果有发生缩放的话,你可以在代码中指定缩放的过滤方法。例如你使用了view.layer.contentsRect属性,就有可能发生了缩放。
4.6 组透明
● UIView有一个叫做
alpha
的属性来确定视图的透明度。CALayer有一个等同的属性叫做opacity
,这两个属性都是影响子层级的。也就是说,如果你给一个图层设置了opacity
属性,那它的子图层都会受此影响。● 图4.20展示了一个内嵌了UILabel的自定义UIButton;左边是一个不透明的按钮,右边是50%透明度的相同按钮。我们可以注意到,里面的标签的轮廓跟按钮的背景很不搭调。
这是由透明度的混合叠加造成的,当你显示一个50%透明度的图层时,图层的每个像素都会一半显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。
但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来自图层本身的颜色,另外的25%则来自 更上一层图层的背景色。
在示例中,按钮和标签都是白色背景。虽然他们都是50%的可见度,但是合起来的可见度是75%,所以标签所在的区域看上去就没有周围的部分那么透明。所以看上去子视图就高亮了,使得这个显示效果都糟透了。
● 你可以通过设置Info.plist文件中的
UIViewGroupOpacity
为YES来达到这个效果,但是这个设置会影响到这个应用,整个app可能会受到不良影响。● 你可以设置CALayer的一个叫做
shouldRasterize
属性(见清单4.7)来实现组透明的效果,如果它被设置为YES,在应用透明度之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了
为了启用
shouldRasterize
属性,我们设置了图层的rasterizationScale
属性。默认情况下,所有图层拉伸都是1.0, 所以如果你使用了shouldRasterize
属性,你就要确保你设置了rasterizationScale
属性来匹配屏幕,以防止出现Retina屏幕像素化的问题。● 当
shouldRasterize
和UIViewGroupOpacity
一起的时候,性能问题就出现了
5. 变换
● 在这一章中,我们将要 研究 可以用来对图层旋转,摆放或者扭曲的 CGAffineTransform
,以及可以将扁平物体转换成三维空间对象的CATransform3D
(而不是仅仅对圆角矩形添加下沉阴影)。
5.1 仿射变换(注意是UIView还是CALayer)
● 实际上“
UIView”
的transform
属性是一个CGAffineTransform
类型,用于在二维空间做旋转,缩放和平移。CGAffineTransform
是一个可以和二维空间向量(例如CGPoint
)做乘法的3X2的矩阵(见图5.1)。
● 当对图层应用变换矩阵,图层矩形内的每一个点都被相应地做变换,从而形成一个新的四边形的形状。
创建一个
CGAffineTransform
● Core Graphics提供了一系列函数,对完全没有数学基础的开发者也能够简单地做一些变换。如下几个函数都创建了一个
CGAffineTransform
实例:CGAffineTransformMakeRotation(CGFloat angle) CGAffineTransformMakeScale(CGFloat sx, CGFloat sy) CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
Translation平移变换是指每个点都移动了向量指定的x或者y值--所以如果向量代表了一个点,那它就平移了这个点的距离。
★
UIView
可以通过设置transform
属性做变换,但实际上它只是封装了内部图层的变换。★
CALayer
同样也有一个transform
属性,但它的类型是CATransform3D
,而不是CGAffineTransform
,后续会解释。★
CALayer
对应于UIView
的transform
属性叫做affineTransform
混合变换
● Core Graphics提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个既要缩放又要旋转的变换,这就会非常有用了。例如下面几个函数:
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle) CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy) CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
● 如果需要混合两个已经存在的变换矩阵,就可以使用如下方法,在两个变换的基础上创建一个新的变换:
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2);
● 也就是创建一个
CGAffineTransform
类型的空值,矩阵论中称作单位矩阵,Core Graphics同样也提供了一个方便的常量:CGAffineTransformIdentity
● 组合一个更加复杂的变换,先缩小50%,再旋转30度,最后向右移动200个像素(清单5.2)。图5.4显示了图层变换最后的结果。
图5.4中有些需要注意的地方:图片向右边发生了平移,但并没有指定距离那么远(200像素),另外它还有点向下发生了平移。原因在于当你按顺序做了变换,上一个变换的结果将会影响之后的变换,所以200像素的向右平移同样也被旋转了30度,缩小了50%,所以它实际上是斜向移动了100像素。
这意味着变换的顺序会影响最终的结果,也就是说旋转之后的平移和平移之后的旋转结果可能不同。
5.2 3D变换
● CG的前缀告诉我们,
CGAffineTransform
类型属于Core Graphics框架,Core Graphics实际上是一个严格意义上的2D绘图API,并且CGAffineTransform
仅仅对2D变换有效。● 在第三章中,我们提到了
zPosition
属性,可以用来让图层靠近或者远离相机(用户视角),图层的transform
属性(CATransform3D
类型)可以真正做到这点,即让图层在3D空间内移动或者旋转。CATransform3D
是一个可以在3维空间内做变换的4x4的矩阵(图5.6)。
● 和Core Graphics的函数类似,但是3D的平移和旋转多处了一个
z
参数,并且旋转函数除了angle
之外多出了x
,y
,z
三个参数,分别决定了每个坐标轴方向上的旋转:CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z) CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz) CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
绕y轴旋转45度的视图,看起来图层并没有被旋转,而是仅仅在水平方向上的一个压缩,其实完全没错,视图看起来更窄实际上是因为我们在用一个斜向的视角看它,而不是透视。
透视投影
● 为了做一些修正,我们需要引入投影变换(又称作z变换)来对除了旋转之外的变换矩阵做一些修改,Core Animation并没有给我们提供设置透视变换的函数,因此我们需要手动修改矩阵值,幸运的是,很简单:(对我来说一点也不简单)
●
CATransform3D
的透视效果通过一个矩阵中一个很简单的元素来控制:m34
。m34
(图5.9)用于按比例缩放X和Y的值来计算到底要离视角多远。
m34
的默认值是0,我们可以通过设置m34
为-1.0 /d
来应用透视效果,d
代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。因为视角相机实际上并不存在,所以可以根据屏幕上的显示效果自由决定它放置的位置。
通常500-1000就已经很好了,但对于特定的图层有时候更小或者更大的值会看起来更舒服,减少距离的值会增强透视效果,所以一个非常微小的值会让它看起来更加失真,然而一个非常大的值会让它基本失去透视效果,对视图应用透视的代码见清单5.5,结果见图5.10。
transform.m34 = - 1.0 / 500.0;
灭点
● 当在透视角度绘图的时候,远离相机视角的物体将会变小变远,当远离到一个极限距离,它们可能就缩成了一个点,于是所有的物体最后都汇聚消失在同一个点。
在现实中,这个点通常是视图的中心(图5.11),于是为了在应用中创建拟真效果的透视,这个点应该聚在屏幕中点,或者至少是包含所有3D对象的视图中点。
● Core Animation定义了这个点位于变换图层的
anchorPoint
(通常位于图层中心,但也有例外,见第三章)。这就是说,当图层发生变换时,这个点永远位于图层变换之前anchorPoint
的位置。● 当改变一个图层的
position
,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34
来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position
),这样所有的3D图层都共享一个灭点。
sublayerTransform
属性● 如果有多个视图或者图层,每个都做3D变换,那就需要分别设置相同的m34值,并且确保在变换之前都在屏幕中央共享同一个
position
● 这里有一个更好的方法,
CALayer
有一个属性叫做sublayerTransform
。它也是CATransform3D
类型,但和对一个图层的变换不同,它影响到所有的子图层。这意味着你可以一次性对包含这些图层的容器做变换,于是所有的子图层都自动继承了这个变换方法。它会带来另一个显著的优势:灭点被设置在容器图层的中点,从而不需要再对子图层分别设置了。
背面
● 如果我们在清单5.4中把角度修改为
M_PI
(180度)而不是当前的M_PI_4
(45度),那么将会把图层完全旋转一个半圈,于是完全背对了相机视角。视图的背面,是一个镜像对称的图片。见图5.14●
CALayer
有一个叫做doubleSided
的属性来控制图层的背面是否要被绘制。这是一个BOOL
类型,默认为YES
,如果设置为NO
,那么当图层正面从相机视角消失的时候,它将不会被绘制。
扁平化图层
● 这是由于尽管Core Animation图层存在于3D空间之内,但它们并不都存在同一个3D空间。每个图层的3D场景其实是扁平化的,当你从正面观察一个图层,看到的实际上由子图层创建的想象出来的3D场景,但当你倾斜这个图层,你会发现实际上这个3D场景仅仅是被绘制在图层的表面。实际上仅仅是把屏幕做了一次倾斜,或许在游戏中可以看见有一面墙在你面前,但是倾斜屏幕并不能够看见墙里面的东西。
反正我不知道说的是什么,反正就是三维的时候绕Y轴和X轴旋转时,有扁平化问题就是了。
● 至少当你用正常的
CALayer
的时候是这样,CALayer
有一个叫做CATransformLayer
的子类来解决这个问题。具体在第六章“特殊的图层”中将会具体讨论。
5.4 固体对象
● 创建一个固态的3D对象(实际上是一个技术上所谓的空洞对象,但它以固态呈现)。我们用六个独立的视图来构建一个“立方体”的各个面。固态在这里就是固体,立体的意思。
搭建立方体某一个面的代码:
//add cube face 2 transform = CATransform3DMakeTranslation(100, 0, 0); transform = CATransform3DRotate(transform, M_PI_2, 0, 1, 0); [self addFace:1 withTransform:transform];
● 从另一个角度去观察立方体,就能看出它的真实面貌(图5.21)。
添加如下几行去旋转
containerView
图层的perspective
变换矩阵即可:perspective = CATransform3DRotate(perspective, -M_PI_4, 1, 0, 0); perspective = CATransform3DRotate(perspective, -M_PI_4, 0, 1, 0);
光亮和阴影
●Core Animation可以用3D显示图层,但是它对光线并没有概念。如果想让立方体看起来更加真实,需要自己做一个阴影效果。你可以通过改变每个面的背景颜色或者直接用带光亮效果的图片来调整。
● 如果需要动态地创建光线效果,你可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但为了计算阴影图层的不透明度,你需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量叉乘结果。叉乘代表了光源和图层之间的角度,从而决定了它有多大程度上的光亮。
清单5.10实现了这样一个结果,我们用GLKit框架来做向量的计算(你需要引入GLKit库来运行代码),每个面的
CATransform3D
都被转换成GLKMatrix4
,然后通过GLKMatrix4GetMatrix3
函数得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。
关键代码:
CATransform3D transform = face.transform; GLKMatrix4 matrix4 = *(GLKMatrix4 *)&transform; GLKMatrix3 matrix3 = GLKMatrix4GetMatrix3(matrix4);
点击事件
● 即使我们看不见4,5,6的表面(因为被1,2,3遮住了),iOS在事件响应上仍然保持之前的顺序。当试图点击表面3上的按钮,表面4,5,6截断了点击事件(取决于点击的位置),这就和普通的2D布局在按钮上覆盖物体一样。
● 这里有几种正确的方案:把除了表面3的其他视图
userInteractionEnabled
属性都设置成NO
来禁止事件传递。或者简单通过代码把视图3覆盖在视图6上。
6. 专用图层
● Core Animation图层不仅仅能作用于图片和颜色而已。本章就会学习其他的一些图层类,进一步扩展使用Core Animation绘图的能力。
6.1 CAShapeLayer
● 在第四章『视觉效果』我们学习到了不使用图片的情况下用
CGPath
去构造任意形状的阴影。如果我们能用同样的方式创建相同形状的图层就好了。●
CAShapeLayer
是一个通过矢量图形而不是bitmap来绘制的图层子类。你指定诸如颜色和线宽等属性,用CGPath
来定义想要绘制的图形,最后CAShapeLayer
就自动渲染出来了。
创建一个
CGPath
●
CAShapeLayer
可以用来绘制所有能够通过CGPath
来表示的形状,你可以控制一些属性比如lineWith
(线宽,用点表示单位),lineCap
(线条结尾的样子),和lineJoin
(线条之间的结合点的样子);但是在图层层面你只有一次机会设置这些属性。如果你想用不同颜色或风格来绘制多个形状,就不得不为每个形状准备一个图层了。关键代码:
//create path UIBezierPath *path = [[UIBezierPath alloc] init]; [path moveToPoint:CGPointMake(175, 100)];  [path addArcWithCenter:CGPointMake(150, 100) radius:25 startAngle:0 endAngle:2*M_PI clockwise:YES]; [path moveToPoint:CGPointMake(150, 125)]; [path addLineToPoint:CGPointMake(150, 175)]; [path addLineToPoint:CGPointMake(125, 225)]; [path moveToPoint:CGPointMake(150, 175)]; [path addLineToPoint:CGPointMake(175, 225)]; //create shape layer CAShapeLayer *shapeLayer = [CAShapeLayer layer]; shapeLayer.path = path.CGPath; //add it to our view [self.containerView.layer addSublayer:shapeLayer];
圆角
●
第四章里面提到了
CALayer
为创建圆角视图提供了一个方法,就是CALayer
的cornerRadius
属性。虽然使用CAShapeLayer
类需要更多的工作,但是它有一个优势就是可以单独指定每个角。事实上
UIBezierPath
有自动绘制圆角矩形的构造方法//define path parameters CGRect rect = CGRectMake(50, 50, 100, 100); CGSize radii = CGSizeMake(20, 20); UIRectCorner corners = UIRectCornerTopRight | UIRectCornerBottomRight | UIRectCornerBottomLeft; //create path UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect byRoundingCorners:corners cornerRadii:radii];
6.2 CATextLayer
● Core Animation提供了一个CALayer
的子类CATextLayer
,它以图层的形式包含了UILabel
几乎所有的绘制特性,并且额外提供了一些新的特性。
6.3 CATransformLayer
● 当我们在构造复杂的3D事物的时候,如果能够组织独立元素就太方便了。CATransformLayer
解决了这个问题, CATransformLayer
不同于普通的CALayer
,因为它不能显示它自己的内容。只有当存在了一个能作用域子图层的变换它才真正存在。CATransformLayer
并不平面化它的子图层,所以它能够用于构造一个层级的3D结构。
6.4 CAGradientLayer
●
CAGradientLayer
是用来生成两种或更多颜色平滑渐变的。用Core Graphics复制一个CAGradientLayer
并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer
的真正好处在于绘制使用了硬件加速。
基础渐变
● 我们将从一个简单的红变蓝的对角线渐变开始(见清单6.6).这些渐变色彩放在一个数组中,并赋给
colors
属性。这个数组成员接受CGColorRef
类型的值(并不是从NSObject
派生而来),所以我们要用通过bridge转换以确保编译正常。●
CAGradientLayer
也有startPoint
和endPoint
属性,他们决定了渐变的方向。这两个参数是以单位坐标系进行的定义,所以左上角坐标是{0, 0},右下角坐标是{1, 1}。代码运行结果如图6.6关键代码:
//create gradient layer and add it to our container view CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = self.containerView.bounds; [self.containerView.layer addSublayer:gradientLayer]; //set gradient colors gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id)[UIColor blueColor].CGColor]; //set gradient start and end points gradientLayer.startPoint = CGPointMake(0, 0); gradientLayer.endPoint = CGPointMake(1, 1);
多重渐变
● 默认情况下,这些颜色在空间上均匀地被渲染,但是我们可以用
locations
属性来调整空间。locations
属性是一个浮点数值的数组(以NSNumber
包装)。这些浮点数定义了colors
属性中每个不同颜色的位置,同样的,也是以单位坐标系进行标定。0.0代表着渐变的开始,1.0代表着结束。
locations
数组并不是强制要求的,但是如果你给它赋值了就一定要确保locations
的数组大小和colors
数组大小一定要相同,否则你将会得到一个空白的渐变。关键代码:
//create gradient layer and add it to our container view CAGradientLayer *gradientLayer = [CAGradientLayer layer]; gradientLayer.frame = self.containerView.bounds; [self.containerView.layer addSublayer:gradientLayer]; //set gradient colors gradientLayer.colors = @[(__bridge id)[UIColor redColor].CGColor, (__bridge id) [UIColor yellowColor].CGColor, (__bridge id)[UIColor greenColor].CGColor]; //set locations gradientLayer.locations = @[@0.0, @0.25, @0.5]; //set gradient start and end points gradientLayer.startPoint = CGPointMake(0, 0); gradientLayer.endPoint = CGPointMake(1, 1);
6.5 CAReplicatorLayer(复制器)
● CAReplicatorLayer
的目的是为了高效生成许多相似的图层。它会绘制一个或多个图层的子图层,并在每个复制体上应用不同的变换。
6.6 CAScrollLayer(滑动)
● 对于一个未转换的图层,它的bounds
和它的frame
是一样的,frame
属性是由bounds
属性自动计算而出的,所以更改任意一个值都会更新其他值。比如说,你可能有一个很大的图片,你希望用户能够随意滑动,或者是一个数据或文本的长列表。
● CAScrollLayer
有一个-scrollToPoint:
方法,它自动适应bounds
的原点以便图层内容出现在滑动的地方。注意,这就是它做的所有事情。CAScrollLayer
并不负责将触摸事件转换为滑动事件,既不渲染滚动条,也不实现任何iOS指定行为例如滑动反弹(当视图滑动超多了它的边界的将会反弹回正确的地方)。
6.7 CATiledLayer(平铺)
● CATiledLayer
为载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入。
6.8 CAEmitterLayer
● 在iOS 5中,苹果引入了一个新的CALayer
子类叫做CAEmitterLayer
。CAEmitterLayer
是一个高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。
6.9 CAEAGLLayer
● 当iOS要处理高性能图形绘制,必要时就是OpenGL。应该说它应该是最后的杀手锏,至少对于非游戏的应用来说是的。因为相比Core Animation和UIkit框架,它不可思议地复杂。
● OpenGL提供了Core Animation的基础,它是底层的C接口,直接和iPhone,iPad的硬件通信,极少地抽象出来的方法。OpenGL没有对象或是图层的继承概念。它只是简单地处理三角形。OpenGL中所有东西都是3D空间中有颜色和纹理的三角形。用起来非常复杂和强大,但是用OpenGL绘制iOS用户界面就需要很多很多的工作了。
● 在iOS 5中,苹果引入了一个新的框架叫做GLKit,它去掉了一些设置OpenGL的复杂性,提供了一个叫做CLKView
的UIView
的子类,帮你处理大部分的设置和绘制工作。前提是各种各样的OpenGL绘图缓冲的底层可配置项仍然需要你用CAEAGLLayer
完成,它是CALayer
的一个子类,用来显示任意的OpenGL图形。大部分情况下你都不需要手动设置CAEAGLLayer
(假设用GLKView)。用OpenGL渲染CAEAGLLayer
图层。
6.10 AVPlayerLayer
● 最后一个图层类型是AVPlayerLayer
。尽管它不是Core Animation框架的一部分(AV前缀看上去像),AVPlayerLayer
是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer
子类来显示自定义的内容类型。
● AVPlayerLayer
是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer
的底层实现,提供了显示视频的底层控制。AVPlayerLayer
的使用相当简单:你可以用+playerLayerWithPlayer:
方法创建一个已经绑定了视频播放器的图层,或者你可以先创建一个图层,然后用player
属性绑定一个AVPlayer
实例。