CALayer教程

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


如你所知,在iOS應(yīng)用里看到的所有東西都是一個(gè)視圖.有按鈕視圖,標(biāo)視圖,滑動(dòng)視圖,甚至容器視圖.
但你有所可能不知的是每個(gè)視圖后面都有一個(gè)CALayer在支持.
這篇文章講解了CALayer的原理,并且展示十個(gè)CALayer效果的示例.
本文基于讀者熟悉iOS基礎(chǔ)開(kāi)發(fā)知識(shí)和Swift,包括故事板(storyboards)的使用.

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

The iOS Apprentice.

CALayer如何關(guān)聯(lián)UIView

UIview處理試圖布局和觸摸事件,但是不直接處理繪圖和動(dòng)畫(huà),UIKIt把這些事交給CoreAnimation處理.UIView實(shí)際上只是CALayer的封裝.當(dāng)你設(shè)置一個(gè)UIView的bounds的時(shí)候,實(shí)際上只是簡(jiǎn)單地設(shè)置背后的CALayer的bounds.如果你調(diào)用UIView的layoutIfNeeded方法,這個(gè)調(diào)用會(huì)向前傳遞到根CALayer上去.每個(gè)UIView都有一個(gè)根CALayer.


起步

觀察CALayer的反應(yīng)是了解他們的最快方法.所以我們從一個(gè)最簡(jiǎn)單地項(xiàng)目開(kāi)始.下載這個(gè)只有一個(gè)視圖在屏幕中央的項(xiàng)目

用下面的代碼替換ViewController.swift里面的內(nèi)容:

  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) {
    
  }
  
}

之前提到每個(gè)視圖都有一個(gè)layer相關(guān)聯(lián),可以通過(guò).layer獲取這個(gè)layer.這段代碼第一件事是創(chuàng)建一個(gè)訪問(wèn)viewForLayer的layer的屬性,名叫l(wèi)ayer.

代碼還調(diào)用setUpLayer()來(lái)設(shè)置layer的一些屬性:陰影,藍(lán)色背景.和一圈很粗的紅色邊界.待會(huì)會(huì)講解setUpLayer(),但讓我們首先跑一下這個(gè)項(xiàng)目看一下效果.

因?yàn)槊總€(gè)視圖都有l(wèi)ayer,所以你可以在任何的視圖上做這些效果.

基本的CALayer屬性

CALayer有一些可以讓你自定義的屬性.想一想我們之前做的:

  • 把默認(rèn)沒(méi)有背景色改成藍(lán)色
  • 把邊界的寬度從0變成100
  • 把邊界的顏色黑色改成紅色
  • 把陰影的可見(jiàn)度從0變成0.7,然后修改陰影的半徑從3改成10.

這些只是部分可設(shè)置的屬性.我們?cè)賮?lái)試兩個(gè),同樣在setUpLayer()里:

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

contents屬性可以讓你設(shè)置layer的內(nèi)容為一張圖片.這里我們?cè)O(shè)置了一張"star"的圖片.這張圖已經(jīng)添加到項(xiàng)目里了.再來(lái)運(yùn)行看一下:



注意到星星是如何居中的,這是因?yàn)槲覀冊(cè)O(shè)置了kCAGravityCenter.當(dāng)然你也可以居上,居下等等.

改變layer的外觀

項(xiàng)目已經(jīng)包含的點(diǎn)擊縮放手勢(shì).
把tapGestureRecognized(_:)變成這樣:

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

這樣在點(diǎn)擊的時(shí)候,layer的陰影可見(jiàn)度會(huì)在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
  }
}

這里根據(jù)用戶的縮放,調(diào)整偏移和layer的大小,邊界大小,圓角大小.
默認(rèn)的圓角值是0,也就是個(gè)矩形.增加圓角值可以讓角變圓.給一個(gè)正方形設(shè)置寬度一半的圓角值可以變成圓形.
但是調(diào)整圓角值并不會(huì)裁剪layer的內(nèi)容,除非把masksToBounds屬性設(shè)置為true.
運(yùn)行一下:


CALayer之旅

CALayer不單單只有這些屬性和方法可以使用.而且還有好多子類提供更多的屬性和方法.
文章接下來(lái)的內(nèi)容需要以下東西:

  • Layer播放器源代碼
  • Layer播放器App (可選)
    這是個(gè)很順手,展示了10個(gè)不同CALayer的app:

    讀下面內(nèi)容之前可以先玩一下這個(gè)app,讀一下源代碼.放心,下面的代碼不需要你全部寫(xiě)一遍.

