Swift 4 动画 - 2. CALayer

所有示例代码均可以在 Animations-Demo 下载到

上节提到 UIView 上所有动画归根结底都是发生在Layer 层,所以动画的学习离不开Layer的学习。

我们平时开发中很少使用layer,但是我们却一直在使用layer。view是不具备绘制能力的,真正绘制的是他的underlying layer 。 每个view都有一个layer属性。view上显示相关的属性也是layer属性的一个映射。屏幕显示的时候 UIViewlayer绘制上去。视图不会被经常重绘;相反,它的绘制会被缓存,在可用的地方都会使用缓存版本(bitmap backing store)。缓存的版本,实际上,就是layer。那么view的图形上下文也就是layer的图形上下文。

所以深入学习layer还是很有必要的,因为它可以完成一些view不能完成的任务(比如,阴影、圆角、3d变换、透明遮罩、多级非线性动画、路径动画等)。尤其是在动画方便表现突出。 CALyaer 前面的 "CA" 代表的 " Core Animation "。但是CALayer 并不清楚具体的响应链(iOS通过视图层级关系用来传送触摸事件的机制),于是它并不能够响应事件,即使它提供了一些方法来判断是否一个触点在图层的范围之内。

我们平时使用layer最好是使用view的underlying layer 。 这样既能享受 UIView的高级api,也能使用到layer的特性。 layer是不支持 AutoLayout 的。我们可以使用AutoLayout 为view布局,那么他的layer的frame会跟随view frame改变。

layer的几个基本属性:

  • contents 是一个Any? ,但实际上接收一个CGImage对象,如果是其他对象,图层将是空白。
-
示例

图层上将会显示会对应的图像。

  • contentGravity 相当于UIViewcontentMode 属性 , 有以下值

    • kCAGravityCenter
    • kCAGravityTop
    • kCAGravityBottom
    • kCAGravityLeft
    • kCAGravityRight
    • kCAGravityTopLeft
    • kCAGravityTopRight
    • kCAGravityBottomLeft
    • kCAGravityBottomRight
    • kCAGravityResize
    • kCAGravityResizeAspect
    • kCAGravityResizeAspectFill
  • contentsScale 图的像素尺寸和视图大小的比例,默认情况下它是一个值为1.0的浮点数,应该设置为对应的scale

view1.layer.contentsScale = #imageLiteral(resourceName: "rabbit").scale
// or 
view1.layer.contentsScale = UIScreen.main.scale
  • maskToBounds 相当于 UIViewclipsToBounds 超出部分是否裁剪
  • contentsRect 允许我们在图层边框里显示寄宿图的一个子域。这涉及到图片是如何显示和拉伸的.contentsRect不是按点来计算的,它使用了单位坐标,单位坐标指定在0到1之间,是一个相对值。
view1.layer.contentsRect = CGRect(x: 0, y: 0, width: 0.5, height: 0.5)
示例

x方向的 一半 y 方向的一半 相当于1/4个兔子。。

可以通过下面的rect分别获取其他的3/4

CGRect(x: 0.5, y: 0, width: 0.5, height: 0.5)
CGRect(x: 0, y: 0.5, width: 0.5, height: 0.5)
CGRect(x: 0.5, y: 0.5, width: 0.5, height: 0.5)

图层跟view一样也有层级树,可以添加,可以有子layer但是最多只能有一个superlayer。layer使用了和视图相似的一整套方法来读取和操纵layer的层次结构。layer有一个superlayer属性和sublayers属性,以及下面的方法

  • addSublayer:
  • insertSublayer:atIndex:
  • insertSublayer:below:, insertSublayer:above:
  • replaceSublayer:with:
  • removeFromSuperlayer

不同于视图的subviews属性,layer的sublayers属性是可写的。你可以通过sublayers属性一次性给layer设置多个sublayer。通过设置sublayers为nil来移除layer的所以子layer。

虽然一个layer的子layer有顺序,可以通过上面提到的方法和sublayers属性来操纵顺序,但这并不和绘制的顺序完全相同。默认情况下,layer有一个CGFloat类型的zPosition属性值,这也决定了绘制顺序。绘制规则是相同的zPosition的所有子layer在sublayers属性所列的顺序绘制,但较低的zPosition属性比较高的zPosition属性的layer先绘制。 (默认的zPosition是0.0)

还有一些方法提供了用于在同一layer层次结构内各layer的坐标系统之间的转换方法:

  • convert:from:, convert:to:

可用来转换 CGPoint 和 CGRect

position 和 anchorPoint

