[toc]
前言
Core Animation
提供了一种通用系统,可对应用程序的视图和其他视觉元素进行动画处理。Core Animation
不能替代您的应用程序视图。相反,它是一种与视图集成以提供更好的性能和动画效果的技术。它通过将视图的内容缓存到可以由图形硬件直接操纵的位图中来实现此行为。在某些情况下,这种缓存行为可能需要您重新考虑如何呈现和管理应用程序的内容,但是大多数情况下,您在不知道它存在的情况下使用Core Animation
。除了缓存视图内容之外,Core Animation
还定义了一种方法,该方法可以指定任意视觉内容,将该内容与视图集成以及将其与其他所有内容一起设置为动画
核心动画知识
从图中可以看出,最底层是图形硬件
(GPU)
;上层是OpenGL
和CoreGraphics
,提供一些接口来访问GPU;再上层的CoreAnimation
在此基础上封装了一套动画的API。最上面的UIKit
属于应用层,处理与用户的交互。所以,学习这CoreAnimation`也会涉及一些图形学的知识,了解这些有助于我们更顺手的使用以及更高效的解决问题。
- Core Animation 结构图
Core Animation Introduction
- 简单易易⽤用的⾼高性能混合编程模型
- ⽤用类似于视图⼀一样,使⽤用图层来创建复杂的编程接⼝口
- 轻量量化的数据结构,它可以同时显示让上百个图层产⽣生动画效果
- 一套⾮非常较简单的动画接⼝口,能让动画运⾏行行在独⽴立的线程中,并可以独⽴立于主线程之外.
- 一旦动画配置完成并启动,核⼼心动画就能独⽴立并完全控制相应的动画帧.
- 提⾼高应⽤用性能.应⽤用程序只有当发⽣生改变的时候才会重绘内容. 使⽤用
Core Animation
可以不不使⽤用其他图形API
,例例如OpenGL
来获取⾼高效的动画性能. - . 灵活的布局管理理模型,允许图层相对同级图层的关系来设置属性的位置和⼤大⼩小
核心动画图层树结构
使用
Core Animation
的应用程序具有三组
图层对象.
-
图层树
是最的那些您的应用程序进行交互。该树中的对象是存储任何动画的目标值的模型对象。每当更改图层的属性时,都将使用这些对象之一。 -
呈现树
中的对象包含任何正在运行的动画的运行中值。图层树对象包含动画的目标值,而表示树中的对象则反映屏幕上显示的当前值。您永远不要修改此树中的对象。相反,您可以使用这些对象读取当前动画值,也许可以从这些值开始创建新动画。 -
渲染树
中的对象执行实际的动画,并且是Core Animation
专有的。
与窗口关联的图层树
三组图层对象
图层与视图
一个视图就是在屏幕上显示的一个矩形块
(比如图片,文字或者视频),它能够拦截类似于鼠标点击或者触摸手势等用户输入。视图在层级关系中可以互相嵌套,一个视图可以管理它的所有子视图的位置
.
在
iOS
当中,所有的视图都从一个叫做UIVIew
的基类派生而来,UIView
可以处理触摸事件,可以支持基于Core Graphics
绘图,可以做仿射变换(例如旋转或者缩放),或者简单的类似于滑动或者渐变的动画。
图层与视图之间的关系
图层不能替代
您应用程序的视图
,也就是说,您不能仅基于图层对象创建可视界面。图层为您的视图提供基础结构。特别是,图层使绘制和动画化视图内容并使其保持动画状态并保持较高的帧速率更加容易和有效。但是,许多事情是图层无法做到的。层不处理事件,绘制内容,参与响应者链或执行其他许多事情。因此,每个应用程序仍必须具有一个或多个视图来处理这些类型的交互.
CALayer
CALayer
类在概念上和UIView
类似,同样也是一些被层级关系树管理的矩形块,同样也可以包含一些内容(像图片,文本或者背景色),管理子图层的位置。它们有一些方法和属性用来做动画和变换。和UIView
最大的不同是CALayer
不处理用户的交互。
CALayer
并不清楚具体的响应链(iOS
通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,即使它提供了一些方法来判断是否一个触点在图层的范围之内
.
平行的层级关系
每一个UIview
都有一个CALayer
实例的图层属性,也就是所谓的backing layer
,视图的职责就是创建并管理这个图层,以确保当子视图在层级关系中添加或者被移除的时候,他们关联的图层也同样对应在层级关系树当中有相同的操作
.
实际上这些背后关联的图层才是真正用来在屏幕上显示和做动画,
UIView
仅仅是对它的一个封装,提供了一些iOS类似于处理触摸的具体功能,以及Core Animation
底层方法的高级接口.
但是为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢?
为什么不用一个简单的层级来处理所有事情呢?
原因在于要做职责分离
,这样也能避免很多重复代码。在iOS
和Mac OS
两个平台上,事件和用户交互有很多地方的不同,基于多点触控的用户界面和基于鼠标键盘有着本质的区别,这就是为什么iOS
有UIKit
和UIView
,但是Mac OS
有AppKit
和NSView
的原因。他们功能上很相似,但是在实现上有着显著的区别
图层的能力
我们已经证实了图层不能像视图那样处理触摸事件,那么他能做哪些视图不能做的呢?这里有一些UIView
没有暴露出来的CALayer
的功能:
- 阴影,圆角,带颜色的边框
-
3D
变换 - 非矩形范围
- 透明遮罩
- 多级非线性动画
简单使用图层
在屏幕上添加一个蓝色view,可以通过设置CALayer
的backgroundColor
CALayer *layer = [CALayer layer];
layer.frame = CGRectMake(100, 100, 100, 100);
layer.backgroundColor = [UIColor greenColor].CGColor;
_layer = layer;
[self.view.layer addSublayer:layer];
然而,当满足以下条件的时候,你可能更需要使用CALayer
而不是UIView
:
- 开发同时可以在
Mac OS
上运行的跨平台应用 - 使用多种
CALayer
的子类(见第六章,“特殊的图层“),并且不想创建额外的UIView
去包封装它们所有 - 做一些对性能特别挑剔的工作,比如对
UIView
一些可忽略不计的操作都会引起显著的不同(尽管如此,你可能会直接想使用OpenGL绘图)
CALayer 常用属性详解
contents
CALayer
有一个属性叫做contents
,这个属性的类型被定义为id,意味着它可以是任何类型的对象。在这种情况下,你可以给contents
属性赋任何值,你的app
仍然能够编译通过。但是,在实践中,如果你给contents
赋的不是CGImage
,那么你得到的图层将是空白的。
- 如果要给图层的寄宿图赋值
UIImage *image = [UIImage imageNamed:@"test.png"];
//add it directly to our view's layer
self.layerView.layer.contents = (__bridge id)image.CGImage;
contents
这个奇怪的表现是由Mac OS
的历史原因造成的。它之所以被定义为id
类型,是因为在Mac OS
系统上,这个属性对CGImage
和NSImage
类型的值都起作用。如果你试图在iOS
平台上将UIImag
e的值赋给它,只能得到一个空白的图层。一些初识Core Animation
的iOS
开发者可能会对这个感到困惑
contentsGravity
CALayer
与contentMode
对应的属性叫做contentsGravity
,但是它是一个NSString
类型,而不是像对应的UIKit
部分,那里面的值是枚举。contentsGravity
可选的常量值有以下一些:
* kCAGravityCenter
* kCAGravityTop
* kCAGravityBottom
* kCAGravityLeft
* kCAGravityRight
* kCAGravityTopLeft
* kCAGravityTopRight
* kCAGravityBottomLeft
* kCAGravityBottomRight
* kCAGravityResize
* kCAGravityResizeAspect
* kCAGravityResizeAspectFill
- 使用
self.layerView.layer.contentsGravity = kCAGravityResizeAspect;
contentsScale
contentsScale
属性定义了寄宿图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数。
如果contentsScale
设置为1.0
,将会以每个点1
个像素绘制图片,如果设置为2.0
,则会以每个点2
个像素绘制图片,这就是我们熟知的Retina
屏幕。
- 使用
当我们使用
UIImage
类去读取我们的雪人图片的时候,他读取了高质量的Retina
版本的图片。但是当我们用CGImage
来设置我们的图层的内容时,拉伸这个因素在转换的时候就丢失了。不过我们可以通过手动设置contentsScale
来修复这个问题
//set the contentsScale to match image
self.layerView.layer.contentsScale = image.scale;
maskToBounds
UIView
有一个叫做clipsToBounds
的属性可以用来决定是否显示超出边界的内容,CALayer
对应的属性叫做masksToBounds
.
contentsRect
CALayer
的contentsRect
属性允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的,所以要比contentsGravity
灵活多了
和bounds
,frame
不同,contentsRect
不是按点来计算的,它使用了单位坐标,单位坐标指定在0
到1
之间,是一个相对值(像素和点就是绝对值)。所以他们是相对与寄宿图的尺寸的。iOS使用了以下的坐标系统:
点 —— 在
iOS
和Mac OS
中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina
设备上,一个点等于2*2
个像素。iOS
用点作为屏幕的坐标测算体系就是为了在Retina
设备和普通设备上能有一致的视觉效果。像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。
UIImage
是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage
就会使用像素,所以你要清楚在Retina
设备和普通设备上,他们表现出来了不同的大小。单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在
OpenGL
这种纹理坐标系统中用得很多,Core Animation
中也用到了单位坐标。
默认的contentsRect是{0, 0, 1, 1},这意味着整个寄宿图默认都是可见的,如果我们指定一个小一点的矩形,图片就会被裁剪
contentsCenter
contentsCenter
其实是一个CGRect
,它定义了一个固定的边框和一个在图层上可拉伸的区域。
self.view.layer.contentsCenter = CGRectMake(0, 0, 1, 1);
self.view.layer.contentsCenter = CGRectMake(0, 0, 0.5, 0.5);
图层几何学
UIView
有三个比较重要的布局属性:frame
,bounds
和center
,CALayer
对应地叫做frame
,bounds
和position
。为了能清楚区分,图层用了“position”
,视图用了“center”
,但是他们都代表同样的值。
如图:
anchorPoint
图层的anchorPoint
通过position
来控制它的frame
的位置,你可以认为anchorPoint是用来移动图层的把柄。
anchorPoint
相当于支点,可以用作旋转变化、平移、缩放.
如果修改anchorPoint
则layer
的frame
会发生改变,position
不会发生改变.修改position
与anchorPoint
中任何一个属性都不影响另一个属性.
- 视图frame计算公式:
frame.origin.x = position.x - anchorPoint.x * bounds.size.width;
frame.origin.y = position.y - anchorPoint.y * bounds.size.height;
坐标系
Core Graphics
源于Mac OS X
系统,在Mac OS X中,坐标原点在左下方并且正
y坐标是朝上的,而在
iOS中,原点坐标是在左上方并且正
y坐标是朝下的。在大多数情况下,这不会出现任何问题,因为图形上下文的坐标系统是会自动调节补偿的。但是创建和绘制一个
CGImage`对象时就会暴露出倒置问题.
ZPosition
和UIView
严格的二维坐标系不同,CALayer
存在于一个三维空间当中。除了我们已经讨论过的position
和anchorPoint
属性之外,CALayer
还有另外两个属性,zPosition
和anchorPointZ
,二者都是在Z
轴上描述图层位置的浮点类型。
此属性的默认值为0
。更改此属性的值会更改屏幕上各层的从前到后的顺序。较高的值比较低的值在视觉上更靠近该图层
CALayer中HitTest属性的实际使用
响应者对象(Responder Object
),顾名思义,指的是有响应和处理事件能力的对象。响应者链就是由一系列的响应者对象构成的一个层次结构。
第一响应者(First responder
)指的是当前接受触摸的响应者对象(通常是一个UIView
对象),即表示当前该对象正在与用户交互,它是响应者链的开端。整个响应者链和事件分发的使命都是找出第一响应者。
-
hitTest:withEven:
方法的处理流程如下
1. 首先调用当前视图的
pointInside:withEvent:`方法判断触摸点是否在当前视图内;
若返回NO
,则hitTest:withEvent
:返回nil
;
若返回YES
,则向当前视图的所有子视图(subviews
)发送hitTest:withEvent:
消息,所有子视图的遍历顺序是从最顶层视图一直到到最底层视图,即从subviews
数组的末尾向前遍历,直到有子视图返回非空对象或者全部子视图遍历完毕.
如图:
-
A
是UIWindow
的根视图,因此,UIWindwo
对象会首相对A
进行hit-test
;
2、显然用户点击的范围是在A
的范围内,因此,pointInside:withEvent:
返回了YES
,这时会继续检查A
的子视图;
3、这时候会有两个分支,B
和C
:
点击的范围不再B
内,因此B
分支的pointInside:withEvent:
返回NO
,对应的hitTest:withEvent:
返回nil
;
点击的范围在C
内,即C
的pointInside:withEvent:
返回YES;
4、这时候有D
和E
两个分支:
点击的范围不再D
内,因此D
的pointInside:withEvent:
返回NO
,对应的hitTest:withEvent:
返回nil
;
点击的范围在E
内,即E
的pointInside:withEvent:
返回YES,由于E没有子视图(也可以理解成对E的子视图进行hit-test
时返回了nil
),因此,E
的hitTest:withEvent:
会将E
返回,再往回回溯,就是C
的hitTest:withEvent:
返回E--->>A
的hitTest:withEvent:
返回E
。
至此,本次点击事件的第一响应者就通过响应者链的事件分发逻辑成功的找到了。
若第一次有子视图返回非空对象,则
hitTest:withEvent:
方法返回此对象,处理结束;
如所有子视图都返回非,则hitTest:withEvent:
方法返回自身(self
)。
如果最终
hit-test
没有找到第一响应者,或者第一响应者没有处理该事件,则该事件会沿着响应者链向上回溯,如果UIWindow
实例和UIApplication
实例都不能处理该事件,则该事件会被丢弃;
-
hitTest:withEvent:
方法将会忽略
1 .忽略隐藏(hidden=YES
)的视图
禁止用户操作(
userInteractionEnabled=YES
)的视图alpha级别小于0.01(
alpha<0.01
)的视图如果一个子视图的区域超过父视图的
bound
区域(父视图的clipsToBounds
属性为NO
,这样超过父视图bound
区域的子视图内容也会显示).
那么正常情况下对子视图在父视图之外区域的触摸操作不会被识别,因为父视图的
pointInside:withEvent:
方法会返回NO
,这样就不会继续向下遍历子视图了。当然,也可以重写pointInside:withEvent:
方法来处理这种情况。
Hit Testing 应⽤用场景
- 事件穿透
- ⼦子视图超出父视图范围
仿射变换数学原理讲解
仿射变换 AffineTransform
,在iOS中的实现类是CGAffineTransform
和CATransform3D
,很多动画效果都需要用到仿射去完成,可以说仿射是动画基础.
原理是利用矩阵的相乘,得到变换后的坐标系.
CGAffineTransform
-
CGAffineTransform
是一个可以和二维空间向量(例如CGPoint
)做乘法的3X2的矩阵
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
- 使用
CGAffineTransform transform = CGAffineTransformMakeRotation(M_PI_4);
self.layerView.layer.affineTransform = transform;
混合变换
Core Graphics
提供了一系列的函数可以在一个变换的基础上做更深层次的变换,如果做一个既要缩放又要旋转的变换.
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
当操纵一个变换的时候,初始生成一个什么都不做的变换很重要--也就是创建一个CGAffineTransform
类型的空值,矩阵论中称作单位矩阵,Core Graphics
同样也提供了一个方便的常量:
CGAffineTransformIdentity
- 使用
CGAffineTransform transform = CGAffineTransformIdentity;
//scale by 50%
transform = CGAffineTransformScale(transform, 0.5, 0.5);
//rotate by 30 degrees
transform = CGAffineTransformRotate(transform, M_PI / 180.0 * 30.0);
//translate by 200 points
transform = CGAffineTransformTranslate(transform, 200, 0);
//apply transform to layer
self.layerView.layer.affineTransform = transform;
CATransform3D
CATransform3D也是一个矩阵,但是和2x3的矩阵不同,CATransform3D是一个可以在3维空间内做变换的4x4的矩阵(图5.6)。
苹果提供的可变换的矩阵:
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)