这篇文章是我在看《iOS Core Animation: Advanced Techniques 》这本书的时候做的一些笔记,这本书很深入的将了Core Animation的原理性的东西,是一本讲解Core Animation原理非常深入的书,豆瓣评分有9.5 。如果把整本书全部读完,理解,我相信iOS 中的动画就是件很轻松的事情了,可惜读的是英文的,很多东西还是没有搞懂,摘抄了一些我自己觉得有用的点,翻译了一下,可能代码比较少,大部分是原理性的东西,希望能对大家理解CALayer有一定的帮助。
在网上也找到了这本书完整的中文翻译,如果感兴趣,可以去看看。
https://www.gitbook.com/book/zsisme/ios-/details
1.The Layer Tree(图层树)
Core Animation这个名字很容易让人产生误解,你可能会觉得他主要的目的是动画,但是其实动画只是这个框架的一方面。
**CALayer: **
CALayer在概念上跟UIView非常像,也是一个矩形的对象,可以形成一个层级树。CALayer中可以包含内容(例如图片,文字,或者背景颜色等),也可以管理其中的子图层(sublayer),有可以用来动画和变换的属性和方法。
每个UIView都内部都包含一个layer对象(backing layer),UIView的渲染,布局和动画,都是通过这个layer对象来管理的。但是UIView最显著的特性之一:用户交互,不是通过CALayer来管理的,因为CALayer不支持响应链,不能响应事件(但是CALayer提供了方法来判断点击事件是否在某个layer的bounds之内)
我是这样理解的,CALayer就是一个不能进行用户交互的UIView,UIView中大部分显示都是通过CALayer来完成的,而UIView自己负责用户交互的这一部分。
1.为什么iOS要设计这样的UIView与CALayer的平行分层?而不是只用一层UIView来处理所有的显示和用户交互?
为了分离职责,减少重复代码。事件处理和用户交互在iOS和Mac OS上差别很大(最基本的用户交互差别是iOS是基于触摸操作,Mac OS基于鼠标和键盘的操作,这也是为什么iOS中是UIKit和UIView而Mac OS中是AppKit和NSView,它们虽然功能像似,但是实现方法不同),但是相反的是绘图,布局和动画等,在iOS和Mac OS中概念基本相同。通过将其中的逻辑抽离出来到独立的Core Animation框架中,Apple可以在iOS平台和Mac OS平台中进行代码共享。
2.CALayer可以做什么?
常见的:
- 绘制阴影,圆角,着色边界
- 3D变换和定位
- 非矩形边界
- 多步,非线性动画
3.使用CALayer
CALayer的使用非常简单,下面这个方法是最基础的使用,可以看到跟UIView的使用非常像。
func addLayer() {
let layer = CALayer()
layer.frame = CGRectMake(100, 100, 100, 100)
layer.backgroundColor = UIColor.redColor().CGColor
view.layer.addSublayer(layer)
}
4.什么情况下你会选择使用CALayer,而不是UIView?
- 当你想要写一份可以同时在iOS和Mac上运行的代码
- 你可能需要使用多种CALayer的子类(后面会提到)
- 当你的程序对性能要求非常高的时候,可能管理UIView额外的对象都对性能有影响的时候
2.The Backing Image(寄宿图)
这个我也不知道咋翻译。。。看网上有翻译成寄宿图的,就借过来用一下了。其实感觉就是layer的一个填充图片内容的容器。
CALayer有一个属性叫做:contents,在OC中是id类型,swift中是AnyObject?类型,这代表它可以是任意类型的对象。但是你会发现如果给contents赋值CGImage类型以外的对象时,得到的都是空白的结果。这是Mac OS中遗留下来的问题。之所以设计为id类型是为了在Mac OS中可以赋值CGImage或者NSImage,并且都可以得到想要的结果,但是在iOS中,如果赋值UIImage得到的也是一个空白的结果。
//下面这段代码可以不通过UIImageView而是通过CALayer,在屏幕上显示一张图片
let layer = CALayer()
layer.frame = CGRectMake(100, 100, 100, 100)
layer.contents = UIImage(named: "layer.jpg")?.CGImage
view.layer.addSublayer(layer)
其实对于UIView的很多属性的操作,其实只是操作UIView内部layer对应的属性
- contentsGravity : 相当于UIView中的contentMode属性。但是它是String类型的,而不是枚举(CALayer中对应的大部分UIView中的枚举属性都是以KCA开头的String)。contentsGravity也是用来判断layer的内容如何对齐。
- masksToBounds : 对应的是UIView中的clipsToBounds属性。
还有很多其他的属性,就不一一列举了,大家可以看一下CALayer和UIView对应的属性,很多名称差不多的,可能就是相对应的。
自定义绘图
除了contents可以设置背景图片以外,我们可以直接使用Core Graphics来直接绘制图形来填充到backing layer中,-drawRect: 方法可以在UIView的子类中重写来实现自定义绘图。只要UIView发现drawRect方法实现了,就会自动生成backing layer,所以如果你不需要这个背景图片的话,最好不要留一个空白的drawRect方法,因为这样会浪费内存。
-drawRect: 方法在view第一次显示在屏幕上时自动调用,在这个方法中用Core Graphics来绘制图形到背景图片中,图形会一直缓存在内存中直到view需要刷新(一般都是通过手动调用 -setNeedsDisplay方法来实现,但是view有一些属性的改变也会引起重绘,比如bounds)
CALayer有一个可选的delegate属性,遵循CALayerDelegate协议(非正式协议),当CALayer需要内容信息的时候,会从delegate中取获取。
当layer需要重绘的时候,CALayer会调用delegate的
- (void)displayLayer:(CALayerCALayer *)layer;
方法来获取背景图片。
如果delegate没有实现上面的方法,CALayer会自动创建一个空的背景图片和绘制的图形上下文(context),作为调用delegate的
- (void)drawLayer:(CALayer *)layer inContext:(CGContextRef)ctx;
方法的参数。
现在你应该理解和知道如何使用CALayerDelegate了,但是除非你是在创建独立的layers,否则基本上不用手动去实现CALayerDelegate协议。这是因为UIView在创建backing layer的时候会自动设置自己为layer的delegate并且提供-displayLayer:方法的实现。在使用UIView的backing layer的时候,你不需要去实现-displayLayer:方法或者-drawLayer:inContext:方法来绘制内容到layer的backing image,只需要实现-drawRect:方法,UIView会自动处理,包括在需要重绘的时候会自动调用 -display方法。
3.Layout Geometry(图层几何)
Layout
- UIView 三个主要的layout属性: frame, bounds, center
- CALayer 的三个layout属性: frame , bounds, position
UIView的frame,bounds和center其实只是内部layer相对应属性的get和set方法,当你改变一个view的frame的时候,你其实改变的是内部layer的frame。
锚点(anchorPoint) :
一个layer的锚点代表的是它的哪个点在position(相对于super layer)的位置,所以锚点与position总是重合的,默认的锚点是在layer的中心点上。锚点是在单位坐标中描述的,左上角的位置是{0,0},右下角的位置是{1,1},默认位置是{0.5,0.5}。
坐标系统
layer跟view一样,也是相对于自己的父图层来放置的,layer的position是以superlayer的bounds为参照的。如果superlayer移动的话,所有的sublayers都要跟着移动,这样的好处是我们可以通过移动最底部的superlayer可以把所有的sublayer作为一个单元一起移动。但是,有时候我们需要知道的是一个layer相对于另外一个layer的位置,而不是相对于它的superlayer的位置。CALayer提供了一些方法来在layer之间的坐标系进行转换:
- (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;
Hit Testing
一般情况下我们会使用带有backing layer的UIView而不是直接使用CALayer来创建视图,很重要的一个原因是,layer处理触摸事件或者手势非常复杂。CALayer不了解响应链,所有不能直接相应触摸或者手势,但是它提供了两个方法来让我们自己实现触摸操作,两个方法的参数都是一个CGPoint,代表当前触摸的点
- -containsPoint: 判断点是否在layer的frame之内
- -hitTest: 返回layer,或者包含触摸点的最深层的sublayer,当点不在layer上时,返回nil
layer masking
layer的mask属性也是一个CALayer对象,设置layer的mask属性与直接添加一个sublayer类似,但是显示的时候并不是一样的。作为mask的layer定义的是parent layer的可视部分,而不是直接绘制在parent layer中。所以,mask的color等属性设置没有任何影响,只有它的轮廓才是最重要的部分。mask的作用像是一个切割的工具,只有parent layer中mask包含的那一部分会被切出来并且保留。
5.Transforms(变换)
下面几个单词我总是混淆,经常会用到。
- transform :变换
- translate : 平移
- transition : 过渡
- transcation : 事务处理(动画中)
1.仿射变换
CGAffineTransform(3*3的矩阵) 代表的是 二维平面 的 旋转(rotation),缩放(scale),平移(translation)
仿射变换(Affine) 代表的是平面之内变化之前平行的线,变化之后依然平行
创建CGAffineTransform
CGAffineTransformMakeRotation(CGFloat angle)
CGAffineTransformMakeScale(CGFloat sx, CGFloat sy)
CGAffineTransformMakeTranslation(CGFloat tx, CGFloat ty)
仿射变换结合
//通过已经存在的变换生成新的变换
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)
2.3D变换
CATransform3D是一个4*4的矩阵,可以在三维空间内对一个对象进行选择,缩放,评、平移等变换。
CATransform3D和CGAffineTransform提供的方法很像
CATransform3DMakeRotation(CGFloat angle, CGFloat x, CGFloat y, CGFloat z)
CATransform3DMakeScale(CGFloat sx, CGFloat sy, CGFloat sz)
CATransform3DMakeTranslation(Gloat tx, CGFloat ty, CGFloat tz)
translation和scale相比CGAffineTransform多了一个代表z轴的z参数,rotation方法接收x,y,z和一个angle参数(一起形成了一个代表旋转轴的矢量)
6.Specialized Layers(特殊的layer)
-
CAShapeLayer
CAShapeLayer绘制的是矢量图形,而不是位图。通过CGPath定义一个合适的性状,指定颜色,线宽等属性来渲染出一个layer。
与直接在CALayer的contents中通过Core Graphics来绘制图形相比,CAShapeLayer具有以下优势:
速度快,内存效率高(不用像CALayer一样创建一个backing image),不会被layer的bounds切边,没有像素
单独设置layer每个角的圆角
//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];
使用CAShapeLayer的一个列子
func setShapeLayer() {
//生成路径
let path = UIBezierPath()
path.moveToPoint(CGPoint(x: 100,y: 100))
path.addLineToPoint(CGPoint(x: 130,y: 200))
path.addLineToPoint(CGPoint(x: 50,y: 130))
path.addLineToPoint(CGPoint(x: 150,y: 130))
path.addLineToPoint(CGPoint(x: 70,y: 200))
path.closePath()
//生成shape layer并且设置渲染属性
let shapeLayer = CAShapeLayer()
shapeLayer.strokeColor = UIColor.redColor().CGColor
shapeLayer.fillColor = UIColor.clearColor().CGColor
shapeLayer.lineWidth = 5
shapeLayer.path = path.CGPath
//添加到view中
view.layer.addSublayer(shapeLayer)
}
-
CAGradientLayer
CAGradientLayer可以用来形成两种或者多种颜色之间的平滑渐变。
-
基础渐变
func addGradientLayer() { let gradientLayer = CAGradientLayer() gradientLayer.frame = CGRectMake(100, 100, 100, 100) gradientLayer.colors = [UIColor.redColor().CGColor, UIColor.blueColor().CGColor] gradientLayer.startPoint = CGPointMake(0, 0) gradientLayer.endPoint = CGPointMake(1, 1) view.layer.addSublayer(gradientLayer)
}
效果:
- 分段渐变
默认的color在渐变中都是均匀分布的,我们可以使用locations属性来控制每个color的分布范围。
locations是一个范围的数组,每个值都是在{0-1}的范围中,代表对应colors数组中颜色在渐变中的范围(所以,如果提供了locations这个属性,即必须保证locations的数量与colors的一致)。
func addMultipartGradientLayer() {
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRectMake(100, 300, 100, 100)
gradientLayer.colors = [UIColor.redColor().CGColor,
UIColor.blueColor().CGColor,
UIColor.greenColor().CGColor]
gradientLayer.startPoint = CGPointMake(0, 0.5)
gradientLayer.endPoint = CGPointMake(1, 0.5)
gradientLayer.locations = [0.2,0.5,0.8]
view.layer.addSublayer(gradientLayer)
}
效果:
locations属性可以用来动画,利用CAShapeLayer作为CAGradientLayer的mask,可以做出文字颜色渐变效果的动画,大家可以自己去试试。
-
CAEmitterLayer
CAEmitterLayer是一个可以用来创建实时粒子动画(比如烟雾,火焰,雨)效果的高性能的粒子引擎。
CAEmitterLayer作为CAEmitterCell的一个容器,我们需要创建一个或者多个CAEmitterCell对象作为粒子的模板,CAEmitterLayer负责基于这些模板创建一个粒子流。
CAEmitterCell跟CALayer很像,也有一个contents的属性,还有其它很多的属性,大部分都是用来控制粒子外观的。
func addEmitterLayer() {
let emitter = CAEmitterLayer()
emitter.frame = CGRectMake(100, 100, 100, 100)
emitter.renderMode = kCAEmitterLayerPoint
emitter.emitterPosition = view.center
let cell = CAEmitterCell()
cell.contents = UIImage(named: "emiter")?.CGImage
cell.birthRate = 150
cell.lifetime = 5.0
cell.alphaSpeed = -0.4
cell.velocityRange = 50
cell.emissionRange = CGFloat(M_PI*2.0)
cell.color = UIColor(red: 1, green: 0.5, blue: 0.1, alpha: 1.0).CGColor
emitter.emitterCells = [cell]
view.layer.addSublayer(emitter)
}
效果:
CAEmitterCell的属性一般分为以下几类 :
- 粒子的某个属性的开始值 : 比如color指定的将被用来与contents里面的image生成混合颜色
- 某个会在粒子之间产生变化的值的范围 : 比如emissionRange代表的是粒子发射的范围
- 某些会随着时间改变的值 : 比如alphaSpeed,表示的是alpha每秒钟的增量,设置成负数的时候,可以产生谈出的效果
总结:
特殊的CALayer还有很多种,包括CALayer的特性也还有很多,因为我也还没读完这本书,所以后面再加入吧,不过大家可以去我上面提到的中文翻译完整地址去看看,我这里面只是提到了书中很小的一部分,是我自己觉得对我来说挺有用的一些方面,总之我觉得 《iOS Core Animation: Advanced Techniques 》这本书非常好,希望有兴趣学习动画的同学可以去看看,强力推荐👍。