position 对应于view的center属性,anchorPoint相当于一个锚点或者移动图层的一个固定点。anchorPoint用单位坐标来描述,也就是图层的相对坐标,图层左上角是{0, 0},右下角是{1, 1},
默认来说,anchorPoint 位于图层的中点,因此默认坐标是{0.5, 0.5}。所以图层的将会以这个点为中心放置。但是图层的anchorPoint可以被移动,比如设置为(0,0)。
那么图层就会像右下角移动。

示例

我们将棕色view的anchorPoint设置为 0,0

view3.layer.anchorPoint = CGPoint.zero
示例

anchorPoint位于图层的中点,所以图层的将会以这个点为中心放置

来看一个钟表的例子 。

我们在界面上放两个view都是基于AutoLayout布局的。


示例

蓝色view表示表盘,白色表示指针,居中显示。

然后在代码中进行设置一下

clockView.backgroundColor = UIColor.clear
clockView.layer.contents = #imageLiteral(resourceName: "clock").cgImage
clockView.layer.contentsScale = #imageLiteral(resourceName: "clock").scale

arrowView.layer.contents = #imageLiteral(resourceName: "arrow").cgImage
arrowView.layer.contentsScale = #imageLiteral(resourceName: "arrow").scale
arrowView.layer.backgroundColor = UIColor.clear.cgColor
arrowView.layer.anchorPoint = CGPoint(x: 0.5, y: 0.9)
let opts: UIViewAnimationOptions = [ .autoreverse , .repeat ]
UIView.animate(withDuration: 1 , delay: 0, options: opts, animations: {
  self.arrowView.transform = CGAffineTransform.identity.rotated(by: CGFloat( Double.pi/2 ) )
}, completion: nil)

就会得到如下效果。

示例

Cool~ ! 我们用了很少代码实现了不错的效果,view配合layer实现的。

这节里只谈layer不谈layer的动画。

视觉效果

  • 圆角
    使用cornerRadius 配合 masksToBounds 可以达到圆角的效果。这个应该都会经常用到,会造成离屏渲染。

  • 边框
    borderWidth 默认是0,黑色 ,可以通过 borderColor设置颜色

  • 阴影
    shadowColorshadowOpacityshadowRadiusshadowOffset属性定义,为使该层绘制阴影,shadowOpacity应该设置为非零值。阴影通常是根据该层的不透明区域的形状绘制,但得到该形状是cpu密集型的。您可以通过自己定义形状和把形状做为CGPath赋值给shadowPath属性,这会大大提高性能。

    如果图层的masksToBounds是true,边界之外的阴影不会被绘制

给shadowOpacity属性一个大于默认值(也就是0)的值,阴影就可以显示在任意图层之下。shadowOpacity是一个必须在0.0(不可见)和1.0(完全不透明)之间的浮点数。如果设置为1.0,将会显示一个有轻微模糊的黑色阴影稍微在图层之上。

shadowOffset属性控制着阴影的方向和距离。它是一个CGSize的值,宽度控制这阴影横向的位移,高度控制着纵向的位移。shadowOffset的默认值是 {0, -3},意即阴影相对于Y轴有3个点的向上位移。

view4.backgroundColor = UIColor.white
view4.layer.shadowColor = UIColor.black.cgColor
view4.layer.shadowOffset = CGSize(width: 0, height: 3)
view4.layer.shadowOpacity = 0.6
示例

shadowRadius属性控制着阴影的模糊度(默认值是3),当它的值是0的时候,阴影就和视图一样有一个非常确定的边界线。当值越来越大的时候,边界线看上去就会越来越模糊和自然。苹果自家的应用设计更偏向于自然的阴影,所以一个非零值再合适不过了。
通常来讲,如果你想让视图或控件非常醒目独立于背景之外(比如弹出框遮罩层),你就应该给shadowRadius设置一个稍大的值。阴影越模糊,图层的深度看上去就会更明显

view4.layer.shadowRadius = 10
示例

实时计算阴影也是一个非常消耗资源的,尤其是图层有多个子图层,每个图层还有一个有透明效果的寄宿图的时候。如果你事先知道你的阴影形状会是什么样子的,你可以通过指定一个shadowPath来提高性能。

view4.layer.shadowPath = // 一个CGPath类型
  • 图层蒙版 mask 属性 :
    CALayer有一个属性叫做mask , 这个属性本身就是个CALayer类型,有和其他图层一样的绘制和布局属性。它类似于一个子图层,相对于父图层(即拥有该属性的图层)布局,但是它却不是一个普通的子图层。不同于那些绘制在父图层中的子图层,mask图层定义了父图层的部分可见区域。

    mask图层的Color属性是无关紧要的,真正重要的是图层的轮廓。mask属性就像是一个饼干切割机,mask图层实心的部分会被保留下来,其他的则会被抛弃。

