书写的很好,翻译的也棒!感谢译者,感谢感谢!
iOS-Core-Animation-Advanced-Techniques
CALayer
和UIView最大的不同是CALayer不处理用户的交互。响应链(iOS通过视图层级关系用来传送触摸事件的机制)
但是为什么iOS要基于UIView和CALayer提供两个平行的层级关系呢?为什么不用一个简单的层级来处理所有事情呢?原因在于要做职责分离,这样也能避免很多重复代码。
CALayer的功能
阴影,圆角,带颜色的边框
3D变换
非矩形范围
透明遮罩
多级非线性动画
更需要使用CALayer
开发同时可以在Mac OS上运行的跨平台应用
使用多种CALayer的子类,并且不想创建额外的UIView去包封装它们所有。
做一些对性能特别挑剔的工作,比如对UIView一些可忽略不计的操作都会引起显著的不同(尽管如此,你可能会直接想使用OpenGL绘图)
<1> contents属性
self.layerView.layer.contents= (__bridge id)image.CGImage;
<2> contentGravity ≈ contentMode
self.layerView.layer.contentsGravity =kCAGravityResizeAspect;
<3> contentsScale -- (用来决定图层内容应该以怎样的分辨率来渲染不关心屏幕的拉伸因素而总是默认为1.0)。
contentsScale的目的并不是那么明显。因为contents由于设置了contentsGravity属性,所以它已经被拉伸以适应图层的边界。
contentsScale属性其实属于支持高分辨率(又称Hi-DPI或Retina)屏幕机制的一部分。它用来判断在绘制图层的时候应该为寄宿图创建的空间大小,和需要显示的图片的拉伸度(假设并没有设置contentsGravity属性)。
当用代码的方式来处理寄宿图的时候,一定要记住要手动的设置图层的contentsScale属性,否则,你的图片在Retina设备上就显示得不正确啦。代码如下:
layer.contentsScale = [UIScreenmainScreen].scale; // 以Retina的质量来显示文字。
<4> maskToBounds ≈ clipsToBounds
UIView有一个叫做clipsToBounds的属性可以用来决定是否显示超出边界的内容,CALayer对应的属性叫做masksToBounds。
<5> contentsRect -- 图片拼合
点 —— 在iOS和Mac OS中最常见的坐标体系。点就像是虚拟的像素,也被称作逻辑像素。在标准设备上,一个点就是一个像素,但是在Retina设备上,一个点等于2*2个像素。iOS用点作为屏幕的坐标测算体系就是为了在Retina设备和普通设备上能有一致的视觉效果。
像素 —— 物理像素坐标并不会用来屏幕布局,但是仍然与图片有相对关系。UIImage是一个屏幕分辨率解决方案,所以指定点来度量大小。但是一些底层的图片表示如CGImage就会使用像素,所以你要清楚在Retina设备和普通设备上,它们表现出来了不同的大小。
单位 —— 对于与图片大小或是图层边界相关的显示,单位坐标是一个方便的度量方式, 当大小改变的时候,也不需要再次调整。单位坐标在OpenGL这种纹理坐标系统中用得很多,Core Animation中也用到了单位坐标。
用于 图片拼合 :
layer.contents= (__bridgeid)image.CGImage;
layer.contentsGravity=kCAGravityResizeAspect;
layer.contentsRect= rect;
<6> contentsCenter -- 默认{0, 0, 1, 1} -- 可拉伸视图
contentsCenter其实是一个CGRect,它定义了一个固定的边框和一个在图层上可拉伸的区域。
layer.contents = (__bridgeid)image.CGImage;
layer.contentsCenter = rect;
(1)-drawRect:
(2)CALayer -- CALayerDelegate
- (void)displayLayer:(CALayer *)layer;
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx; -- CALayer创建了一个合适尺寸的空寄宿图(尺寸由bounds和contentsScale决定)和一个Core Graphics的绘制上下文环境
[layer display]; -- 强制重画
UIView -- frame,bounds,center => CALayer -- frame,bounds,position
center和position都代表了相对于父图层anchorPoint所在的位置。
视图的frame,bounds和center属性仅仅是存取方法,当操纵视图的frame,实际上是在改变位于视图下方CALayer的frame,不能够独立于图层之外改变视图的frame。
frame 是根据bounds,position和transform计算而来。
anchorPoint -- 锚点
图层的 anchorPoint 通过 position 来控制它的 frame 的位置,你可以认为anchorPoint是用来移动图层的把柄。
坐标系 -- 检测点击图层间会相互转换坐标
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;
翻转坐标系
geometryFlipped = YES
Z坐标轴
和UIView严格的二维坐标系不同,CALayer存在于一个三维空间当中。
CALayer还有另外两个属性,zPosition 和 anchorPointZ
最实用的功能就是改变图层的显示顺序
self.greenView.layer.zPosition=1.0f;
- containsPoint:
-hitTest:
zPosition属性可以明显改变屏幕上图层的顺序,但不能改变事件传递的顺序。
自动布局 -- Core Animation对自动调整和自动布局支持的缺乏。
CALayerDelegate如下函数:
- (void) layoutSublayersOfLayer:(CALayer *) layer;
"不能像`UIView`的`autoresizingMask`和`constraints`属性做到自适应屏幕旋转。"这也是为什么最好使用视图而不是单独的图层来构建应用程序的另一个重要原因之一。
圆角 -- conrnerRadius
默认情况下,这个曲率值只影响背景颜色而不影响背景图片或是子图层。不过,如果把masksToBounds = YES的话,图层里面的所有东西都会被截取。
图层边框 -- borderWidth,borderColor
阴影 -- shadowOpacity,shadowOffset,shadowRadius,shadowColor
shadowRadius属性控制着阴影的模糊度,当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然。
Tip:阴影裁剪
和图层边框不同,图层的阴影继承自内容的外形,而不是根据边界和角半径来确定。
阴影和裁剪限制:阴影通常就是在Layer的边界之外,如果你开启了masksToBounds属性,所有从图层中突出来的内容都会被才剪掉。maskToBounds属性裁剪掉了阴影和内容
你需要用到两个图层:一个只画阴影的空的外图层,和一个用masksToBounds裁剪内容的内图层。
shadowPath
如果是一个矩形或者是圆,用CGPath会相当简单明了。但是如果是更加复杂一点的图形,UIBezierPath类会更合适,它是一个由UIKit提供的在CGPath基础上的Objective-C包装类。
图层蒙板
CALayer有一个属性叫做 mask 可以解决这个问题。这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。
mask图层 真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。
CALayer蒙板图层真正厉害的地方在于蒙板图不局限于静态图。任何有图层构成的都可以作为mask属性,这意味着你的蒙板可以通过代码甚至是动画实时生成。
拉伸过滤
minificationFilter 和 magnificationFilter
当图片需要显示不同的大小的时候,有一种叫做拉伸过滤的算法就起到作用了。它作用于原图的像素上并根据需要生成新的像素显示在屏幕上。
CALayer为此提供了三种拉伸过滤方法,
kCAFilterLinear -- 默认,采用双线性滤波算法,它在大多数情况下都表现良好。双线性滤波算法通过对多个像素取样最终生成新的值,得到一个平滑的表现不错的拉伸。但是当放大倍数比较大的时候图片就模糊不清了。
kCAFilterNearest -- 三线性滤波算法存储了多个大小情况下的图片(也叫多重贴图),并三维取样,同时结合大图和小图的存储进而得到最后的结果。
kCAFilterTrilinear 最近过滤 -- 就是取样最近的单像素点而不管其他的颜色。这样做非常快,也不会使图片模糊。但是,最明显的效果就是,会使得压缩图片更糟,图片放大之后也显得块状或是马赛克严重。
线性过滤保留了形状,最近过滤则保留了像素的差异。
例子!!!
组透明 -- UIViewGroupOpacity && shouldRasterize(rasterizationScale)
UIView有一个叫做alpha的属性来确定视图的透明度。CALayer -- opacity,这两个属性都是影响子层级的。一个问题,透明度的混合叠加。
当你显示一个50%透明度的图层时,图层的每个像素都会一般显示自己的颜色,另一半显示图层下面的颜色。这是正常的透明度的表现。但是如果图层包含一个同样显示50%透明的子图层时,你所看到的视图,50%来自子视图,25%来了图层本身的颜色,另外的25%则来自背景色。
(1)通过设置Info.plist文件中的UIViewGroupOpacity = YES来达到这个效果,但是这个设置会影响到这个应用,整个app可能会受到不良影响。
(2)另一个方法就是,你可以设置CALayer的一个叫做 shouldRasterize = YES 属性来实现组透明的效果,在应用透明度之前,图层及其子图层都会被整合成一个整体的图片,这样就没有透明度混合的问题了。
shouldRasterize -> rasterizationScale 要确保你设置了rasterizationScale属性去匹配屏幕,以防止出现Retina屏幕像素化的问题。
CGAffineTransform && CATransform3D
仿射变换
仿射:平行的两条线在变换之后任然保持平行
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
UIView (transform) == CALayer (affineTransform)
混合变换
CGAffineTransformRotate(CGAffineTransform t, CGFloat angle)
CGAffineTransformScale(CGAffineTransform t, CGFloat sx, CGFloat sy)
CGAffineTransformTranslate(CGAffineTransform t, CGFloat tx, CGFloat ty)
CGAffineTransformIdentity -- 单位矩阵
CGAffineTransformConcat(CGAffineTransform t1, CGAffineTransform t2) -- 混合两个变化矩阵
剪切变换
很少需要直接设置CGAffineTransform的值。除非需要创建一个斜切的变换,Core Graphics并没有提供直接的函数。倾斜
3D变换
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
透视投影
投影变换(又称作z变换)
CATransform3D的m34元素,用来做透视
灭点
Core Animation定义了这个点位于变换图层的 anchorPoint
当改变一个图层的position,你也改变了它的灭点,做3D变换的时候要时刻记住这一点,当你视图通过调整m34来让它更加有3D效果,应该首先把它放置于屏幕中央,然后通过平移来把它移动到指定位置(而不是直接改变它的position),这样所有的3D图层都共享一个灭点。
sublayerTransform属性 -- CATransform3D类型
self.containerView.layer.sublayerTransform= perspective;
背面 -- 旋转M_PI
图层是双面绘制的,反面显示的是正面的一个镜像图片。
CALayer有一个叫做doubleSided的属性来控制图层的背面是否要被绘制。这是一个BOOL类型,默认为YES,如果设置为NO,那么当图层正面从相机视角消失的时候,它将不会被绘制。提升性能。
扁平化图层 -- Y,Z轴抵消操作
-在相同场景下的任何3D表面必须和同样的图层保持一致,这是因为每个的父视图都把它的子视图扁平化了。
固体对象 -- cube
光亮和阴影
可以通过改变每个面的背景颜色或者直接用带光亮效果的图片来调整。
如果需要动态地创建光线效果,你可以根据每个视图的方向应用不同的alpha值做出半透明的阴影图层,但为了计算阴影图层的不透明度,你需要得到每个面的正太向量(垂直于表面的向量),然后根据一个想象的光源计算出两个向量叉乘结果。叉乘代表了光源和图层之间的角度,从而决定了它有多大程度上的光亮。
我们用GLKit框架来做向量的计算(你需要引入GLKit库来运行代码),每个面的CATransform3D都被转换成GLKMatrix4,然后通过GLKMatrix4GetMatrix3函数得出一个3×3的旋转矩阵。这个旋转矩阵指定了图层的方向,然后可以用它来得到正太向量的值。
点击事件
问题在于视图顺序。在第三章中我们简要提到过,点击事件的处理由视图在父视图中的顺序决定的,并不是3D空间中的Z轴顺序。
因为背对相机而隐藏的视图仍然会响应点击事件(这和通过设置hidden属性或者设置alpha为0而隐藏的视图不同,那两种方式将不会响应事件)
隐式动画 -- 平滑过渡
当你改变CALayer的一个可做动画的属性,它并不能立刻在屏幕上体现出来。相反,它是从先前的值平滑过渡到新的值。这一切都是默认的行为,你不需要做额外的操作。
这其实就是所谓的隐式动画。之所以叫隐式是因为我们并没有指定任何动画的类型。我们仅仅改变了一个属性,然后Core Animation来决定如何并且何时去做动画。
改变一个属性 -> 动画执行时间 == 当前事务的设置,动画类型 == 图层行为。
事务 CATransaction
实际上是Core Animation用来包含一系列属性动画集合的机制,任何用指定事务去改变可以做动画的图层属性都不会立刻发生变化,而是当事务一旦提交的时候开始用一个动画过渡到新值。
但是可以用+begin和+commit分别来入栈或者出栈。可以通过+setAnimationDuration:方法设置当前事务的动画时间,或者通过+animationDuration方法来获取值(默认0.25秒)
Core Animation在每个run loop周期中自动开始一次新的事务(run loop是iOS负责收集用户输入,处理定时器或者网络事件并且重新绘制屏幕的东西),即使你不显式的用[CATransaction begin]开始一次事务,任何在一次run loop循环中属性的改变都会被集中起来,然后做一次0.25秒的动画。
UIView有两个方法,+beginAnimations:context:和+commitAnimations,和CATransaction的+begin和+commit方法类似。animateWithDuration:animations
CATranscation -- setCompletionBlock
隐式动画是如何被UIKit禁用掉呢?
Core Animation通常对CALayer的所有属性(可动画的属性)做动画,但是UIView把它关联的图层的这个特性关闭了。
隐式动画实现过程: -- 对应官方文档 动作对象
我们把改变属性时CALayer自动应用的动画称作行为,当CALayer的属性被修改时候,它会调用-actionForKey:方法,传递属性的名称。剩下的操作都在CALayer的头文件中有详细的说明,实质上是如下几步:
(1)图层首先检测它是否有委托,并且是否实现CALayerDelegate协议指定的-actionForLayer:forKey方法。如果有,直接调用并返回结果。
(2)如果没有委托,或者委托没有实现-actionForLayer:forKey方法,图层接着检查包含属性名称对应行为映射的actions字典。
(3)如果actions字典没有包含对应的属性,那么图层接着在它的style字典接着搜索属性名。
(4)最后,如果在style里面也找不到对应的行为,那么图层将会直接调用定义了每个属性的标准行为的-defaultActionForKey:方法。
所以一轮完整的搜索结束之后,-actionForKey:要么返回空(这种情况下将不会有动画发生),要么是CAAction协议对应的对象,最后CALayer拿这个结果去对先前和当前的值做动画。
解释了UIKit是如何禁用隐式动画的:每个UIView对它关联的图层都扮演了一个委托,并且提供了-actionForLayer:forKey的实现方法。当不在一个动画块的实现中,UIView对所有图层行为返回nil,但是在动画block范围之内,它就返回了一个非空值。
返回nil并不是禁用隐式动画唯一的办法,[CATransaction setDisableActions:YES];,可以用来对所有属性打开或者关闭隐式动画。
总结:
(1)UIView关联的图层禁用了隐式动画,对这种图层做动画的唯一办法就是使用UIView的动画函数(而不是依赖CATransaction),或者继承UIView,并覆盖-actionForLayer:forKey:方法,或者直接创建一个显式动画。
(2)对于单独存在的图层,我们可以通过实现图层的-actionForLayer:forKey:委托方法,或者提供一个actions字典来控制隐式动画。
如何创建一个合适的行为对象呢? -- 自定义的actions字典 && 使用委托
推进过渡 CATransition
呈现与模型 - 呈现==定时器动画&点击事件
当你改变一个图层的属性,属性值的确是立刻更新的,但是屏幕上并没有马上发生改变。这是因为你设置的属性并没有直接调整图层的外观,相反,他只是定义了图层动画结束之后将要变化的外观。
Core Animation扮演了一个控制器的角色,并且负责根据图层行为和事务设置去不断更新视图的这些属性在屏幕上的状态。
我们讨论的就是一个典型的微型MVC模式。CALayer是一个连接用户界面(就是MVC中的view)虚构的类,但是在界面本身这个场景下,CALayer的行为更像是存储了视图如何显示和动画的数据模型。实际上,在苹果自己的文档中,图层树通常都是值的图层树模型。
每个图层属性的显示值都被存储在一个叫做呈现图层的独立图层当中,他可以通过-presentationLayer方法来访问。
图层树 -- 呈现树。图层树中所有图层的呈现图层 = 呈现树
NOTE:呈现图层仅仅当图层首次被提交(就是首次第一次在屏幕上显示)的时候创建,所以在那之前调用-presentationLayer将会返回nil。
有一个叫做–modelLayer的方法。在呈现图层上调用–modelLayer将会返回它正在呈现所依赖的CALayer。通常在一个图层上调用-modelLayer会返回–self
两种情况下呈现图层会变得很有用,一个是同步动画,一个是处理用户交互。
(1)如果你在实现一个基于定时器的动画,而不仅仅是基于事务的动画,这个时候准确地知道在某一时刻图层显示在什么位置就会对正确摆放图层很有用了。
(2)如果你想让你做动画的图层响应用户输入,你可以使用-hitTest:方法来判断指定图层是否被触摸,这时候对呈现图层而不是模型图层调用-hitTest:会显得更有意义,因为呈现图层代表了用户当前看到的图层位置,而不是当前动画结束之后的位置。
[self.colorLayer.presentationLayer hitTest:point]
如果修改代码让-hitTest:直接作用于colorLayer而不是呈现图层,点击目标区域方块会变色,因为colorLayer是当前动画结束之后的值。而呈现图层是实时的值。
显式动画
属性动画 -- 基础和关键帧
基础动画:
CABasicAnimation <- CAPropertyAnimation <- CAAnimation (CA所有动画类型的抽象基类)
(1)CAAnimation提供了一个计时函数,一个委托(用于反馈动画状态)以及一个removedOnCompletion,用于标识动画是否该在结束后自动释放(默认YES,为了防止内存泄露)。
CAAction && CAMediaTiming
(2)CABasicAnimation 添加了 id fromValue (动画开始之前属性的值),id toValue (动画结束之后的值),id byValue (动画执行过程中改变的值)。
当之前在使用隐式动画的时候,实际上它就是用例子中CABasicAnimation来实现的(回忆第七章,我们在-actionForLayer:forKey:委托方法打印出来的结果就是CABasicAnimation)。
把动画设置成一个图层的行为(然后通过改变属性值来启动动画)是到目前为止同步属性值和动画状态最简单的方式了。
animation.fromValue = (__bridge id)self.colorLayer.backgroundColor;
self.colorLayer.backgroundColor = color.CGColor;
两个问题:
(1)已经正在进行一段动画,需要从呈现图层那里去获得fromValue,而不是模型图层。
(2)这里的图层并不是UIView关联的图层,我们需要用CATransaction来禁用隐式动画行为,否则默认的图层行为会干扰我们的显式动画
CAAnimationDelegate
为了知道一个显式动画在何时结束,我们需要使用一个实现了CAAnimationDelegate协议的delegate
我们用-animationDidStop:finished:方法在动画结束之后来更新图层的backgroundColor。
Note:
对CAAnimation而言,使用委托模式而不是一个完成块会带来一个问题,就是当你有多个动画的时候,无法在在回调方法中区分。
resolution:
当使用-addAnimation:forKey:把动画添加到图层,这里有一个到目前为止我们都设置为nil的key参数。<1>这里的键是-animationForKey:方法找到对应动画的唯一标识符,<2>而当前动画的所有键都可以用animationKeys获取。<3>如果我们对每个动画都关联一个唯一的键,就可以对每个图层循环所有键,然后调用-animationForKey:来比对结果。尽管这不是一个优雅的实现。
更加简单的方法:
CAAnimation实现了KVC(键-值-编码)协议,于是你可以用-setValue:forKey:和-valueForKey:方法来存取属性。CAAnimation更像一个NSDictionary,可以让你随意设置键值对,即使和你使用的动画类所声明的属性并不匹配。
可以对动画用任意类型打标签
关键帧动画 -- CAKeyframeAnimation
不限制于设置一个起始和结束的值,而是可以根据一连串随意的值来做动画。
(1)animation.values= @[...]
开始和结束的颜色都是蓝色,CAKeyframeAnimation不能自动把当前值作为第一帧,开始和结束设置其他颜色都会出现跳帧。当然可以创建一个结束和开始值不同的动画,需要在动画启动之前手动更新属性和最后一帧的值保持一致。
动画渐变效果有些奇怪,需要调整一下缓冲 --> 第十章
(2)CAKeyframeAnimation --> CGPath
三次贝塞尔曲线,它是一种使用开始点,结束点和另外两个控制点来定义形状的曲线 (CG && UIBezierPath) -- path -> 绘制 + 关联动画
创建path -> 用图层绘制path -> 创建运动层 -> 创建动画关联path,添加到运动层
CAKeyFrameAnimation -- rotationMode = kCAAnimationRotateAuto
虚拟属性
transform.rotation关键路径应用动画,而不是transform本身
(1)我们可以不通过关键帧一步旋转多于180度的动画。
(2)可以用相对值而不是绝对值旋转(设置byValue而不是toValue)。
(3)可以不用创建CATransform3D,而是使用一个简单的数值来指定角度。
(4)不会和transform.position或者transform.scale冲突(同样是使用关键路径来做独立的动画属性)。
transform.rotation属性有一个奇怪的问题是它其实并不存在。这是因为CATransform3D并不是一个对象,它实际上是一个结构体,也没有符合KVC相关属性,transform.rotation实际上是一个CALayer用于处理动画变换的虚拟属性。
note: -- 不能直接使用,只能做动画
你不可以直接设置transform.rotation或者transform.scale,他们不能被直接使用。当你对他们做动画时,Core Animation自动地根据通过CAValueFunction来计算的值来更新transform属性。
CAValueFunction用于把我们赋给虚拟的transform.rotation简单浮点值 => 真正的用于摆放图层的CATransform3D矩阵值。你可以通过设置CAPropertyAnimation的valueFunction属性来改变,于是你设置的函数将会覆盖默认的函数。
动画组 -- CAAnimationGroup:animations
groupAnimation.animations= @[animation1, animation2];
过渡 -- CATransition
type = kCATransitionFade、MoveIn、Push、Reveal
subtype = kCATransitionFromRight、Left、Top、Bottom
隐式过渡
CATransision可以对图层任何变化平滑过渡的事实使得它成为那些不好做动画的属性图层行为的理想候选。设置CALayer的content,CATransition是默认的行为。(视图关联的图层 && 隐式动画的行为 特性被禁用)。自己创建图层可用。
对图层树的动画
CATransition并不作用于指定的图层属性,对随便什么变化都支持过渡。
tricks:要确保CATransition添加到的图层在过渡动画发生时不会在树状结构中被移除,否则CATransition将会和图层一起被移除。一般来说,你只需要将动画添加到被影响图层的superlayer。
自定义动画
UIView:+transitionFromView:toView:duration:options:completion:和+transitionWithView:duration:options:animations:
options:UIViewAnimationOptionTransition_FlipFromLeft、Right、CurlUp、CurlDown
CrossDissolve、FlipFromTop、FlipFromBottom -- 翻转酷炫动画
因此,根据要实现的效果,你只用关心是用CATransition还是用UIView的过渡方法就可以了。
过渡动画原理:
过渡动画做基础的原则就是对原始的图层外观截图,然后添加一段动画,平滑过渡到图层改变之后那个截图的效果。如果我们知道如何对图层截图,我们就可以使用属性动画来代替CATransition或者是UIKit的过渡方法来实现动画。
对图层做截图: -- 例子简单,但是很酷炫
CALayer有一个-renderInContext:方法,可以通过把它绘制到Core Graphics的上下文中捕获当前内容的图片,然后在另外的视图中显示出来。
note:
-renderInContext:捕获了图层的图片和子图层,但是不能对子图层正确地处理变换效果,而且对视频和OpenGL内容也不起作用。但是用CATransition,或者用私有的截屏方式就没有这个限制了。
在动画过程中取消动画
- (CAAnimation *)animationForKey:(NSString *)key;
key参数来在添加动画之后检索一个动画,不支持在动画运行过程中修改动画,所以这个方法主要用来检测动画的属性,或者判断它是否被添加到当前图层中。
终止动画:
- (void)removeAnimationForKey:(NSString *)key; && - (void)removeAllAnimations;
Tip:removedOnCompletion = NO,设置不被移除,不需要的时候就要手动移除。
CAMediaTiming协议
CAMediaTiming协议定义了在一段动画内用来控制逝去时间的属性的集合,CALayer和CAAnimation都实现了这个协议,所以时间可以被任意基于一个图层或者一段动画的类控制。
(1)duration + repeatCount
(2) repeatDuration = INFINITY,autoreverses
相对时间
每次讨论到Core Animation,时间都是相对的,每个动画都有它自己描述的时间,可以独立地加速,延时或者偏移。
beginTime -- 动画开始之前的的延迟时间。
speed -- 一个时间的倍数,默认1.0
timeOffset -- 增加timeOffset只是让动画从某点开始
timeOffset并不受speed的影响,对于speed=2.0,duration=1.0,timeOffset=0.5,动画从最后结束的地方开始,循环一圈。
fillMode
对于beginTime非0的一段动画来说,会出现当动画添加到图层上但什么也没发生的状态。类似的,removeOnCompletion = NO的动画将会在动画结束的时候仍然保持之前的状态。
保持动画开始之前那一帧,或者动画结束之后的那一帧。这就是所谓的填充
CAMediaTiming --> fillMode --> kCAFillModeForwards、Backwards、Both、Removed
这就对避免在动画结束的时候急速返回提供另一种方案(removeOnCompletion = NO,另外需要给动画添加一个非空的键,于是可以在不需要动画的时候把它从图层上移除。)
层级关系时间
对CALayer或者CAGroupAnimation调整duration和repeatCount/repeatDuration属性并不会影响到子动画。但是beginTime,timeOffset和speed属性将会影响到子动画。
beginTime指定了父图层开始动画(或者组合关系中的父动画)和对象将要开始自己动画之间的偏移。
全局时间
CoreAnimation有一个全局时间的概念,也就是所谓的马赫时间
CFTimeInterval time = CACurrentMediaTime(); -- 设备自从上次启动后的秒数
真实的作用在于对动画的时间测量提供了一个相对值。
本地时间
每个CALayer和CAAnimation实例都有自己本地时间的概念,是根据父图层/动画层级关系中的beginTime,timeOffset和speed属性计算。
转换不同图层之间的本地时间:
- (CFTimeInterval)convertTime:(CFTimeInterval)t fromLayer:(CALayer *)l;
- (CFTimeInterval)convertTime:(CFTimeInterval)t toLayer:(CALayer *)l;
当用来同步不同图层之间有不同的speed,timeOffset和beginTime的动画,这些方法会很有用。
暂停,倒回和快进
动画的speed -- 设置动画的speed属性为0可以暂停动画,但在动画被添加到图层之后不太可能再修改它了,所以不能对正在进行的动画使用这个属性。给图层添加一个CAAnimation实际上是给动画对象做了一个不可改变的拷贝,所以对原始动画对象属性的改变对真实的动画并没有作用。相反,直接用-animationForKey:来检索图层正在进行的动画可以返回正确的动画对象,但是修改它的属性将会抛出异常。
(1)如果移除图层正在进行的动画,图层将会急速返回动画之前的状态。但如果在动画移除之前拷贝呈现图层到模型图层,动画将会看起来暂停在那里。但是不好的地方在于之后就不能再恢复动画了。
图层的speed(2)一个简单的方法是可以利用CAMediaTiming来暂停图层本身。如果把图层的speed设置成0,它会暂停任何添加到图层上的动画。类似的,设置speed大于1.0将会快进,设置成一个负值将会倒回动画。
app delegate ==> self.window.layer.speed = 100;
手动动画
timeOffset -- 让你手动控制动画进程(speed=0,禁用动画的自动播放)timeOffset来显示动画序列。
缓冲
Core Animation使用缓冲来使动画移动更平滑更自然
动画速度
velocity = change / time -- 对于这种恒定速度的动画我们称之为“线性步调”,而且从技术的角度而言这也是实现动画最简单的方式,但也是完全不真实的一种效果。
那么我们如何在动画中实现这种加速度呢?一种方法是使用物理引擎来对运动物体的摩擦和动量来建模,然而这会使得计算过于复杂。我们称这种类型的方程为缓冲函数,幸运的是,Core Animation内嵌了一系列标准函数提供给我们使用。
CAMediaTimingFunction
缓冲方程式 = CAAnimation(timingFunction),隐式动画 CATransaction(setAnimationTimingFunction)
(1) +timingFunctionWithName:
kCAMediaTimingFunctionLinear -- 为nil时默认,创建了一个线性的计时函数,
kCAMediaTimingFunctionEaseIn -- 创建了一个慢慢加速然后突然停止的方法(慢->快)
kCAMediaTimingFunctionEaseOut -- 以一个全速开始,然后慢慢减速停止。削弱的效果(快->慢)
kCAMediaTimingFunctionEaseInEaseOut -- 创建了一个慢慢加速然后再慢慢减速的过程。这是现实世界大多数物体移动的方式,也是大多数动画来说最好的选择。实际上当使用UIView的动画方法时,是默认的,创建CAAnimation的时候,就需要手动设置它了。
kCAMediaTimingFunctionDefault -- 加速和减速的过程都稍微有些慢。创建显式的CAAnimation它并不是默认选项
UIView的动画缓冲
UIViewanimateWithDuration: options: animations:
给options参数:UIViewAnimationOptionCurveEaseInOut,EaseIn,EaseOut,Linear
缓冲和关键帧动画
CAKeyframeAnimation有一个NSArray类型的timingFunctions属性,我们可以用它来对每次动画的步骤指定不同的计时函数。但是指定函数的个数一定要等于keyframes数组的count - 1,因为它是描述每一帧之间动画速度的函数。
自定义缓冲函数
+functionWithControlPoints::::,有四个浮点参数
三次贝塞尔曲线 -- 4个点,起点终点,两个控制点在外(控制了曲线的形状)
CAMediaTimingFunction函数的主要原则在于它把输入的时间转换成起点和终点之间成比例的改变。
CAMediaTimingFunction -- -getControlPointAtIndex:values: 用来检索曲线的点,但是使用它我们可以找到标准缓冲函数的点,然后用 UIBezierPath 和 CAShapeLayer 来把它画出来。
更加复杂的动画曲线
(1)用CAKeyframeAnimation创建一个动画,然后分割成几个步骤,每个小步骤使用自己的计时函数
(2)使用定时器逐帧更新实现动画
基于关键帧的缓冲
为了使用关键帧实现反弹动画,我们需要在缓冲曲线中对每一个显著的点创建一个关键帧(在这个情况下,关键点也就是每次反弹的峰值),然后应用缓冲函数把每段曲线连接起来。同时,我们也需要通过keyTimes来指定每个关键帧的时间偏移,由于每次反弹的时间都会减少,于是关键帧并不会均匀分布。
用缓冲函数来把任何简单的属性动画转换成关键帧动画呢,下面我们来实现它。
流程自动化
用直线来拼接这些曲线(也就是线性缓冲)
(1)自动把任意属性动画分割成多个关键帧
(2)用一个数学函数表示弹性动画,使得可以对帧做遍历
(1) 我们需要复制Core Animation的插值机制。这是一个传入起点和终点,然后在这两个点之间指定时间点产出一个新点的机制。
value = (endValue – startValue) × time + startValue;
基于定时器的动画
CAMediaTimingFunction -- 它是一个通过控制动画缓冲来模拟物理效果例如加速或者减速来增强现实感的东西,
定时帧
我们之前提到过iOS按照每秒60次刷新屏幕,然后CAAnimation计算出需要展示的新的帧,然后在每次屏幕更新的时候同步绘制上去,CAAnimation最机智的地方在于每次刷新需要展示的时候去计算插值和缓冲。
NSTimer
需要确切地知道NSTimer是如何工作的。iOS上的每个线程都管理了一个NSRunloop,字面上看就是通过一个循环来完成一些任务列表。但是对主线程,这些任务包含如下几项:
(1)处理触摸事件
(2)发送和接受网络数据包
(3)执行使用gcd的代码
(4)处理计时器行为
(5)屏幕重绘
当你设置一个NSTimer,他会被插入到当前任务列表中,然后直到指定时间过去之后才会被执行。但是何时启动定时器并没有一个时间上限,而且它只会在列表中上一个任务完成之后开始执行。这通常会导致有几毫秒的延迟,但是如果上一个任务过了很久才完成就会导致延迟很长一段时间。
屏幕重绘的频率是一秒钟六十次,但是和定时器行为一样,如果列表中上一个执行了很长时间,它也会延迟。这些延迟都是一个随机值,于是就不能保证定时器精准地一秒钟执行六十次。>>有时候发生在屏幕重绘之后,这就会使得更新屏幕会有个延迟,看起来就是动画卡壳了。有时候定时器会在屏幕更新的时候执行两次,于是动画看起来就跳动了。
我们可以通过一些途径来优化:
(1)我们可以用CADisplayLink让更新频率严格控制在每次屏幕刷新之后。
(2)基于真实帧的持续时间而不是假设的更新频率来做动画。
(3)调整动画计时器的run loop模式,这样就不会被别的事件干扰。
CADisplayLink
CADisplayLink是CoreAnimation提供的另一个类似于NSTimer的类,它总是在屏幕完成一次更新之前启动,它的接口设计的和NSTimer很类似,所以它实际上就是一个内置实现的替代,但是和timeInterval以秒为单位不同,CADisplayLink有一个整型的frameInterval属性,指定了间隔多少帧之后才执行。默认值是1,意味着每次屏幕更新之前都会执行一次。但是如果动画的代码执行起来超过了六十分之一秒,你可以指定frameInterval = 2,就是说动画每隔一帧执行一次(一秒钟30帧)或者3,也就是一秒钟20次,等等。
用CADisplayLink而不是NSTimer,会保证帧率足够连续,使得动画看起来更加平滑,但即使CADisplayLink也不能保证每一帧都按计划执行,一些失去控制的离散的任务或者事件(例如资源紧张的后台程序)可能会导致动画偶尔地丢帧。当使用NSTimer的时候,一旦有机会计时器就会开启,但是CADisplayLink却不一样:如果它丢失了帧,直接忽略它们,然后在下一次更新的时候接着运行。
计算帧的持续时间
需要处理一帧的时间超出了预期的六十分之一秒。由于我们不能够计算出一帧真实的持续时间,所以需要手动测量。我们可以在每帧开始刷新的时候用CACurrentMediaTime()记录当前时间,然后和上一帧记录的时间去比较。
Run Loop 模式
注意到当创建CADisplayLink的时候,我们需要指定一个run loop和run loop mode,对于run loop来说,我们就使用了主线程的run loop,因为任何用户界面的更新都需要在主线程执行,但是模式的选择就并不那么清楚了,每个添加到run loop的任务都有一个指定了优先级的模式,为了保证用户界面保持平滑,iOS会提供和用户界面相关任务的优先级,而且当UI很活跃的时候的确会暂停一些别的任务。
一个典型的例子就是当是用UIScrollview滑动的时候,重绘滚动视图的内容会比别的任务优先级更高,所以标准的NSTimer和网络请求就不会启动,一些常见的run loop模式如下:
(1)NSDefaultRunLoopMode -- 标准优先级
(2)NSRunLoopCommonModes -- 高优先级
(3)UITrackingRunLoopMode -- 用于UIScrollView和别的控件的动画
在我们的例子中,我们是用了NSDefaultRunLoopMode,但是不能保证动画平滑的运行,所以就可以用NSRunLoopCommonModes来替代。但是要小心,因为如果动画在一个高帧率情况下运行,你会发现一些别的类似于定时器的任务或者类似于滑动的其他iOS动画会暂停,直到动画结束。
同样可以同时对CADisplayLink指定多个run loop模式,于是我们可以同时加入NSDefaultRunLoopMode和UITrackingRunLoopMode来保证它不会被滑动打断,也不会被其他UIKit控件动画影响性能,像这样:
self.timer = [CADisplayLink displayLinkWithTarget:self selector:@selector(step:)];
[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:NSDefaultRunLoopMode];[self.timer addToRunLoop:[NSRunLoop mainRunLoop] forMode:UITrackingRunLoopMode];
和CADisplayLink类似,NSTimer同样也可以使用不同的run loop模式配置,通过别的函数,而不是+scheduledTimerWithTimeInterval:构造器
物理模拟
即使使用了基于定时器的动画来复制第10章中关键帧的行为,但还是会有一些本质上的区别: <1> 在关键帧的实现中,我们提前计算了所有帧,<2> 但是在新的解决方案中,我们实际上实在按需要在计算。意义在于我们可以根据用户输入实时修改动画的逻辑,或者和别的实时动画系统例如物理引擎进行整合。
Chipmunk
当前基于缓冲的弹性动画 ==> 真实的重力模拟效果
(1)cpSpace- 这是所有的物理结构体的容器。它有一个大小和一个可选的重力矢量
(2)cpBody- 它是一个固态无弹力的刚体。它有一个坐标,以及其他物理属性,例如质量,运动和摩擦系数等等。
(3)cpShape- 它是一个抽象的几何形状,用来检测碰撞。可以给结构体添加一个多边形,而且cpShape有各种子类来代表不同形状的类型。
性能调优
CPU所做的工作都在软件层面,而GPU在硬件层面。我们可以用软件(使用CPU)做任何事情,但是对于图像处理,通常用硬件会更快,
Core Animation处在iOS的核心地位:应用内和应用间都会用到它。
动画和屏幕上组合的图层实际上被一个单独的进程管理,而不是你的应用程序。这个进程就是所谓的渲染服务。BackBoard
当运行一段动画时候,这个过程会被四个分离的阶段被打破:
(1)布局- 这是准备你的视图/图层的层级关系,以及设置图层属性(位置,背景色,边框等等)的阶段。
(2)显示- 这是图层的寄宿图片被绘制的阶段。绘制有可能涉及你的-drawRect:和-drawLayer:inContext:方法的调用路径。
(3)准备- 这是Core Animation准备发送动画数据到渲染服务的阶段。这同时也是Core Animation将要执行一些别的事务例如解码动画过程中将要显示的图片的时间点。
(4)提交- 这是最后的阶段,Core Animation打包所有图层和动画属性,然后通过IPC(内部处理通信)发送到渲染服务进行显示。
一旦打包的图层和动画到达渲染服务进程,他们会被反序列化来形成另一个叫做渲染树的图层树。使用这个树状结构,渲染服务对动画的每一帧做出如下工作:
(1)对所有的图层属性计算中间值,设置OpenGL几何形状(纹理化的三角形)来执行渲染
(2)在屏幕上渲染可见的三角形
所以一共有六个阶段;最后两个阶段在动画过程中不停地重复。前五个阶段都在软件层面处理(通过CPU),只有最后一个被GPU执行。而且,你真正只能控制前两个阶段:布局和显示。Core Animation框架在内部处理剩下的事务,你也控制不了它。
在布局和显示阶段,你可以决定哪些由CPU执行,哪些交给GPU去做。
GPU相关的操作
宽泛的说,大多数CALayer的属性都是用GPU来绘制。
有一些事情会降低(基于GPU)图层绘制,比如:
(1)太多的几何结构 - 这发生在需要太多的三角板来做变换,以应对处理器的栅格化的时候。
(2)重绘 - 主要由重叠的半透明图层引起。GPU的填充比率(用颜色填充像素的比率)是有限的,所以需要避免重绘(每一帧用相同的像素填充多次)的发生。
(3)离屏绘制 - 这发生在当不能直接在屏幕上绘制,并且必须绘制到离屏图片的上下文中的时候。离屏绘制发生在基于CPU或者是GPU的渲染,或者是为离屏图片分配额外内存,以及切换绘制上下文,这些都会降低GPU性能。对于特定图层效果的使用,比如圆角,图层遮罩,阴影或图层光栅化都会强制Core Animation提前渲染图层的离屏绘制。
(4)过大的图片 - 如果视图绘制超出GPU支持的2048x2048或者4096x4096尺寸的纹理,就必须要用CPU在图层每次显示之前对图片预处理,同样也会降低性能。
CPU相关的操作
发生在动画开始之前,不会影响到帧率,会延迟动画开始的时间。
以下CPU的操作都会延迟动画的开始时间: -- 视图懒加载,图片绘制懒解码
(1)布局计算 - 如果你的视图层级过于复杂,当视图呈现或者修改的时候,计算图层帧率就会消耗一部分时间。特别是使用iOS6的自动布局机制尤为明显,它应该是比老版的自动调整逻辑加强了CPU的工作。
(2)视图惰性加载 - iOS只会当视图控制器的视图显示到屏幕上时才会加载它。
(3)Core Graphics绘制 - 如果对视图实现了-drawRect:方法,或者CALayerDelegate的-drawLayer:inContext:方法,那么在绘制任何东西之前都会产生一个巨大的性能开销。为了支持对图层内容的任意绘制,Core Animation必须创建一个内存中等大小的寄宿图片。然后一旦绘制结束之后,必须把图片数据通过IPC传到渲染服务器。在此基础上,Core Graphics绘制就会变得十分缓慢,所以在一个对性能十分挑剔的场景下这样做十分不好。
(4) 解压图片 - PNG或者JPEG压缩之后的图片文件会比同质量的位图小得多。但是在图片绘制到屏幕上之前,必须把它扩展成完整的未解压的尺寸(通常等同于图片宽 x 长 x 4个字节)。为了节省内存,iOS通常直到真正绘制的时候才去解码图片。
IO相关操作
IO比内存访问更慢,所以如果动画涉及到IO,就是一个大问题。多线程,缓存和投机加载(提前加载当前不需要的资源,但是之后可能需要用到)。
测量,而不是猜测
真机测试,而不是模拟器
另一件重要的事情就是性能测试一定要用发布配置,而不是调试模式。因为当用发布环境打包的时候,编译器会引入一系列提高性能的优化,例如去掉调试符号或者移除并重新组织代码
保持一致的帧率
为了做到动画的平滑,你需要以60FPS(帧每秒)的速度运行,以同步屏幕刷新速率。通过基于NSTimer或者CADisplayLink的动画你可以降低到30FPS,而且效果还不错,但是没办法通过Core Animation做到这点。如果不保持60FPS的速率,就可能随机丢帧,影响到体验。
Instruments