CALayer教程

原文:CALayer Tutorial for iOS: Getting Started
本文介绍什么是CALayer,以及十个使用CALayer的列子.此教程更新到了iOS 11, Swift 4, 和 Xcode 9.


如你所知,在iOS应用里看到的所有东西都是一个视图.有按钮视图,标视图,滑动视图,甚至容器视图.
但你有所可能不知的是每个视图后面都有一个CALayer在支持.
这篇文章讲解了CALayer的原理,并且展示十个CALayer效果的示例.
本文基于读者熟悉iOS基础开发知识和Swift,包括故事板(storyboards)的使用.

提示:如果不熟悉可以看这些文章
Learn to Code iOS Apps with Swif

The iOS Apprentice.

CALayer如何关联UIView

UIview处理试图布局和触摸事件,但是不直接处理绘图和动画,UIKIt把这些事交给CoreAnimation处理.UIView实际上只是CALayer的封装.当你设置一个UIView的bounds的时候,实际上只是简单地设置背后的CALayer的bounds.如果你调用UIView的layoutIfNeeded方法,这个调用会向前传递到根CALayer上去.每个UIView都有一个根CALayer.


起步

观察CALayer的反应是了解他们的最快方法.所以我们从一个最简单地项目开始.下载这个只有一个视图在屏幕中央的项目

用下面的代码替换ViewController.swift里面的内容:

  import UIKit

class ViewController: UIViewController {
  
  @IBOutlet weak var viewForLayer: UIView!
  
  var layer: CALayer {
    return viewForLayer.layer
  }
  
  override func viewDidLoad() {
    super.viewDidLoad()
    setUpLayer()
  }
  
  func setUpLayer() {
    layer.backgroundColor = UIColor.blue.cgColor
    layer.borderWidth = 100.0
    layer.borderColor = UIColor.red.cgColor
    layer.shadowOpacity = 0.7
    layer.shadowRadius = 10.0
  }

  @IBAction func tapGestureRecognized(_ sender: Any) {
    
  }
  
  @IBAction func pinchGestureRecognized(_ sender: Any) {
    
  }
  
}

之前提到每个视图都有一个layer相关联,可以通过.layer获取这个layer.这段代码第一件事是创建一个访问viewForLayer的layer的属性,名叫layer.

代码还调用setUpLayer()来设置layer的一些属性:阴影,蓝色背景.和一圈很粗的红色边界.待会会讲解setUpLayer(),但让我们首先跑一下这个项目看一下效果.

因为每个视图都有layer,所以你可以在任何的视图上做这些效果.

基本的CALayer属性

CALayer有一些可以让你自定义的属性.想一想我们之前做的:

  • 把默认没有背景色改成蓝色
  • 把边界的宽度从0变成100
  • 把边界的颜色黑色改成红色
  • 把阴影的可见度从0变成0.7,然后修改阴影的半径从3改成10.

这些只是部分可设置的属性.我们再来试两个,同样在setUpLayer()里:

layer.contents = UIImage(named: "star")?.cgImage
layer.contentsGravity = kCAGravityCenter

contents属性可以让你设置layer的内容为一张图片.这里我们设置了一张"star"的图片.这张图已经添加到项目里了.再来运行看一下:



注意到星星是如何居中的,这是因为我们设置了kCAGravityCenter.当然你也可以居上,居下等等.

改变layer的外观

项目已经包含的点击缩放手势.
把tapGestureRecognized(_:)变成这样:

@IBAction func tapGestureRecognized(_ sender: UITapGestureRecognizer) {
  layer.shadowOpacity = layer.shadowOpacity == 0.7 ? 0.0 : 0.7
}

这样在点击的时候,layer的阴影可见度会在0.7和0之间变化.
再把pinchGestureRecognized(_:)变成这样:

@IBAction func pinchGestureRecognized(_ sender: UIPinchGestureRecognizer) {
  let offset: CGFloat = sender.scale < 1 ? 5.0 : -5.0
  let oldFrame = layer.frame
  let oldOrigin = oldFrame.origin
  let newOrigin = CGPoint(x: oldOrigin.x + offset, y: oldOrigin.y + offset)
  let newSize = CGSize(width: oldFrame.width + (offset * -2.0), height: oldFrame.height + (offset * -2.0))
  let newFrame = CGRect(origin: newOrigin, size: newSize)
  if newFrame.width >= 100.0 && newFrame.width <= 300.0 {
    layer.borderWidth -= offset
    layer.cornerRadius += (offset / 2.0)
    layer.frame = newFrame
  }
}

这里根据用户的缩放,调整偏移和layer的大小,边界大小,圆角大小.
默认的圆角值是0,也就是个矩形.增加圆角值可以让角变圆.给一个正方形设置宽度一半的圆角值可以变成圆形.
但是调整圆角值并不会裁剪layer的内容,除非把masksToBounds属性设置为true.
运行一下:


CALayer之旅

CALayer不单单只有这些属性和方法可以使用.而且还有好多子类提供更多的属性和方法.
文章接下来的内容需要以下东西:

示例1: CALayers

之前你已经使用过CALayer的一些属性.
但是下面这些还未提及过:

  • Layers可以拥有子Layers
  • Layers属性是有动画的.设置Layers属性的时候,它是会随着默认时间变化的,当然也可以修改时间.
  • Layers是轻量化的. Layers比view更轻量,所以它可以提供更好的性能.
  • Layers拥有大量有用的属性.