比如我们有这样一张猫咪图片

cat

一张星星图片

star

我们想让猫咪显示星星形状。

view5.backgroundColor = UIColor.clear
view5.layer.contents = #imageLiteral(resourceName: "cat").cgImage
view5.layer.contentsScale = #imageLiteral(resourceName: "cat").scale

由于要指定mask layer的frame

override func viewDidLayoutSubviews() {
    super.viewDidLayoutSubviews()
    let layer = CALayer()
    layer.contents = #imageLiteral(resourceName: "star").cgImage
    layer.contentsScale = #imageLiteral(resourceName: "star").scale
    layer.frame = view5.bounds
    view5.layer.mask = layer
}

效果

mask

图层的Transform

不同于view的transform 图层是可以做3d变换的。如果只是想执行2d的变换可以调用

layer.setAffineTransform(_:)

传入CGAffineTransform 参数,和view的变换方式一样的。如果要有可能用到3d变换就要使用 transform属性 一个 CAtransform3D对象,也可以进行2d变换,指定z为默认。

  • CATransform3DMakeScale
  • CATransform3DMakeRotation
  • CATransform3DMakeTranslation
  • CATransform3D.init(m11: , m12: , m13: , m14: , m21: , m22: , m23: , m24: , m31: , m32: , m33: , m34: , m41: , m42: , m43: , m44: )

最后一个是他的初始化方法,需要设置一个4X4的矩阵,如果你的数学功底足够厉害,你可以那么干。

有两种方式来放置layer在不同的深度。一种是通过它们的位置,就是zPosition属性。另一种是在z轴上施加一个平移变换来改变layer的位置。layer的position的z分量(zPosition)和在z轴的偏移量这两个量是相关的;在某种意义上说,zPosition是在z方向的平移变换的简写形式。

在现实世界中,改变一个对象的zPosition会使其显示更大或更小,因为它和眼睛的距离更近或更远;但是layer的绘制和真实世界不一样。这里没有视角的概念;layer在平面上按照它们真实的大小绘制而且叠在一起没有间隙。(这就是所谓的正投影,并且蓝图经常以这样的方式从侧面显示一个物体)。

CATransform3D的透视效果通过一个矩阵中一个很简单的元素来控制:m34m34 用于按比例缩放X和Y的值来计算到底要离视角多远。

m34的默认值是0,我们可以通过设置m34为-1.0 / d来应用透视效果,d代表了想象中视角相机和屏幕之间的距离,以像素为单位,那应该如何计算这个距离呢?实际上并不需要,大概估算一个就好了。通常500-1000就已经很好了

var transform = CATransform3DIdentity
transform.m34 = -1.0 / 500.0
transform = CATransform3DRotate(transform, CGFloat(Double.pi / 4), 0, 1, 0)
view4.layer.transform = transform

对上面带阴影的layer做了变换后的效果

示例

CALayer有一个属性 sublayerTransform 他允许对他所有的子图层做变幻,参考示例: iOS 3D变换 -- CALayer的transform

图层的 KVC

所有图层属性都可以通过具有相同名称的属性键的键值编码来访问。因此,为layer添加mask,可以这样:
layer.mask = mask
也可以这样:
layer.setValue(mask, forKeyPath: "mask")
当然你也可以用swift4 的新语法

layer[keyPath:\CALayer.mask] = layer

此外,CATransform3D和CGAffineTransform值可以通过键 - 值编码和key path表示。例如。

self.ratationLayer.transform = CATransform3DMakeRotation(CGFloat(M_PI) / 4.0, 0, 1, 0)

也可以这样:

self.rotationLayer.setValue(M_PI / 4, forKeyPath: "transform.rotation.y")

transform相关属性可以这样使用

  • rotation.x,rotation.y,rotation.z
  • rotation(和rotation.z一样)
  • scale.x,scale.y,scale.z
  • translation.x,translate.y,translate.z
  • translation

甚至你可以把CALayer作为一种字典,获取和设置任意键的值。这意味着你可以将任意信息附加到一个单独的层实例,并在以后检索。例如,手动布局layer需要先引用到此layer。那么可以这样做:

myLayer1.setValue("Foo", forKey: "name")
myLayer2.setVlaue("Foo2", forKey: "name")

图层没有一个name属性;'name'属性是我附加给layer的。现在,我可以通过获取各自的“name”键的值后确定这些层。

其他Layer

iOS 系统为我们提供了很多有特殊功能的layer。如CAGradientLayer 可以生成两种或更多颜色平滑渐变的图层。用Core Graphics复制一个CAGradientLayer并将内容绘制到一个普通图层的寄宿图也是有可能的,但是CAGradientLayer的真正好处在于绘制使用了硬件加速。