示例1: CALayers

之前你已經(jīng)使用過(guò)CALayer的一些屬性.
但是下面這些還未提及過(guò):

  • Layers可以擁有子Layers
  • Layers屬性是有動(dòng)畫(huà)的.設(shè)置Layers屬性的時(shí)候,它是會(huì)隨著默認(rèn)時(shí)間變化的,當(dāng)然也可以修改時(shí)間.
  • Layers是輕量化的. Layers比view更輕量,所以它可以提供更好的性能.
  • Layers擁有大量有用的屬性.

帶你看一下CALayer所有的屬性-有一些還沒(méi)見(jiàn)過(guò),但是很有用.

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

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

創(chuàng)建一個(gè)CALayer實(shí)例,設(shè)置他的frame為someView的frame.設(shè)置內(nèi)容為一張圖片在中央.

layer.magnificationFilter = kCAFilterLinear
layer.isGeometryFlipped = false

設(shè)置放大模式.

前面的變化不會(huì)有動(dòng)畫(huà)效果,如果不把isGeometryFlipped設(shè)置成true,坐標(biāo)系是不一致的.繼續(xù):

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

背景色設(shè)為綠色.同時(shí),不要裁剪內(nèi)容,如果圖片比layer大,圖片不會(huì)被裁剪掉.

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

通過(guò)設(shè)置圓角為寬度的一半,創(chuàng)建一個(gè)視覺(jué)上的圓形.注意顏色是CGColor.

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

創(chuàng)建陰影,設(shè)置shouldRasterize為true.添加到視圖層上.
結(jié)果:


CALayer有2個(gè)可以提高性能的屬性:
shouldRasterize 和 drawsAsynchronously.
shouldRasterize在默認(rèn)情況下是false,當(dāng)設(shè)置為true時(shí),它可以提高性能,因?yàn)閷拥膬?nèi)容只需要呈現(xiàn)一次。它非常適合在屏幕上動(dòng)畫(huà)但外觀不變的對(duì)象。

drawsAsynchronously 是shouldRasterize的反義詞。默認(rèn)情況下也是false。將它設(shè)置為true,以便在必須重復(fù)重繪圖層內(nèi)容時(shí)提高性能,例如在處理連續(xù)呈現(xiàn)動(dòng)畫(huà)粒子的發(fā)射器圖層時(shí)。(稍后請(qǐng)參閱CAEmitterLayer示例)。

在異步設(shè)置shouldrastze或drawsasynchronze之前,請(qǐng)考慮這些影響。比較true和false之間的性能,這樣您就知道激活這些屬性是否真的提高了性能。當(dāng)使用不當(dāng)時(shí),性能很可能急劇下降。
現(xiàn)在把你的注意力暫時(shí)轉(zhuǎn)移到Layer播放器上。它包括操縱許多CALayer屬性的控件:



試一下的控制-這是一個(gè)很棒的方法來(lái)獲得你可以用CALayer做什么感覺(jué)!
Layers不是響應(yīng)鏈的一部分,所以它們不會(huì)像視圖那樣直接響應(yīng)觸摸或手勢(shì),就像您在CALayerPlayground示例中看到的那樣。
但是,您可以測(cè)試它們,您將在CATransformLayer的示例代碼中看到。您還可以向圖層添加自定義動(dòng)畫(huà),您將在CAReplicatorLayer中看到這些。

示例2: Layers

CAScrollLayer顯示可滾動(dòng)層的一部分。它非常的基礎(chǔ),不能直接響應(yīng)用戶的觸摸,甚至不能檢查可滾動(dòng)層的邊界,但它做了一些很酷的事情,比如防止?jié)L動(dòng)超出邊界無(wú)限!

UIScrollView不使用CAScrollLayer來(lái)做它的工作,而是直接改變它的層的界限。

你可以用CAScrollLayer來(lái)設(shè)置它的滾動(dòng)模式為水平或垂直,并通過(guò)編程告訴它滾動(dòng)到一個(gè)特定的點(diǎn)或區(qū)域:

// 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)
    })
  }
}