带你看一下CALayer所有的属性-有一些还没见过,但是很有用.

let layer = CALayer()
layer.frame = someView.bounds

layer.contents = UIImage(named: "star")?.cgImage
layer.contentsGravity = kCAGravityCenter

创建一个CALayer实例,设置他的frame为someView的frame.设置内容为一张图片在中央.

layer.magnificationFilter = kCAFilterLinear
layer.isGeometryFlipped = false

设置放大模式.

前面的变化不会有动画效果,如果不把isGeometryFlipped设置成true,坐标系是不一致的.继续:

layer.backgroundColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0).cgColor
layer.opacity = 1.0
layer.isHidden = false
layer.masksToBounds = false

背景色设为绿色.同时,不要裁剪内容,如果图片比layer大,图片不会被裁剪掉.

layer.cornerRadius = 100.0
layer.borderWidth = 12.0
layer.borderColor = UIColor.white.cgColor

通过设置圆角为宽度的一半,创建一个视觉上的圆形.注意颜色是CGColor.

layer.shadowOpacity = 0.75
layer.shadowOffset = CGSize(width: 0, height: 3)
layer.shadowRadius = 3.0
someView.layer.addSublayer(layer)

创建阴影,设置shouldRasterize为true.添加到视图层上.
结果:


CALayer有2个可以提高性能的属性:
shouldRasterize 和 drawsAsynchronously.
shouldRasterize在默认情况下是false,当设置为true时,它可以提高性能,因为层的内容只需要呈现一次。它非常适合在屏幕上动画但外观不变的对象。

drawsAsynchronously 是shouldRasterize的反义词。默认情况下也是false。将它设置为true,以便在必须重复重绘图层内容时提高性能,例如在处理连续呈现动画粒子的发射器图层时。(稍后请参阅CAEmitterLayer示例)。

在异步设置shouldrastze或drawsasynchronze之前,请考虑这些影响。比较true和false之间的性能,这样您就知道激活这些属性是否真的提高了性能。当使用不当时,性能很可能急剧下降。
现在把你的注意力暂时转移到Layer播放器上。它包括操纵许多CALayer属性的控件:



试一下的控制-这是一个很棒的方法来获得你可以用CALayer做什么感觉!
Layers不是响应链的一部分,所以它们不会像视图那样直接响应触摸或手势,就像您在CALayerPlayground示例中看到的那样。
但是,您可以测试它们,您将在CATransformLayer的示例代码中看到。您还可以向图层添加自定义动画,您将在CAReplicatorLayer中看到这些。

示例2: Layers

CAScrollLayer显示可滚动层的一部分。它非常的基础,不能直接响应用户的触摸,甚至不能检查可滚动层的边界,但它做了一些很酷的事情,比如防止滚动超出边界无限!

UIScrollView不使用CAScrollLayer来做它的工作,而是直接改变它的层的界限。

你可以用CAScrollLayer来设置它的滚动模式为水平或垂直,并通过编程告诉它滚动到一个特定的点或区域:

// 1
var scrollingViewLayer: CAScrollLayer {
  return scrollingView.layer as! CAScrollLayer
}

override func viewDidLoad() {
  super.viewDidLoad()
  // 2
  scrollingViewLayer.scrollMode = kCAScrollBoth
}

@IBAction func panRecognized(_ sender: UIPanGestureRecognizer) {
  var newPoint = scrollingView.bounds.origin
  newPoint.x -= sender.translation(in: scrollingView).x
  newPoint.y -= sender.translation(in: scrollingView).y
  sender.setTranslation(CGPoint.zero, in: scrollingView)
  // 3
  scrollingViewLayer.scroll(to: newPoint)
  
  if sender.state == .ended {
    UIView.animate(withDuration: 0.3, delay: 0, options: [], animations: {
        self.scrollingViewLayer.scroll(to: CGPoint.zero)
    })
  }
}

以上代码:

  • 一个属性,用于返回scrollingView的底层CAScrollLayer。
  • 滚动最初设置为水平和垂直。
  • 当一个平移被识别时,一个新的点被创建,滚动层在UIView动画中滚动到那个点。注意,scroll(to:)不会自动动画。

Layer播放器演示了一个CAScrollLayer,它包含一个图像视图,其中的图像大于滚动视图的边界。当您运行上述代码并平移视图时,结果如下:

层播放器包括两个控件来锁定水平和垂直滚动。

下面是一些使用(或不使用)CAScrollLayer的经验法则:

如果您想要轻量级的东西,并且只需要通过编程滚动,可以考虑使用CAScrollLayer。
如果希望用户能够滚动,最好使用UIScrollView。要了解更多信息,请查看我们关于此的18部分视频教程系列。
如果您正在滚动一个非常大的图像,请考虑使用CATiledLayer(更多信息见下文)。播放器包括两个控件来锁定水平和垂直滚动。

下面是一些使用(或不使用)CAScrollLayer的经验法则:

  • 如果您想要轻量级的东西,并且只需要通过编程滚动,可以考虑使用CAScrollLayer。
  • 如果希望用户能够滚动,最好使用UIScrollView。要了解更多信息,请查看我们关于此的18部分视频教程系列。
  • 如果您正在滚动一个非常大的图像,请考虑使用CATiledLayer(更多信息见下文)。

示例3: CATextLayer

CATextLayer提供了简单但快速的纯文本或带属性字符串呈现。与UILabel不同,CATextLayer不能有指定的UIFont,只能有CTFontRef或CGFontRef。

