Swift程序员首页投稿(暂停使用,暂停投稿)

Swift 4 动画 - 2. CALayer

2017-11-25  本文已影响550人  smalldu

所有示例代码均可以在 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的几个基本属性:

- 示例

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

view1.layer.contentsScale = #imageLiteral(resourceName: "rabbit").scale
// or 
view1.layer.contentsScale = UIScreen.main.scale
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属性,以及下面的方法

不同于视图的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的坐标系统之间的转换方法:

可用来转换 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的动画。

视觉效果

给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类型

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

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为默认。

最后一个是他的初始化方法,需要设置一个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相关属性可以这样使用

甚至你可以把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 做一些路径动画的时候将非常有用。目前只看下基本使用

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 这里不一一举例了,感兴趣的可以一一查看文档

上一篇下一篇

猜你喜欢

热点阅读