以上代碼:

  • 一個(gè)屬性,用于返回scrollingView的底層CAScrollLayer。
  • 滾動(dòng)最初設(shè)置為水平和垂直。
  • 當(dāng)一個(gè)平移被識(shí)別時(shí),一個(gè)新的點(diǎn)被創(chuàng)建,滾動(dòng)層在UIView動(dòng)畫(huà)中滾動(dòng)到那個(gè)點(diǎn)。注意,scroll(to:)不會(huì)自動(dòng)動(dòng)畫(huà)。

Layer播放器演示了一個(gè)CAScrollLayer,它包含一個(gè)圖像視圖,其中的圖像大于滾動(dòng)視圖的邊界。當(dāng)您運(yùn)行上述代碼并平移視圖時(shí),結(jié)果如下:

層播放器包括兩個(gè)控件來(lái)鎖定水平和垂直滾動(dòng)。

下面是一些使用(或不使用)CAScrollLayer的經(jīng)驗(yàn)法則:

如果您想要輕量級(jí)的東西,并且只需要通過(guò)編程滾動(dòng),可以考慮使用CAScrollLayer。
如果希望用戶能夠滾動(dòng),最好使用UIScrollView。要了解更多信息,請(qǐng)查看我們關(guān)于此的18部分視頻教程系列。
如果您正在滾動(dòng)一個(gè)非常大的圖像,請(qǐng)考慮使用CATiledLayer(更多信息見(jiàn)下文)。播放器包括兩個(gè)控件來(lái)鎖定水平和垂直滾動(dòng)。

下面是一些使用(或不使用)CAScrollLayer的經(jīng)驗(yàn)法則:

  • 如果您想要輕量級(jí)的東西,并且只需要通過(guò)編程滾動(dòng),可以考慮使用CAScrollLayer。
  • 如果希望用戶能夠滾動(dòng),最好使用UIScrollView。要了解更多信息,請(qǐng)查看我們關(guān)于此的18部分視頻教程系列。
  • 如果您正在滾動(dòng)一個(gè)非常大的圖像,請(qǐng)考慮使用CATiledLayer(更多信息見(jiàn)下文)。

示例3: CATextLayer

CATextLayer提供了簡(jiǎn)單但快速的純文本或帶屬性字符串呈現(xiàn)。與UILabel不同,CATextLayer不能有指定的UIFont,只能有CTFontRef或CGFontRef。

使用這樣的代碼塊,可以操作字體、字體大小、顏色、對(duì)齊、換行和截?cái)啵琇ayer Player包含兩個(gè)控件,用于鎖定水平和垂直滾動(dòng),以及動(dòng)畫(huà)更改:

// 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)

以上代碼說(shuō)明:

  • 創(chuàng)建一個(gè)CATextLayer實(shí)例,并將其設(shè)置為someView的邊界。
  • 創(chuàng)建一個(gè)重復(fù)文本字符串,并將其分配給文本層。
  • 創(chuàng)建字體并將其分配給文本layer。
  • 將文本layer設(shè)置為換行和左對(duì)齊(您可以選擇將其設(shè)置為自然、右、中、對(duì)齊),并將其contentsScale匹配到屏幕上,然后將該layer添加到視圖層次結(jié)構(gòu)中。

所有的layer類,不僅僅是CATextLayer,在默認(rèn)情況下呈現(xiàn)的比例系數(shù)為1。當(dāng)附加到視圖時(shí),layer自動(dòng)將它們的contentsScale設(shè)置為當(dāng)前屏幕的適當(dāng)比例因子。你需要為你手動(dòng)創(chuàng)建的圖層顯式設(shè)置contentsScale,否則它們的比例系數(shù)將是1,你會(huì)在視網(wǎng)膜顯示屏上有像素化。

如果添加到一個(gè)方形的UIView中,創(chuàng)建的文本層將如下所示:



截?cái)嗍且环N可以使用的設(shè)置,當(dāng)您希望用省略號(hào)表示截?cái)嗟奈谋緯r(shí),這種設(shè)置非常好。截?cái)嗄J(rèn)為none,可以設(shè)置為start、end和middle:


播放器有控制改變?cè)S多CATextLayer的屬性:

layer

示例4: AVPlayerLayer
AVPlayerLayer為AVFoundation添加了一個(gè)layer。它持有一個(gè)AVPlayer播放AV媒體文件(AVPlayerItems)。下面是一個(gè)創(chuàng)建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)
}

上述代碼的分項(xiàng):