使用这样的代码块,可以操作字体、字体大小、颜色、对齐、换行和截断,Layer Player包含两个控件,用于锁定水平和垂直滚动,以及动画更改:

// 1
let textLayer = CATextLayer()
textLayer.frame = someView.bounds

// 2
let string = String(
  repeating: "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit 
             congue dictum. ",
  count: 20
)

textLayer.string = string

// 3
textLayer.font = CTFontCreateWithName(fontName, fontSize, nil)

// 4
textLayer.foregroundColor = UIColor.darkGray.cgColor
textLayer.isWrapped = true
textLayer.alignmentMode = kCAAlignmentLeft
textLayer.contentsScale = UIScreen.main.scale
someView.layer.addSublayer(textLayer)

以上代码说明:

  • 创建一个CATextLayer实例,并将其设置为someView的边界。
  • 创建一个重复文本字符串,并将其分配给文本层。
  • 创建字体并将其分配给文本layer。
  • 将文本layer设置为换行和左对齐(您可以选择将其设置为自然、右、中、对齐),并将其contentsScale匹配到屏幕上,然后将该layer添加到视图层次结构中。

所有的layer类,不仅仅是CATextLayer,在默认情况下呈现的比例系数为1。当附加到视图时,layer自动将它们的contentsScale设置为当前屏幕的适当比例因子。你需要为你手动创建的图层显式设置contentsScale,否则它们的比例系数将是1,你会在视网膜显示屏上有像素化。

如果添加到一个方形的UIView中,创建的文本层将如下所示:



截断是一种可以使用的设置,当您希望用省略号表示截断的文本时,这种设置非常好。截断默认为none,可以设置为start、end和middle:


播放器有控制改变许多CATextLayer的属性:

layer

示例4: AVPlayerLayer
AVPlayerLayer为AVFoundation添加了一个layer。它持有一个AVPlayer播放AV媒体文件(AVPlayerItems)。下面是一个创建AVPlayerLayer的例子:

var player: AVPlayer!

override func viewDidLoad() {
  super.viewDidLoad()

  // 1
  let playerLayer = AVPlayerLayer()
  playerLayer.frame = someView.bounds
  
  // 2
  let url = Bundle.main.url(forResource: "someVideo", withExtension: "m4v")
  player = AVPlayer(url: url!)
  
  // 3
  player.actionAtItemEnd = .none
  playerLayer.player = player
  someView.layer.addSublayer(playerLayer)
  
  // 4
  NotificationCenter.default.addObserver(self,
                                         selector: #selector(playerDidReachEnd),
                                         name: .AVPlayerItemDidPlayToEndTime,
                                         object: player.currentItem)
}

deinit {
  NotificationCenter.default.removeObserver(self)
}

上述代码的分项:

创建一个新的播放器layer并设置它的frame。
创建具有AV asset的播放器。
告诉玩家在游戏结束后什么也不要做;其他选项包括暂停或推进到下一个asset,如果适用。
当AVPlayer完成对asset的操作时注册通知(并删除控制器作为deinit中的观察者)。

接下来,当点击play按钮时,它切换控件来播放AV asset并设置按钮的标题。

  if playButton.titleLabel?.text == "Play" {
    player.play()
    playButton.setTitle("Pause", for: .normal)
  } else {
    player.pause()
    playButton.setTitle("Play", for: .normal)
  }
}

然后当播放器到达结束时,将播放光标移动到开始位置。

@objc func playerDidReachEnd(notification: NSNotification) {
  let playerItem = notification.object as! AVPlayerItem
  playerItem.seek(to: kCMTimeZero, completionHandler: nil)
}

注意,这只是一个简单的示例。在真实的项目中,通常不建议将焦点放在按钮的标题文本上。

上面创建的AVPlayerLayer及其AVPlayer将由AVPlayerItem实例的第一帧可视化表示,如下所示:

AVPlayerLayer有几个额外的属性:

  • videoGravity 设置视频显示的缩放行为。
  • isReadyForDisplay 检查视频是否准备好显示。
    另一方面,AVPlayer有一些额外的属性和方法。需要注意的一点是速率,速率是从0到1的回放速率。0表示暂停,1表示视频按正常速度播放(1x)。

然而,设置速率也指示回放以该速率开始。换句话说,调用pause()和设置速率为0与调用play()和设置速率为1做相同的事情。

那么快进、慢动作或者倒立播放呢?AVPlayer都帮你搞定了。将速率设置为任何高于1的值,就相当于要求播放器以正常速度的数倍开始播放,例如,将速率设置为2意味着双倍速度。

正如您可能设想的那样,将速率设置为负数将指示回放以该数字乘以正常速度的倒数开始。

然而,在以常规速度(向前)以外的任何速度播放之前,AVPlayerItem上要检查适当的变量,以验证它可以以该速度播放:

  • canPlayFastForward 任何大于1的数
  • canPlaySlowForward 对0到1之间的任何数字进行慢进,但不包括1
  • canPlayReverse -1
  • canPlaySlowReverse 对-1到(但不包括)0之间的任何数字进行慢速反转
  • canPlayFastReverse 任何小于-1的数字
    大多数视频都可以以不同的前进速度播放,但倒着播放就不那么典型了。层播放器还包括回放控制:


示例5: CAGradientLayer

CAGradientLayer可以轻松地将两种或两种以上的颜色混合在一起,使其特别适合于背景。为了配置它,您需要分配一个CGColors数组,以及一个起始点和一个端点来指定渐变层的起始点和结束点。

