iOS Review | 你所不知道的10种Layer

1. CALayer

UIView和CALayer的关系
UIView和CALayer的关系
  • 一个View只能有一个Root Layer;
  • 一个Layer可以包含多个Sub Layer;
  • View只负责子元素的布局Layout和事件处理Events
  • Layer负责View上内容的绘制drawRect和显示,以及动画CAAnimation
CALayer Basic Properties
  • corner 圆角
let layer = view.layer
layer.masksToBounds = true // 必须为true
layer.cornerRadius = 10
  • border 边框
let layer = view.layer
layer.borderColor = UIColor.red.cgColor
layer.borderWidth = 10
  • shadow 投影
let layer = view.layer
layer.shadowColor = UIColor.green.cgColor
layer.shadowRadius = 10 // 半径
layer.shadowOpacity = 0.5 // 透明度
layer.shadowOffset = CGSize(width: 0, height: 10) // 偏移: x, y
  • contents 只能设置为图片CGImage / NSImage
let layer = view.layer
layer.contentsGravity = kCAGravityCenter
layer.contentsScale = UIScreen.main.scale
layer.contents = UIImage.init(named: "star")?.cgImage
  • shouldRasterize 栅格化

    • 默认是false,设置为true时,layer只被渲染一次(相当于一张静态图片);
    • 这适用于做一些与appearance无关的动画(如posistion, scale, rotate)等;
    • 可以很大程度提升性能(如果layer上内容比较复杂的话);
  • drawsAsynchronously 异步绘制

    • 和shouldRasterize的功能相反,为异步连续多次绘制;
    • 默认是false,为true时,如果layer必须得连续重新绘制时,可以提升性能(例如粒子发射layer);

2. CAScrollLayer

  • CAScrollLayer其实是一个比较简单功能的类,它只有一个有用的方法scroll(to p: CGPoint)scroll(to r: CGRect)
  • 只能部分地滚动,如果要实现全部的滚动,只能用UIScrollView;
  • 不过它可以结合UIImageView和UIPanGesture简单模拟UIScrollView;
  • 可以设置滚动模式为水平、垂直、二者、不可;
scrollingViewLayer.scrollMode = kCAScrollBoth
ScrollingView + UIImageView
  • 自定义Scrolling View
import QuartzCore

class ScrollingView: UIView {
  override class var layerClass : AnyClass {
    return CAScrollLayer.self
  }
}
  • 手势处理
    @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)
// 滚动到新点
        scrollingViewLayer.scroll(to: newPoint)
    }
CAScrollLayer

3. CATextLayer

  • CATextLayer是一个在指定Rect的Layer里快速绘制纯文本或富文本的layer类;
  • 它可以设置字体、字体大小、字体颜色、对齐方式、多行/单行显示模式、文本截断模式等,所有属性均可动画显示;
    let string = String.init(repeating: "这是测试CATextLayer的文字--", count: 10)

    textLayer.string = string // 可以是NSAttributedString
    textLayer.font = CTFontCreateWithName("Noteworthy-Light" as CFString, 0, nil)
    textLayer.fontSize = 18
    textLayer.foregroundColor = UIColor.red.cgColor // 字体颜色
    textLayer.isWrapped = true // 单行/多行
    textLayer.alignmentMode = kCAAlignmentLeft
    textLayer.truncationMode = kCATruncationEnd
    textLayer.contentsScale = UIScreen.main.scale
CATextLayer

4. AVPlayerLayer

  • AVPlayerLayer是AVFoundation库中的一个视频播放layer,用法较为简单;
  • 层级关系为AVPlayerLayer>AVPlayer>AVPlayerItem;
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

  // 播放完成后执行的操作--无、暂停、下一个`advance`(只适用于AVQueuePlayer)
  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)
}
AVPlayerLayer

5. CAGradientLayer

  • 顾名思义为渐变层,专门用来做渐变色的。
  • 用法很简单,设置一组colors、startPoint和endPoint就可以了,如果了解PS的话,相信很容易理解startPoint和endPoint;
  • 当然,它只支持线性渐变;
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)
  • 当然你也可以通过设置locations来控制每个颜色的渐变起始位置;
gradientLayer.locations: [NSNumber] = [:]
垂直渐变
水平渐变

6. CAReplicatorLayer

  • CAReplicatorLayer是将一个layer复制了instanceCount次,主要用来做一些动画;

  • 另外它可以设置复制间隔instanceDelay、和主要色instanceColor(针对subLayer起作用),以及复制层的颜色偏移(即过渡值),分别有instanceRedOffsetinstanceGreenOffsetinstanceBlueOffsetinstanceAlphaOffset属性;

  • 如果设置了instanceColor为whiteColor,即RGBA均为1,则instanceRedOffset设置范围为-1~0,对应颜色component的范围则是0~1

  • 最终的instanceColor即为RGBA三者offset值计算的混合色;