如果我们想要实现一个 view 自带渐变背景,那么我们可以改变view自身的 underlying layer 。

class CustomView: UIView {
  override class var layerClass: AnyClass {
    return CAGradientLayer.self
  }
}

这个默认是什么 CALayer

那么我们用这个方法就可以实现一个渐变色的view

class CustomView: UIView {
  override class var layerClass: AnyClass {
    return CAGradientLayer.self
  }
  
  override init(frame: CGRect) {
    super.init(frame: frame)
    prepareView()
  }
  
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    prepareView()
  }
  
  func prepareView(){
    if let gradientLayer = self.layer as? CAGradientLayer{
      gradientLayer.colors = [ UIColor.red.cgColor,UIColor.blue.cgColor ]
      gradientLayer.startPoint = CGPoint.zero
      gradientLayer.endPoint = CGPoint(x: 1, y: 1)
    }
  } 
}

当然这个颜色可以为多个通过locations指定每个渐变颜色改变的点(相对坐标)

gradientLayer.locations = [0,0.5]
示例
CAShapeLayer

这个layer非常厉害,可以使用CGPath 来定义想要绘制的图形 ,最后CAShapeLayer就自动渲染出来了。当然,你也可以用Core Graphics直接向原始的CALyer的内容中绘制一个路径,相比直下,使用CAShapeLayer有以下一些优点:

  • 渲染快速。CAShapeLayer使用了硬件加速,绘制同一图形会比用Core Graphics快很多。
  • 高效使用内存。一个CAShapeLayer不需要像普通CALayer一样创建一个寄宿图形,所以无论有多大,都不会占用太多的内存。
  • 不会被图层边界剪裁掉。一个CAShapeLayer可以在边界之外绘制。你的图层路径不会像在使用CoreGraphics的普通CALayer一样被剪裁掉
  • 不会出现像素化。当你给CAShapeLayer做3D变换时,它不像一个有寄宿图的普通图层一样变得像素化。

CAShapeLayer 做一些路径动画的时候将非常有用。目前只看下基本使用

let shapeLayer = CAShapeLayer()
func configShapelayer(){
    let rect = view7.bounds
    let path = UIBezierPath(ovalIn: rect)
    shapeLayer.path = path.cgPath
    shapeLayer.strokeColor = UIColor.red.cgColor
    shapeLayer.fillColor = UIColor.clear.cgColor
    shapeLayer.frame = view7.bounds
    view7.layer.addSublayer(shapeLayer)
}
示例
其他layer

还有一些其他的layer 这里不一一举例了,感兴趣的可以一一查看文档

  • CATextLayer 用来显示文本
  • CATransformLayer 用来做变换
  • CAReplicatorLayer 高效生成许多相似的图层
  • CAScrollLayer 可以实现滚动
  • CATiledLayer 载入大图造成的性能问题提供了一个解决方案:将大图分解成小片然后将他们单独按需载入
  • CAEmitterLayer 高性能的粒子引擎,被用来创建实时例子动画如:烟雾,火,雨等等这些效果。
  • CAEAGLLayer OpenGL相关的替代, 还有一个CLKView
  • AVPlayerLayer AVPlayerLayer是有别的框架(AVFoundation)提供的,它和Core Animation紧密地结合在一起,提供了一个CALayer子类来显示自定义的内容类型。AVPlayerLayer是用来在iOS上播放视频的。他是高级接口例如MPMoivePlayer的底层实现。 如果想要哪个View的背景播放一段视频。可以考虑使用它。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,376评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,126评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,966评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,432评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,519评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,792评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,933评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,701评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,143评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,488评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,626评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,292评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,896评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,742评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,324评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,494评论 2 348

推荐阅读更多精彩内容

  • 转载://www.greatytc.com/p/32fcadd12108 每个UIView有一个伙伴称为l...
    F麦子阅读 6,168评论 0 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥iOS动画全貌。在这里你可以看...
    F麦子阅读 5,101评论 5 13
  • 在iOS中随处都可以看到绚丽的动画效果,实现这些动画的过程并不复杂,今天将带大家一窥ios动画全貌。在这里你可以看...
    每天刷两次牙阅读 8,471评论 6 30
  • 每个UIView有一个伙伴称为layer,一个CALayer。UIView实际上并没有把自己画到屏幕上;它绘制本身...
    shenzhenboy阅读 3,089评论 0 17
  • 媒体爆出榆林产妇跳楼自尽的新闻,我本不想追这个热点。可怜之人必有可恨之处,一个生前掌握不了自己命运的人,身后怎样的...
    小邪说阅读 578评论 1 0