记住,起始点和终点不是显式点。相反,它们是在单位坐标空间中定义的,然后在绘制时映射到层的边界。换句话说,x值为1表示该点在层的右边缘,y值为1表示该点在层的底边缘。

CAGradientLayer有一个类型属性,尽管kCAGradientLayerAxial是唯一的选项,它通过数组中的每个颜色线性地过渡。

这意味着,如果你在起始点和终点之间画一条直线(a),渐变会沿着一条与a垂直的假想线(B)进行,而沿着B的所有点都是相同的颜色:

或者,您可以使用一个值在0到1之间的数组来控制location属性,该数组指定渐变层应该在颜色数组中使用下一个颜色的相对位置。

如果未指定停止位置,则停止位置默认为均匀间隔。但是,如果设置了位置,它的计数必须与颜色计数匹配,否则将发生不希望发生的事情:[

下面是一个如何创建渐变层的例子:

func cgColor(red: CGFloat, green: CGFloat, blue: CGFloat) -> CGColor {
  return UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0).cgColor
}

let gradientLayer = CAGradientLayer()
gradientLayer.frame = someView.bounds
gradientLayer.colors = [cgColor(red: 209.0, green: 0.0, blue: 0.0),
                        cgColor(red: 255.0, green: 102.0, blue: 34.0),
                        cgColor(red: 255.0, green: 218.0, blue: 33.0),
                        cgColor(red: 51.0, green: 221.0, blue: 0.0),
                        cgColor(red: 17.0, green: 51.0, blue: 204.0),
                        cgColor(red: 34.0, green: 0.0, blue: 102.0),
                        cgColor(red: 51.0, green: 0.0, blue: 68.0)]

gradientLayer.startPoint = CGPoint(x: 0, y: 0)
gradientLayer.endPoint = CGPoint(x: 0, y: 1)
someView.layer.addSublayer(gradientLayer)

在上面的代码中,您创建了一个渐变layer,将其frame与someView的边界一样,分配一个颜色数组,设置起点和终点,并将渐变层添加到视图层次结构中。它看起来是这样的:


如此丰富多彩!接下来,你将编写一个从应用程序中飞来的蝴蝶来逗你的鼻子。

layer播放器提供你的控制改变开始和结束点,颜色和地点:


示例6: CAReplicatorLayer

CAReplicatorLayer复制一个图层指定次数,这允许您创建一些很酷的效果。

每一层复制都可以有自己的颜色和位置变化,它的绘制可以延迟,给复制器的整个层一个动画效果。深度也可以保持给复制层一个3D效果。这里有一个例子:

首先,创建一个CAReplicatorLayer的实例,并将它的frame设置为someView的bounds。

let replicatorLayer = CAReplicatorLayer()
replicatorLayer.frame = someView.bounds

接下来,设置replicator层的副本数量(instanceCount)和绘制延迟。还要将replicator层设置为2D (preservesDepth = false),并将其实例颜色设置为白色。

replicatorLayer.instanceCount = 30
replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0)
replicatorLayer.preservesDepth = false
replicatorLayer.instanceColor = UIColor.white.cgColor

然后,将红/绿/蓝偏移量添加到每个连续复制实例的颜色值。

replicatorLayer.instanceRedOffset = 0.0
replicatorLayer.instanceGreenOffset = -0.5
replicatorLayer.instanceBlueOffset = -0.5
replicatorLayer.instanceAlphaOffset = 0.0

每个默认值为0,这有效地在所有实例中保留颜色值。但是,在本例中,实例颜色最初设置为白色,这意味着红、绿和蓝已经是1.0了。因此,将红色设置为0,将绿色和蓝色偏移量设置为负数,就可以让红色成为突出的颜色。类似地,将alpha偏移量添加到每个连续复制实例的alpha中。

然后,创建一个转换,使每个后续实例围绕一个圆旋转。

let angle = Float(Double.pi * 2.0) / 30
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
someView.layer.addSublayer(replicatorLayer)

然后为replicator layer创建一个要使用的实例layer
,并设置它的frame,以便第一个实例将在x中心和someView边界的顶部绘制。另外,设置实例的颜色,并将实例层添加到replicator layer。

let instanceLayer = CALayer()
let layerWidth: CGFloat = 10.0
let midX = someView.bounds.midX - layerWidth / 2.0
instanceLayer.frame = CGRect(x: midX, y: 0.0, width: layerWidth, height: layerWidth * 3.0)
instanceLayer.backgroundColor = UIColor.white.cgColor
replicatorLayer.addSublayer(instanceLayer)

现在,制作一个渐变动画,使不透明度从1(opaque)变为0(transparent)。

let fadeAnimation = CABasicAnimation(keyPath: "opacity")
fadeAnimation.fromValue = 1.0
fadeAnimation.toValue = 0.0
fadeAnimation.duration = 1
fadeAnimation.repeatCount = Float.greatestFiniteMagnitude

最后,将实例layer的不透明度设置为0,以便在绘制每个实例并设置其颜色和alpha值之前保持透明。

instanceLayer.opacity = 0.0
instanceLayer.add(fadeAnimation, forKey: "FadeAnimation")

效果:



layer播放器可以控制大多数这些属性:


示例7: CATiledLayer

CATiledLayer在tile中异步绘制层内容。对于非常大的图片或其他内容集来说,这是非常好的,因为可以看到内容,而不必将其全部加载到内存中。