// 复制器层
replicatorLayer.backgroundColor = UIColor.clear.cgColor
replicatorLayer.instanceCount = 30
replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0)
replicatorLayer.preservesDepth = false
replicatorLayer.instanceColor = UIColor.red.cgColor
replicatorLayer.instanceRedOffset = 0
replicatorLayer.instanceGreenOffset = -1
replicatorLayer.instanceBlueOffset = -1
replicatorLayer.instanceAlphaOffset = -1 / Float(replicatorLayer.instanceCount)
replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat.pi * 2 / 30, 0, 0, 1)

// 实例层
let layerWidth: CGFloat = 10
let instanceLayer = CALayer()
instanceLayer.backgroundColor = UIColor.white.cgColor
let midX = replicatorLayer.bounds.midX - layerWidth / 2
instanceLayer.frame = CGRect(x: midX, y: 0, width: layerWidth, height: layerWidth * 3)
replicatorLayer.addSublayer(instanceLayer)
RGBA offset = 0, -1, -1, (-1 / Float(replicatorLayer.instanceCount))
// 动画
let fadeAnimation = CABasicAnimation.init(keyPath: "opacity")
fadeAnimation.fromValue = 1
fadeAnimation.toValue = 0
fadeAnimation.duration = 1
fadeAnimation.repeatCount = Float(Int.max)
instanceLayer.opacity = 0
instanceLayer.add(fadeAnimation, forKey: "FadeAnimation")
动画效果

7. CATiledLayer

  • 瓷砖层,也叫马赛克层,相信很多人不太了解。
  • 它必将实用的一个地方是可以异步绘制,这在处理需要很占内存的视图时很有好处,比如讲一张全景photo加载到scrollView上,非常耗内存,这是可以用CATiledLayer异步绘制只在当前屏幕区域内的小图。
  • 另外它还有两个提高绘制精度的属性levelsOfDetaillevelsOfDetailBias后者是抗锯齿的级别,数值越高显示效果越细腻,单位像素点越多;
  • 使用该类要自定义view基于UIView,重写drawRect方法来实现内容的绘制,但注意不能直接设置layer.contents;
levelsOfDetailBias = 1
levelsOfDetailBias = 5
class TiledBackgroundView: UIView {

    let sideLength: CGFloat = 50
    override class var layerClass: AnyClass{
        return CATiledLayer.self
    }
    
    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)
        context?.fill(rect)
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        
        let layer = self.layer as! CATiledLayer
        let scale = UIScreen.main.scale
        layer.contentsScale = scale
        layer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)
    }

}
image.png
  • 将大图切割成多个小图
extension UIImage {
  
  class func saveTileOfSize(_ size: CGSize, name: String) -> () {
    let cachesPath = NSSearchPathForDirectoriesInDomains(.cachesDirectory, .userDomainMask, true)[0] as String
    let filePath = "\(cachesPath)/\(name)_0_0.png"
    let fileManager = FileManager.default
    let fileExists = fileManager.fileExists(atPath: filePath)
        
    if fileExists == false {
      var tileSize = size
      let scale = Float(UIScreen.main.scale)
      
      if let image = UIImage(named: "\(name).jpg") {
        let imageRef = image.cgImage
        let totalColumns = Int(ceilf(Float(image.size.width / tileSize.width)) * scale)
        let totalRows = Int(ceilf(Float(image.size.height / tileSize.height)) * scale)
        let partialColumnWidth = Int(image.size.width.truncatingRemainder(dividingBy: tileSize.width))
        let partialRowHeight = Int(image.size.height.truncatingRemainder(dividingBy: tileSize.height))
        
        DispatchQueue.global(qos: .default).async {
          for y in 0..<totalRows {
            for x in 0..<totalColumns {
              if partialRowHeight > 0 && y + 1 == totalRows {
                tileSize.height = CGFloat(partialRowHeight)
              }
              
              if partialColumnWidth > 0 && x + 1 == totalColumns {
                tileSize.width = CGFloat(partialColumnWidth)
              }
              
              let xOffset = CGFloat(x) * tileSize.width
              let yOffset = CGFloat(y) * tileSize.height
              let point = CGPoint(x: xOffset, y: yOffset)
              
              if let tileImageRef = imageRef?.cropping(to: CGRect(origin: point, size: tileSize)), let imageData = UIImagePNGRepresentation(UIImage(cgImage: tileImageRef)) {
                let path = "\(cachesPath)/\(name)_\(x)_\(y).png"
                try? imageData.write(to: URL(fileURLWithPath: path), options: [])
              }
            }
          }
        }
      }
    }
  }
  
}
  • 异步绘制小图
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 image = imageForTile(atColumn: column, row: row) else { continue }
            let x = sideLength * CGFloat(column)
            let y = sideLength * CGFloat(row)
            let tileRect = CGRect.init(x: x, y: y, width: sideLength, height: sideLength)
            image.draw(in: tileRect)
        }
    }
}