創(chuàng)建一個(gè)新的播放器layer并設(shè)置它的frame。
創(chuàng)建具有AV asset的播放器。
告訴玩家在游戲結(jié)束后什么也不要做;其他選項(xiàng)包括暫?;蛲七M(jìn)到下一個(gè)asset,如果適用。
當(dāng)AVPlayer完成對(duì)asset的操作時(shí)注冊(cè)通知(并刪除控制器作為deinit中的觀察者)。

接下來(lái),當(dāng)點(diǎn)擊play按鈕時(shí),它切換控件來(lái)播放AV asset并設(shè)置按鈕的標(biāo)題。

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

然后當(dāng)播放器到達(dá)結(jié)束時(shí),將播放光標(biāo)移動(dòng)到開(kāi)始位置。

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

注意,這只是一個(gè)簡(jiǎn)單的示例。在真實(shí)的項(xiàng)目中,通常不建議將焦點(diǎn)放在按鈕的標(biāo)題文本上。

上面創(chuàng)建的AVPlayerLayer及其AVPlayer將由AVPlayerItem實(shí)例的第一幀可視化表示,如下所示:

AVPlayerLayer有幾個(gè)額外的屬性:

  • videoGravity 設(shè)置視頻顯示的縮放行為。
  • isReadyForDisplay 檢查視頻是否準(zhǔn)備好顯示。
    另一方面,AVPlayer有一些額外的屬性和方法。需要注意的一點(diǎn)是速率,速率是從0到1的回放速率。0表示暫停,1表示視頻按正常速度播放(1x)。

然而,設(shè)置速率也指示回放以該速率開(kāi)始。換句話說(shuō),調(diào)用pause()和設(shè)置速率為0與調(diào)用play()和設(shè)置速率為1做相同的事情。

那么快進(jìn)、慢動(dòng)作或者倒立播放呢?AVPlayer都幫你搞定了。將速率設(shè)置為任何高于1的值,就相當(dāng)于要求播放器以正常速度的數(shù)倍開(kāi)始播放,例如,將速率設(shè)置為2意味著雙倍速度。

正如您可能設(shè)想的那樣,將速率設(shè)置為負(fù)數(shù)將指示回放以該數(shù)字乘以正常速度的倒數(shù)開(kāi)始。

然而,在以常規(guī)速度(向前)以外的任何速度播放之前,AVPlayerItem上要檢查適當(dāng)?shù)淖兞?,以?yàn)證它可以以該速度播放:

  • canPlayFastForward 任何大于1的數(shù)
  • canPlaySlowForward 對(duì)0到1之間的任何數(shù)字進(jìn)行慢進(jìn),但不包括1
  • canPlayReverse -1
  • canPlaySlowReverse 對(duì)-1到(但不包括)0之間的任何數(shù)字進(jìn)行慢速反轉(zhuǎn)
  • canPlayFastReverse 任何小于-1的數(shù)字
    大多數(shù)視頻都可以以不同的前進(jìn)速度播放,但倒著播放就不那么典型了。層播放器還包括回放控制:


示例5: CAGradientLayer

CAGradientLayer可以輕松地將兩種或兩種以上的顏色混合在一起,使其特別適合于背景。為了配置它,您需要分配一個(gè)CGColors數(shù)組,以及一個(gè)起始點(diǎn)和一個(gè)端點(diǎn)來(lái)指定漸變層的起始點(diǎn)和結(jié)束點(diǎn)。

記住,起始點(diǎn)和終點(diǎn)不是顯式點(diǎn)。相反,它們是在單位坐標(biāo)空間中定義的,然后在繪制時(shí)映射到層的邊界。換句話說(shuō),x值為1表示該點(diǎn)在層的右邊緣,y值為1表示該點(diǎn)在層的底邊緣。

CAGradientLayer有一個(gè)類型屬性,盡管kCAGradientLayerAxial是唯一的選項(xiàng),它通過(guò)數(shù)組中的每個(gè)顏色線性地過(guò)渡。

這意味著,如果你在起始點(diǎn)和終點(diǎn)之間畫(huà)一條直線(a),漸變會(huì)沿著一條與a垂直的假想線(B)進(jìn)行,而沿著B(niǎo)的所有點(diǎn)都是相同的顏色:

或者,您可以使用一個(gè)值在0到1之間的數(shù)組來(lái)控制location屬性,該數(shù)組指定漸變層應(yīng)該在顏色數(shù)組中使用下一個(gè)顏色的相對(duì)位置。