有几种处理绘图的方法。一种是覆盖UIView,使用CATiledLayer重复绘制tile来填充view的背景,就像这样:

视图控制器显示一个TiledBackgroundView:

import UIKit

class ViewController: UIViewController {
  
  @IBOutlet weak var tiledBackgroundView: TiledBackgroundView!
  
}

TiledBackgroundView定义:

import UIKit

class TiledBackgroundView: UIView {
  
  let sideLength: CGFloat = 50.0
  
  // 1
  override class var layerClass: AnyClass {
    return CATiledLayer.self
  }
  
  // 2
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    srand48(Int(Date().timeIntervalSince1970))
    let layer = self.layer as! CATiledLayer
    let scale = UIScreen.main.scale
    layer.contentsScale = scale
    layer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)
  }
  
  // 3
  override func draw(_ rect: CGRect) {
    let context = UIGraphicsGetCurrentContext()
    let red = CGFloat(drand48())
    let green = CGFloat(drand48())
    let blue = CGFloat(drand48())
    context?.setFillColor(red: red, green: green, blue: blue, alpha: 1.0)
    context?.fill(rect)
  }
  
}

以下是在上述代码中发生的事情:

  • layerClass被重写,因此该视图的alyer被创建为CATiledLayer的实例。
  • 在draw(_:)中生成随机颜色的rand48()函数的种子。然后缩放该layer的内容(转换为CATiledLayer),以匹配屏幕的缩放和其平铺大小设置。
  • 重写draw(_:),用随机颜色的平铺层填充视图。
    最后,上面的代码绘制了一个由随机着色的正方形瓷砖组成的6x6网格,如下所示:


Layer 播放器通过在平铺层背景上绘制路径来扩展这种用法:


CATiledLayer 细节

当你放大视图时,上面截图中的星星变得模糊:


这种模糊是由图层保持的细节层次造成的。CATiledLayer有两个属性,levelsOfDetail和levelsOfDetailBias。

levelsOfDetail,顾名思义,是由层维护的详细级别的数量。它的默认值是1,每一增量级别的缓存分辨率都是前一级别的一半。一个图层的最大层次细节值是它最下面的层次至少有一个像素。

另一方面,levelsOfDetailBias是这个层缓存的放大级别的细节数量。它的默认值是0,这意味着不会缓存额外的放大级别,而且每个增量级别的缓存速度都是前一级别的两倍。

例如,将上面模糊的平铺层的levelsOfDetailBias增加到5会导致缓存级别放大到2x、4x、8x、16x和32x,放大后的图层会是这样的:


CATiledLayer 异步绘制

CATiledLayer还有另一个有用的用途:异步绘制非常大的图像的图像块,例如,在滚动视图中。

您必须提供tiles和逻辑来告诉tiles layer在用户滚动时应该抓取哪个tiles,但是这里的性能提升是显著的。

它的工作是将源图像分割成指定大小的正方形块,根据每个块的列和行位置命名;例如,windingRoad_6_2。第7列第3行平铺的png(零索引):


有了这些贴图,可以创建一个自定义UIView子类来绘制这些贴图层:

import UIKit
// 1
let sideLength: CGFloat = 640.0
let fileName = "windingRoad"

class TilingViewForImage: UIView {
  
  let cachesPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0] as String
  
  // 2
  override class var layerClass : AnyClass {
    return CATiledLayer.self
  }
  
  // 3
  required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)
    guard let layer = self.layer as? CATiledLayer else { return nil }
    layer.tileSize = CGSize(width: sideLength, height: sideLength)
  }

上面的代码:

为tile边的长度、基本映像文件名和TileCutter扩展保存tiles的缓存目录的路径创建属性。
覆盖层类以返回CATiledLayer。
在视图层中实现init(coder:),将其转换为平铺层并设置其平铺大小。请注意,没有必要将contentsScale与屏幕比例匹配,因为您直接使用视图的支持层。
下一步,重写draw(_:),根据每个tile的列和行位置绘制每个tile。

  override func draw(_ rect: CGRect) {
    let firstColumn = Int(rect.minX / sideLength)
    let lastColumn = Int(rect.maxX / sideLength)
    let firstRow = Int(rect.minY / sideLength)
    let lastRow = Int(rect.maxY / sideLength)
    
    for row in firstRow...lastRow {
      for column in firstColumn...lastColumn {
        guard let tile = imageForTile(atColumn: column, row: row) else {
          continue
        }
        let x = sideLength * CGFloat(column)
        let y = sideLength * CGFloat(row)
        let point = CGPoint(x: x, y: y)
        let size = CGSize(width: sideLength, height: sideLength)
        var tileRect = CGRect(origin: point, size: size)
        tileRect = bounds.intersection(tileRect)
        tile.draw(in: tileRect)
      }
    }
  }
  
  func imageForTile(atColumn column: Int, row: Int) -> UIImage? {
    let filePath = "\(cachesPath)/\(fileName)_\(column)_\(row)"
    return UIImage(contentsOfFile: filePath)
  }
  
}

然后,可以将大小与原始图像的尺寸相同的TilingViewForImage添加到滚动视图中。

这样,你就可以流畅地滚动一个大图像(在layer播放器中是5120 x 3200),这要归功于CATiledLayer:


正如你可以在上面的动画中看到的,当绘制单个的块时,快速滚动会出现明显的阻塞。通过使用较小的tiles(上面例子中使用的tiles被切为640 x 640)和创建一个定制的CATiledLayer子类并覆盖fadeDuration()来返回0,从而最小化这种行为:

class TiledLayer: CATiledLayer {
  
  override class func fadeDuration() -> CFTimeInterval {
    return 0.0
  }
  
}

示例8: CAShapeLayer
CAShapeLayer使用可缩放的矢量路径绘制,比使用图像快得多。这里的另一个好处是,您不再需要提供常规的@2x和@3x大小的图像。w00t !

此外,您还可以使用各种属性来定制线条粗细、颜色、虚线、线条如何连接其他线条,以及是否应该填充该区域以及使用什么颜色等等。这里有一个例子:

首先,创建颜色、路径和形状层。

import UIKit

class ViewController: UIViewController {
  
  @IBOutlet weak var someView: UIView!
  
  let rwColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0)
  let rwPath = UIBezierPath()
  let rwLayer = CAShapeLayer()

接下来,绘制形状层的路径。您可以使用move(to:)或addLine(to:)等方法从一个点绘制到另一个点。

  func setUpRWPath() {
    rwPath.move(to: CGPoint(x: 0.22, y: 124.79))
    rwPath.addLine(to: CGPoint(x: 0.22, y: 249.57))
    rwPath.addLine(to:CGPoint(x: 124.89, y: 249.57))
    rwPath.addLine(to:CGPoint(x: 249.57, y: 249.57))
    rwPath.addLine(to:CGPoint(x: 249.57, y: 143.79))
    rwPath.addCurve(to:CGPoint(x: 249.37, y: 38.25), 
                    controlPoint1: CGPoint(x: 249.57, y: 85.64), 
                    controlPoint2: CGPoint(x: 249.47, y: 38.15))
    rwPath.addCurve(to:CGPoint(x: 206.47, y: 112.47), 
                    controlPoint1: CGPoint(x: 249.27, y: 38.35), 
                    controlPoint2: CGPoint(x: 229.94, y: 71.76))
    rwPath.addCurve(to:CGPoint(x: 163.46, y: 186.84), 
                    controlPoint1: CGPoint(x: 182.99, y: 153.19), 
                    controlPoint2: CGPoint(x: 163.61, y: 186.65))
    rwPath.addCurve(to:CGPoint(x: 146.17, y: 156.99), 
                    controlPoint1: CGPoint(x: 163.27, y: 187.03), 
                    controlPoint2: CGPoint(x: 155.48, y: 173.59))
    rwPath.addCurve(to:CGPoint(x: 128.79, y: 127.08), 
                    controlPoint1: CGPoint(x: 136.82, y: 140.43), 
                    controlPoint2: CGPoint(x: 129.03, y: 126.94))
    rwPath.addCurve(to:CGPoint(x: 109.31, y: 157.77), 
                    controlPoint1: CGPoint(x: 128.59, y: 127.18), 
                    controlPoint2: CGPoint(x: 119.83, y: 141.01))
    rwPath.addCurve(to:CGPoint(x: 89.83, y: 187.86), 
                    controlPoint1: CGPoint(x: 98.79, y: 174.52), 
                    controlPoint2: CGPoint(x: 90.02, y: 188.06))
    rwPath.addCurve(to:CGPoint(x: 56.52, y: 108.28), 
                    controlPoint1: CGPoint(x: 89.24, y: 187.23), 
                    controlPoint2: CGPoint(x: 56.56, y: 109.11))
    rwPath.addCurve(to:CGPoint(x: 64.02, y: 102.25), 
                    controlPoint1: CGPoint(x: 56.47, y: 107.75), 
                    controlPoint2: CGPoint(x: 59.24, y: 105.56))
    rwPath.addCurve(to:CGPoint(x: 101.42, y: 67.57), 
                    controlPoint1: CGPoint(x: 81.99, y: 89.78), 
                    controlPoint2: CGPoint(x: 93.92, y: 78.72))
    rwPath.addCurve(to:CGPoint(x: 108.38, y: 30.65), 
                    controlPoint1: CGPoint(x: 110.28, y: 54.47), 
                    controlPoint2: CGPoint(x: 113.01, y: 39.96))
    rwPath.addCurve(to:CGPoint(x: 10.35, y: 0.41), 
                    controlPoint1: CGPoint(x: 99.66, y: 13.17), 
                    controlPoint2: CGPoint(x: 64.11, y: 2.16))
    rwPath.addLine(to:CGPoint(x: 0.22, y: 0.07))
    rwPath.addLine(to:CGPoint(x: 0.22, y: 124.79))
    rwPath.close()
  }

如果写这种样板图纸代码不是你的爱好,检查PaintCode
;它通过允许您使用直观的可视化控件绘图或导入现有矢量(SVG)或Photoshop (PSD)文件来生成代码。

然后,建立形状层:

  func setUpRWLayer() {
    rwLayer.path = rwPath.cgPath
    rwLayer.fillColor = rwColor.cgColor
    rwLayer.fillRule = kCAFillRuleNonZero
    rwLayer.lineCap = kCALineCapButt
    rwLayer.lineDashPattern = nil
    rwLayer.lineDashPhase = 0.0
    rwLayer.lineJoin = kCALineJoinMiter
    rwLayer.lineWidth = 1.0
    rwLayer.miterLimit = 10.0
    rwLayer.strokeColor = rwColor.cgColor
  }