func imageForTile(atColumn column: Int, row: Int) -> UIImage? {
    let filePath = "\(cachesPath)/\(fileName)_\(column)_\(row).png"
    return UIImage(contentsOfFile: filePath)
}
异步绘制分块图像

8. CAShapeLayer

  • 这个图形类相信大家都比较熟悉了,基于QuartzCore图形绘制库;
  • 使用方法也比较简单,建议使用PaintCode这个软件测试和学习;

9. CATransformLayer

  • 这是一个可以transform子层的抽象layer类,对它设置backgroundColor等都不会起作用,得addSublayer才能达到想要的效果;
  • 主要通过sublayerTransform属性来重新绘制subLayers,已达到3D变换的效果;
  • 它也没法接受events,只能通过检测对sublayer的event来处理touch等事件;
CATransformLayer

创建六面体

override func viewDidLoad() {
    super.viewDidLoad()
    
    // 前
    var layer = sideLayer(color: .red)
    transformLayer.addSublayer(layer)
    layer.transform = CATransform3DMakeTranslation(0.0, 0.0, sideLength / 2)
    
    // 后
    layer = sideLayer(color: .green)
    transformLayer.addSublayer(layer)
    layer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength / 2)
    
    // 上
    layer = sideLayer(color: .orange)
    transformLayer.addSublayer(layer)
    var transform = CATransform3DMakeTranslation(0.0, sideLength / 2, 0.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)
    layer.transform = transform
    
    // 下
    layer = sideLayer(color: .blue)
    transformLayer.addSublayer(layer)
    transform = CATransform3DMakeTranslation(0.0, -sideLength / 2, 0.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 1.0, 0.0, 0.0)
    layer.transform = transform
    
    // 左
    layer = sideLayer(color: .cyan)
    transformLayer.addSublayer(layer)
    transform = CATransform3DMakeTranslation(-sideLength / 2, 0.0, 0.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)
    layer.transform = transform
    
    // 右
    layer = sideLayer(color: .purple)
    transformLayer.addSublayer(layer)
    transform = CATransform3DMakeTranslation(sideLength / 2, 0.0, 0.0)
    transform = CATransform3DRotate(transform, degreesToRadians(90.0), 0.0, 1.0, 0.0)
    layer.transform = transform
    
    rotate(xOffset: 200, yOffset: 200)
}

func sideLayer(color: UIColor) -> CALayer {
    let layer = CALayer()
    layer.backgroundColor =
        color.withAlphaComponent(0.6).cgColor
    layer.frame = CGRect(origin: .zero, size: CGSize(width: sideLength, height: sideLength))
    layer.position = CGPoint(x: transformLayerView.bounds.midX, y: transformLayerView.bounds.midY)
    return layer
}

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

旋转变换

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 touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: transformLayerView) else {
        return
    }
    rotate(xOffset: Double(location.x / 50), yOffset: Double(location.y / 50))
}

hitTest

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

10. CAEmitterLayer

  • 顾名思义,CAEmitterLayer主要配合CAEmitterCell来发射粒子,比较高效;
  • 设置emitterCells属性可以在一个layer上添加多个cell发射器;
  • 可以设置emitterPosition设置发射器的位置;

setupEmitterLayer

func setupEmitterLayer() {
    emitterLayer.emitterCells = [emitterCell]
    emitterLayer.seed = UInt32(Date().timeIntervalSince1970)
    emitterLayer.renderMode = kCAEmitterLayerAdditive
    emitterLayer.drawsAsynchronously = true
    setEmitterPosition(CGPoint(x: view.bounds.midX, y: view.bounds.midY))
}

setupEmitterCell

func setupEmitterCell(){
    emitterCell.contents = UIImage.init(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
}

其他设置

func setEmitterPosition(_ position: CGPoint) {
    emitterLayer.emitterPosition = position
}

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

override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
    guard let location = touches.first?.location(in: view) else {
        return
    }
    setEmitterPosition(location)
}
CAEmitterLayer

以上就是全部的CALayer相关的知识了,工程文件见https://github.com/BackWorld/LayerPlayer

如果对你有帮助,别忘了点个👍或关注下哦~

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

推荐阅读更多精彩内容