如果未指定停止位置,則停止位置默認(rèn)為均勻間隔。但是,如果設(shè)置了位置,它的計(jì)數(shù)必須與顏色計(jì)數(shù)匹配,否則將發(fā)生不希望發(fā)生的事情:[

下面是一個(gè)如何創(chuàng)建漸變層的例子:

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)

在上面的代碼中,您創(chuàng)建了一個(gè)漸變layer,將其frame與someView的邊界一樣,分配一個(gè)顏色數(shù)組,設(shè)置起點(diǎn)和終點(diǎn),并將漸變層添加到視圖層次結(jié)構(gòu)中。它看起來(lái)是這樣的:


如此豐富多彩!接下來(lái),你將編寫(xiě)一個(gè)從應(yīng)用程序中飛來(lái)的蝴蝶來(lái)逗你的鼻子。

layer播放器提供你的控制改變開(kāi)始和結(jié)束點(diǎn),顏色和地點(diǎn):


示例6: CAReplicatorLayer

CAReplicatorLayer復(fù)制一個(gè)圖層指定次數(shù),這允許您創(chuàng)建一些很酷的效果。

每一層復(fù)制都可以有自己的顏色和位置變化,它的繪制可以延遲,給復(fù)制器的整個(gè)層一個(gè)動(dòng)畫(huà)效果。深度也可以保持給復(fù)制層一個(gè)3D效果。這里有一個(gè)例子:

首先,創(chuàng)建一個(gè)CAReplicatorLayer的實(shí)例,并將它的frame設(shè)置為someView的bounds。

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

接下來(lái),設(shè)置replicator層的副本數(shù)量(instanceCount)和繪制延遲。還要將replicator層設(shè)置為2D (preservesDepth = false),并將其實(shí)例顏色設(shè)置為白色。

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

然后,將紅/綠/藍(lán)偏移量添加到每個(gè)連續(xù)復(fù)制實(shí)例的顏色值。

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

每個(gè)默認(rèn)值為0,這有效地在所有實(shí)例中保留顏色值。但是,在本例中,實(shí)例顏色最初設(shè)置為白色,這意味著紅、綠和藍(lán)已經(jīng)是1.0了。因此,將紅色設(shè)置為0,將綠色和藍(lán)色偏移量設(shè)置為負(fù)數(shù),就可以讓紅色成為突出的顏色。類似地,將alpha偏移量添加到每個(gè)連續(xù)復(fù)制實(shí)例的alpha中。

然后,創(chuàng)建一個(gè)轉(zhuǎn)換,使每個(gè)后續(xù)實(shí)例圍繞一個(gè)圓旋轉(zhuǎn)。

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創(chuàng)建一個(gè)要使用的實(shí)例layer
,并設(shè)置它的frame,以便第一個(gè)實(shí)例將在x中心和someView邊界的頂部繪制。另外,設(shè)置實(shí)例的顏色,并將實(shí)例層添加到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)

現(xiàn)在,制作一個(gè)漸變動(dòng)畫(huà),使不透明度從1(opaque)變?yōu)?(transparent)。

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

最后,將實(shí)例layer的不透明度設(shè)置為0,以便在繪制每個(gè)實(shí)例并設(shè)置其顏色和alpha值之前保持透明。

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

效果:



layer播放器可以控制大多數(shù)這些屬性:


示例7: CATiledLayer

CATiledLayer在tile中異步繪制層內(nèi)容。對(duì)于非常大的圖片或其他內(nèi)容集來(lái)說(shuō),這是非常好的,因?yàn)榭梢钥吹絻?nèi)容,而不必將其全部加載到內(nèi)存中。

有幾種處理繪圖的方法。一種是覆蓋UIView,使用CATiledLayer重復(fù)繪制tile來(lái)填充view的背景,就像這樣:

視圖控制器顯示一個(gè)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)
  }
  
}

以下是在上述代碼中發(fā)生的事情:

  • layerClass被重寫(xiě),因此該視圖的alyer被創(chuàng)建為CATiledLayer的實(shí)例。
  • 在draw(_:)中生成隨機(jī)顏色的rand48()函數(shù)的種子。然后縮放該layer的內(nèi)容(轉(zhuǎn)換為CATiledLayer),以匹配屏幕的縮放和其平鋪大小設(shè)置。
  • 重寫(xiě)draw(_:),用隨機(jī)顏色的平鋪層填充視圖。
    最后,上面的代碼繪制了一個(gè)由隨機(jī)著色的正方形瓷磚組成的6x6網(wǎng)格,如下所示:


Layer 播放器通過(guò)在平鋪層背景上繪制路徑來(lái)擴(kuò)展這種用法:


CATiledLayer 細(xì)節(jié)

當(dāng)你放大視圖時(shí),上面截圖中的星星變得模糊:


這種模糊是由圖層保持的細(xì)節(jié)層次造成的。CATiledLayer有兩個(gè)屬性,levelsOfDetail和levelsOfDetailBias。

levelsOfDetail,顧名思義,是由層維護(hù)的詳細(xì)級(jí)別的數(shù)量。它的默認(rèn)值是1,每一增量級(jí)別的緩存分辨率都是前一級(jí)別的一半。一個(gè)圖層的最大層次細(xì)節(jié)值是它最下面的層次至少有一個(gè)像素。

另一方面,levelsOfDetailBias是這個(gè)層緩存的放大級(jí)別的細(xì)節(jié)數(shù)量。它的默認(rèn)值是0,這意味著不會(huì)緩存額外的放大級(jí)別,而且每個(gè)增量級(jí)別的緩存速度都是前一級(jí)別的兩倍。

例如,將上面模糊的平鋪層的levelsOfDetailBias增加到5會(huì)導(dǎo)致緩存級(jí)別放大到2x、4x、8x、16x和32x,放大后的圖層會(huì)是這樣的:


CATiledLayer 異步繪制

CATiledLayer還有另一個(gè)有用的用途:異步繪制非常大的圖像的圖像塊,例如,在滾動(dòng)視圖中。

您必須提供tiles和邏輯來(lái)告訴tiles layer在用戶滾動(dòng)時(shí)應(yīng)該抓取哪個(gè)tiles,但是這里的性能提升是顯著的。

它的工作是將源圖像分割成指定大小的正方形塊,根據(jù)每個(gè)塊的列和行位置命名;例如,windingRoad_6_2。第7列第3行平鋪的png(零索引):


有了這些貼圖,可以創(chuàng)建一個(gè)自定義UIView子類來(lái)繪制這些貼圖層:

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邊的長(zhǎng)度、基本映像文件名和TileCutter擴(kuò)展保存tiles的緩存目錄的路徑創(chuàng)建屬性。
覆蓋層類以返回CATiledLayer。
在視圖層中實(shí)現(xiàn)init(coder:),將其轉(zhuǎn)換為平鋪層并設(shè)置其平鋪大小。請(qǐng)注意,沒(méi)有必要將contentsScale與屏幕比例匹配,因?yàn)槟苯邮褂靡晥D的支持層。
下一步,重寫(xiě)draw(_:),根據(jù)每個(gè)tile的列和行位置繪制每個(gè)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添加到滾動(dòng)視圖中。

這樣,你就可以流暢地滾動(dòng)一個(gè)大圖像(在layer播放器中是5120 x 3200),這要?dú)w功于CATiledLayer:


正如你可以在上面的動(dòng)畫(huà)中看到的,當(dāng)繪制單個(gè)的塊時(shí),快速滾動(dòng)會(huì)出現(xiàn)明顯的阻塞。通過(guò)使用較小的tiles(上面例子中使用的tiles被切為640 x 640)和創(chuàng)建一個(gè)定制的CATiledLayer子類并覆蓋fadeDuration()來(lái)返回0,從而最小化這種行為:

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

示例8: CAShapeLayer
CAShapeLayer使用可縮放的矢量路徑繪制,比使用圖像快得多。這里的另一個(gè)好處是,您不再需要提供常規(guī)的@2x和@3x大小的圖像。w00t !

此外,您還可以使用各種屬性來(lái)定制線條粗細(xì)、顏色、虛線、線條如何連接其他線條,以及是否應(yīng)該填充該區(qū)域以及使用什么顏色等等。這里有一個(gè)例子:

首先,創(chuàng)建顏色、路徑和形狀層。

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()

接下來(lái),繪制形狀層的路徑。您可以使用move(to:)或addLine(to:)等方法從一個(gè)點(diǎn)繪制到另一個(gè)點(diǎn)。

  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()
  }

如果寫(xiě)這種樣板圖紙代碼不是你的愛(ài)好,檢查PaintCode
;它通過(guò)允許您使用直觀的可視化控件繪圖或?qū)氍F(xiàn)有矢量(SVG)或Photoshop (PSD)文件來(lái)生成代碼。

然后,建立形狀層:

  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
  }