将其路径设置为上面绘制的路径,其填充颜色设置为步骤1中创建的颜色,并将填充规则显式设置为非零的默认值。

唯一的另一个选项是偶数-奇数,对于这个没有相交路径的形状,填充规则没有什么区别。
非零规则将从左到右的路径记为+1,从右到左的路径记为-1;它将路径的所有值相加,如果总数大于0,它将填充路径形成的形状。
本质上,非零填充了形状内的所有点。
偶数-奇数规则计算形成形状的路径交叉点的总数,如果计数为奇数,则填充该形状。这绝对是一个图片胜过千言万语的例子。
形成五边形的奇偶图中的路径交叉点的数量是偶数,所以五边形没有被填满,而形成每个三角形的路径交叉点的数量是奇数,所以三角形被填满。



最后,调用路径绘制和层设置代码,然后将层添加到视图层次结构中。

  override func viewDidLoad() {
    super.viewDidLoad()
    
    setUpRWPath()
    setUpRWLayer()
    someView.layer.addSublayer(rwLayer)
  }

}

效果:


如果你想知道这幅画在PaintCode中是什么样子的,请点击PaintCode
:


layer播放器包括控制操纵许多CAShapeLayer的属性:

你可能会注意到,我们跳过了Layer Player应用程序的下一个演示。这是因为CAEAGLLayer实际上已经被CAMetalLayer淘汰了,后者与Metal框架一起在iOS 8中首次亮相。你可以在这里
找到一个关于CAMetalLayer的教程。

示例9: CATransformLayer

CATransformLayer不像其他层类那样扁平它的子层层次结构,因此它便于绘制3D结构。它实际上是它的子层的容器,每个子层都可以有自己的变换和不透明度变化,但是,它忽略了对其他呈现层属性的更改,比如边框宽度和颜色。

您不能直接点击测试转换层,因为它没有一个二维坐标空间来映射一个接触点,然而,它可以点击测试单个子层。这里有一个例子:

首先为边长、立方体每边的颜色和转换层创建属性。

import UIKit

class ViewController: UIViewController {

  @IBOutlet weak var someView: UIView!
  
  let sideLength = CGFloat(160.0)
  let redColor = UIColor.red
  let orangeColor = UIColor.orange
  let yellowColor = UIColor.yellow
  let greenColor = UIColor.green
  let blueColor = UIColor.blue
  let purpleColor = UIColor.purple
  let transformLayer = CATransformLayer()

创建一些辅助代码,以创建具有指定颜色的立方体的每个边层,并将度数转换为弧度。为什么弧度?因为我觉得用角度比用弧度更直观.

  func sideLayer(color: UIColor) -> CALayer {
    let layer = CALayer()
    layer.frame = CGRect(origin: CGPoint.zero, size: CGSize(width: sideLength, height: sideLength))
    layer.position = CGPoint(x: someView.bounds.midX, y: someView.bounds.midY)
    layer.backgroundColor = color.cgColor
    return layer
  }
  
  func degreesToRadians(_ degrees: Double) -> CGFloat {
    return CGFloat(degrees * .pi / 180.0)
  }

然后通过创建、旋转和向转换层添加每一边来构建多维数据集。然后设置转换层的z轴锚点,旋转立方体并将立方体添加到视图层次结构中。

  func setUpTransformLayer() {
    var layer = sideLayer(color: redColor)
    transformLayer.addSublayer(layer)
    
    layer = sideLayer(color: orangeColor)
    var transform = CATransform3DMakeTranslation(sideLength / 2.0, 0.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
    
    layer = sideLayer(color: yellowColor)
    layer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength)
    transformLayer.addSublayer(layer)
    
    layer = sideLayer(color: greenColor)
    transform = CATransform3DMakeTranslation(sideLength / -2.0, 0.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
    
    layer = sideLayer(color: blueColor)
    transform = CATransform3DMakeTranslation(0.0, sideLength / -2.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
    
    layer = sideLayer(color: purpleColor)
    transform = CATransform3DMakeTranslation(0.0, sideLength / 2.0, sideLength / -2.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)
    layer.transform = transform
    transformLayer.addSublayer(layer)
    
    transformLayer.anchorPointZ = sideLength / -2.0
    rotate(xOffset: 16.0, yOffset: 16.0)
  }

接下来编写一个函数,该函数应用基于指定的x和y偏移量的旋转。注意,代码将转换设置为subblayertransform,这适用于转换层的子层。

  func rotate(xOffset: Double, yOffset: Double) {
    let totalOffset = sqrt(xOffset * xOffset + yOffset * yOffset)
    let totalRotation = CGFloat(totalOffset * .pi / 180.0)
    let xRotationalFactor = CGFloat(totalOffset) / totalRotation
    let yRotationalFactor = CGFloat(totalOffset) / totalRotation
    let currentTransform = CATransform3DTranslate(transformLayer.sublayerTransform, 0.0, 0.0, 0.0)
    let x = xRotationalFactor * currentTransform.m12 - yRotationalFactor * currentTransform.m11
    let y = xRotationalFactor * currentTransform.m22 - yRotationalFactor * currentTransform.m21
    let z = xRotationalFactor * currentTransform.m32 - yRotationalFactor * currentTransform.m31
    let rotation = CATransform3DRotate(transformLayer.sublayerTransform, totalRotation, x, y, z)
    transformLayer.sublayerTransform = rotation
  }

然后观察触摸并循环通过转换层的子层。对每一层进行命中测试,并在检测到命中后立即跳出,因为命中测试剩余的层没有任何好处。

  override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: someView) else {
      return
    }
    for layer in transformLayer.sublayers! where layer.hitTest(location) != nil {
      print("Transform layer tapped!")
      break
    }
  }

最后,设置转换层并将其添加到视图层次结构中。

那么这些current transform .m##s有什么用呢?我很高兴你这样问:。这些是CATransform3D属性,表示由行和列组成的矩形数组组成的矩阵的元素。
要了解更多与本例中使用的矩阵转换类似的信息,请查看教程3DTransformFun projectEnter The Matrix project

运行上面的代码,someView是一个250 x 250的视图,结果是:


现在,尝试一些事情:点击立方体上的任何地方,“Transform layer tapped!”将打印到控制台。


示例10: CAEmitterLayer

CAEmitterLayer渲染作为CAEmitterCell实例的动画粒子。CAEmitterLayer和CAEmitterCell都具有更改呈现速率、大小、形状、颜色、速度、寿命等属性。这里有一个例子:

import UIKit

class ViewController: UIViewController {
  
