Core Animation 之 CALayer

这篇文章是我在看《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可以用来形成两种或者多种颜色之间的平滑渐变。

  1. 基础渐变

     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)
    

    }

效果:

颜色渐变
  1. 分段渐变

默认的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 》这本书非常好,希望有兴趣学习动画的同学可以去看看,强力推荐👍。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343

推荐阅读更多精彩内容