將其路徑設(shè)置為上面繪制的路徑,其填充顏色設(shè)置為步驟1中創(chuàng)建的顏色,并將填充規(guī)則顯式設(shè)置為非零的默認(rèn)值。

唯一的另一個(gè)選項(xiàng)是偶數(shù)-奇數(shù),對(duì)于這個(gè)沒(méi)有相交路徑的形狀,填充規(guī)則沒(méi)有什么區(qū)別。
非零規(guī)則將從左到右的路徑記為+1,從右到左的路徑記為-1;它將路徑的所有值相加,如果總數(shù)大于0,它將填充路徑形成的形狀。
本質(zhì)上,非零填充了形狀內(nèi)的所有點(diǎn)。
偶數(shù)-奇數(shù)規(guī)則計(jì)算形成形狀的路徑交叉點(diǎn)的總數(shù),如果計(jì)數(shù)為奇數(shù),則填充該形狀。這絕對(duì)是一個(gè)圖片勝過(guò)千言萬(wàn)語(yǔ)的例子。
形成五邊形的奇偶圖中的路徑交叉點(diǎn)的數(shù)量是偶數(shù),所以五邊形沒(méi)有被填滿,而形成每個(gè)三角形的路徑交叉點(diǎn)的數(shù)量是奇數(shù),所以三角形被填滿。



最后,調(diào)用路徑繪制和層設(shè)置代碼,然后將層添加到視圖層次結(jié)構(gòu)中。

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

}

效果:


如果你想知道這幅畫(huà)在PaintCode中是什么樣子的,請(qǐng)點(diǎn)擊PaintCode
:


layer播放器包括控制操縱許多CAShapeLayer的屬性:

你可能會(huì)注意到,我們跳過(guò)了Layer Player應(yīng)用程序的下一個(gè)演示。這是因?yàn)镃AEAGLLayer實(shí)際上已經(jīng)被CAMetalLayer淘汰了,后者與Metal框架一起在iOS 8中首次亮相。你可以在這里
找到一個(gè)關(guān)于CAMetalLayer的教程。

示例9: CATransformLayer

CATransformLayer不像其他層類那樣扁平它的子層層次結(jié)構(gòu),因此它便于繪制3D結(jié)構(gòu)。它實(shí)際上是它的子層的容器,每個(gè)子層都可以有自己的變換和不透明度變化,但是,它忽略了對(duì)其他呈現(xiàn)層屬性的更改,比如邊框?qū)挾群皖伾?/p>

您不能直接點(diǎn)擊測(cè)試轉(zhuǎn)換層,因?yàn)樗鼪](méi)有一個(gè)二維坐標(biāo)空間來(lái)映射一個(gè)接觸點(diǎn),然而,它可以點(diǎn)擊測(cè)試單個(gè)子層。這里有一個(gè)例子:

首先為邊長(zhǎng)、立方體每邊的顏色和轉(zhuǎn)換層創(chuàng)建屬性。

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()

創(chuàng)建一些輔助代碼,以創(chuàng)建具有指定顏色的立方體的每個(gè)邊層,并將度數(shù)轉(zhuǎn)換為弧度。為什么弧度?因?yàn)槲矣X(jué)得用角度比用弧度更直觀.

  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)
  }

然后通過(guò)創(chuàng)建、旋轉(zhuǎn)和向轉(zhuǎn)換層添加每一邊來(lái)構(gòu)建多維數(shù)據(jù)集。然后設(shè)置轉(zhuǎn)換層的z軸錨點(diǎn),旋轉(zhuǎn)立方體并將立方體添加到視圖層次結(jié)構(gòu)中。

  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)
  }

接下來(lái)編寫(xiě)一個(gè)函數(shù),該函數(shù)應(yīng)用基于指定的x和y偏移量的旋轉(zhuǎn)。注意,代碼將轉(zhuǎn)換設(shè)置為subblayertransform,這適用于轉(zhuǎn)換層的子層。

  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
  }

然后觀察觸摸并循環(huán)通過(guò)轉(zhuǎn)換層的子層。對(duì)每一層進(jìn)行命中測(cè)試,并在檢測(cè)到命中后立即跳出,因?yàn)槊袦y(cè)試剩余的層沒(méi)有任何好處。

  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
    }
  }

最后,設(shè)置轉(zhuǎn)換層并將其添加到視圖層次結(jié)構(gòu)中。