  // 1
  let emitterLayer = CAEmitterLayer()
  let emitterCell = CAEmitterCell()
  
  // 2
  func setUpEmitterLayer() {
    emitterLayer.frame = view.bounds
    emitterLayer.seed = UInt32(Date().timeIntervalSince1970)
    emitterLayer.renderMode = kCAEmitterLayerAdditive
    emitterLayer.drawsAsynchronously = true
    setEmitterPosition()
  }
}

以上代码准备emitterLayer:

创建emitterLayer和cell。
通过以下步骤设置emitterLayer:
为层的随机数生成器提供一个种子,该生成器依次对层的发射器单元的某些属性(如速度)进行随机化。下面将进一步解释这一点。
以renderMode指定的顺序将发射器单元格渲染到图层背景颜色和边框之上。
将绘制异步设置为true,这可能会提高性能,因为发射器层必须不断地重新绘制其发射器单元。
接下来,通过助手方法设置发射器的位置。这是一个很好的案例研究如何设置绘图异步为真对动画的性能和流畅性有积极的影响。
最后,解释ViewController中设置CAEmitterCell缺少的方法:

接下来,设置发射器单元:

func setUpEmitterCell() {
  emitterCell.contents = UIImage(named: "smallStar")?.cgImage
  
  emitterCell.velocity = 50.0
  emitterCell.velocityRange = 500.0
  
  emitterCell.color = UIColor.black.cgColor
  emitterCell.redRange = 1.0
  emitterCell.greenRange = 1.0
  emitterCell.blueRange = 1.0
  emitterCell.alphaRange = 0.0
  emitterCell.redSpeed = 0.0
  emitterCell.greenSpeed = 0.0
  emitterCell.blueSpeed = 0.0
  emitterCell.alphaSpeed = -0.5
  
  let zeroDegreesInRadians = degreesToRadians(0.0)
  emitterCell.spin = degreesToRadians(130.0)
  emitterCell.spinRange = zeroDegreesInRadians
  emitterCell.emissionRange = degreesToRadians(360.0)
  
  emitterCell.lifetime = 1.0
  emitterCell.birthRate = 250.0
  emitterCell.xAcceleration = -800.0
  emitterCell.yAcceleration = 1000.0
}

这种方法有很多准备:

它通过将发射器单元的内容设置为图像(该图像在Layer Player项目中可用)来设置发射器单元。
然后指定初始速度和最大方差(速度范围);发射器层使用上述种子创建一个随机数生成器,该生成器将范围内的值随机化(初始值+/-范围值)。这种随机化适用于任何以范围结束的属性。
颜色设置为黑色,以允许差异(下面将讨论)从默认的白色变化,因为白色会导致颗粒过亮。
接下来设置一系列颜色范围,使用与velocityRange相同的随机化方法,这次指定每种颜色的方差范围。速度值指示在单元格的生命周期中每种颜色的变化有多快。
接下来,block 3指定如何在一个完整的圆锥周围分布单元格。更详细:设定发射器单元的旋转速度和发射范围。此外,发射范围决定了发射单元如何分布在一个由弧度中指定的发射范围定义的锥周围。
将单元格的生存期设置为1秒。这个属性的默认值是0,所以如果不显式地设置这个值,单元格将永远不会出现!出生率也是如此(每秒);默认值是0,因此必须将其设置为某个正数才能出现单元格。
最后,设置单元x、y加速度;这些数值影响粒子发射的视角。
接下来,有一些助手方法将角度转换为弧度,并将发射器单元的位置设置为视图的中点。

func setEmitterPosition() {
  emitterLayer.emitterPosition = CGPoint(x: view.bounds.midX, y: view.bounds.midY)
}

func degreesToRadians(_ degrees: Double) -> CGFloat {
  return CGFloat(degrees * Double.pi / 180.0)
}

然后设置发射器层和单元格,并将该单元格添加到该层,并将该层添加到视图层次结构。

override func viewDidLoad() {
  super.viewDidLoad()
  
  setUpEmitterLayer()
  setUpEmitterCell()
  emitterLayer.emitterCells = [emitterCell]
  view.layer.addSublayer(emitterLayer)
}

重写traitCollectionDidChange(_:):

override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
  setEmitterPosition()
}

该方法提供了一种处理当前trait集合更改的方法,例如当设备旋转时。

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

推荐阅读更多精彩内容