Swift 4 动画 - 2. CALayer

作者: smalldu | 来源:发表于2017-11-25 09:04 被阅读550次

    所有示例代码均可以在 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的背景播放一段视频。可以考虑使用它。

    相关文章

      网友评论

        本文标题:Swift 4 动画 - 2. CALayer

        本文链接:https://www.haomeiwen.com/subject/fnravxtx.html