那么這些current transform .m##s有什么用呢?我很高興你這樣問(wèn):。這些是CATransform3D屬性,表示由行和列組成的矩形數(shù)組組成的矩陣的元素。
要了解更多與本例中使用的矩陣轉(zhuǎn)換類似的信息,請(qǐng)查看教程3DTransformFun project,Enter The Matrix project

運(yùn)行上面的代碼,someView是一個(gè)250 x 250的視圖,結(jié)果是:


現(xiàn)在,嘗試一些事情:點(diǎn)擊立方體上的任何地方,“Transform layer tapped!”將打印到控制臺(tái)。


示例10: CAEmitterLayer

CAEmitterLayer渲染作為CAEmitterCell實(shí)例的動(dòng)畫(huà)粒子。CAEmitterLayer和CAEmitterCell都具有更改呈現(xiàn)速率、大小、形狀、顏色、速度、壽命等屬性。這里有一個(gè)例子:

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()
  }
}

以上代碼準(zhǔn)備emitterLayer:

創(chuàng)建emitterLayer和cell。
通過(guò)以下步驟設(shè)置emitterLayer:
為層的隨機(jī)數(shù)生成器提供一個(gè)種子,該生成器依次對(duì)層的發(fā)射器單元的某些屬性(如速度)進(jìn)行隨機(jī)化。下面將進(jìn)一步解釋這一點(diǎn)。
以renderMode指定的順序?qū)l(fā)射器單元格渲染到圖層背景顏色和邊框之上。
將繪制異步設(shè)置為true,這可能會(huì)提高性能,因?yàn)榘l(fā)射器層必須不斷地重新繪制其發(fā)射器單元。
接下來(lái),通過(guò)助手方法設(shè)置發(fā)射器的位置。這是一個(gè)很好的案例研究如何設(shè)置繪圖異步為真對(duì)動(dòng)畫(huà)的性能和流暢性有積極的影響。
最后,解釋ViewController中設(shè)置CAEmitterCell缺少的方法:

接下來(lái),設(shè)置發(fā)射器單元:

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
}

這種方法有很多準(zhǔn)備:

它通過(guò)將發(fā)射器單元的內(nèi)容設(shè)置為圖像(該圖像在Layer Player項(xiàng)目中可用)來(lái)設(shè)置發(fā)射器單元。
然后指定初始速度和最大方差(速度范圍);發(fā)射器層使用上述種子創(chuàng)建一個(gè)隨機(jī)數(shù)生成器,該生成器將范圍內(nèi)的值隨機(jī)化(初始值+/-范圍值)。這種隨機(jī)化適用于任何以范圍結(jié)束的屬性。
顏色設(shè)置為黑色,以允許差異(下面將討論)從默認(rèn)的白色變化,因?yàn)榘咨珪?huì)導(dǎo)致顆粒過(guò)亮。
接下來(lái)設(shè)置一系列顏色范圍,使用與velocityRange相同的隨機(jī)化方法,這次指定每種顏色的方差范圍。速度值指示在單元格的生命周期中每種顏色的變化有多快。
接下來(lái),block 3指定如何在一個(gè)完整的圓錐周圍分布單元格。更詳細(xì):設(shè)定發(fā)射器單元的旋轉(zhuǎn)速度和發(fā)射范圍。此外,發(fā)射范圍決定了發(fā)射單元如何分布在一個(gè)由弧度中指定的發(fā)射范圍定義的錐周圍。
將單元格的生存期設(shè)置為1秒。這個(gè)屬性的默認(rèn)值是0,所以如果不顯式地設(shè)置這個(gè)值,單元格將永遠(yuǎn)不會(huì)出現(xiàn)!出生率也是如此(每秒);默認(rèn)值是0,因此必須將其設(shè)置為某個(gè)正數(shù)才能出現(xiàn)單元格。
最后,設(shè)置單元x、y加速度;這些數(shù)值影響粒子發(fā)射的視角。
接下來(lái),有一些助手方法將角度轉(zhuǎn)換為弧度,并將發(fā)射器單元的位置設(shè)置為視圖的中點(diǎn)。

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)
}

然后設(shè)置發(fā)射器層和單元格,并將該單元格添加到該層,并將該層添加到視圖層次結(jié)構(gòu)。

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

重寫(xiě)traitCollectionDidChange(_:):

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

該方法提供了一種處理當(dāng)前trait集合更改的方法,例如當(dāng)設(shè)備旋轉(zhuǎn)時(shí)。

以下是運(yùn)行上述代碼的結(jié)果:
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容