Swift 4 动画 - 2. CALayer
所有示例代码均可以在 Animations-Demo 下载到
上节提到 UIView 上所有动画归根结底都是发生在Layer 层,所以动画的学习离不开Layer的学习。
我们平时开发中很少使用layer,但是我们却一直在使用layer。view是不具备绘制能力的,真正绘制的是他的underlying layer 。 每个view都有一个layer属性。view上显示相关的属性也是layer属性的一个映射。屏幕显示的时候 UIView
将layer
绘制上去。视图不会被经常重绘;相反,它的绘制会被缓存,在可用的地方都会使用缓存版本(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
相当于UIView
的contentMode
属性 , 有以下值- 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
相当于UIView
上clipsToBounds
超出部分是否裁剪 -
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
设置颜色 -
阴影
由shadowColor
,shadowOpacity
,shadowRadius
和shadowOffset
属性定义,为使该层绘制阴影,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
的透视效果通过一个矩阵中一个很简单的元素来控制:m34
。m34
用于按比例缩放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的背景播放一段视频。可以考虑使用它。