如你所知,咱們在iOS應用中看到的都是視圖(view),包括按鈕視圖、表視圖、滑動條視圖,還有能夠容納其餘視圖的父視圖等。swift
AD:【活動】Web和APP兼容性實戰 Win10訓練營免費報名 數組
如你所知,咱們在iOS應用中看到的都是視圖(view),包括按鈕視圖、表視圖、滑動條視圖,還有能夠容納其餘視圖的父視圖等。緩存
但你或許不知道在iOS中支撐起每一個視圖的是一個叫作"圖層(layer)"的類,確切地說是CALayer。app
本文中您會了解CALayer及其工做原理,還有應用CALayer打造酷炫效果的十則示例,好比繪製矢量圖形、漸變色,甚至是粒子系統。框架
本文要求讀者熟悉iOS應用開發和Swift語言的基礎知識,包括利用Storyboard構建用戶界面。異步
注:若是您還沒有掌握這些基礎,沒必要擔憂,咱們有很多相關教程,例如使用Swift語言編寫iOS應用和iOS學徒。ide
準備開始函數
要理解圖層是什麼,最簡便的方式就是"實地考察"。咱們這就建立一個簡單的項目,從頭開始玩轉圖層。工具
準備好寫代碼了嗎?好!啓動Xcode,而後:佈局
1.選擇File\New\Project菜單項。
2.在對話框中選擇iOS\Application\Single View Application。
3.點擊Next,Product Name填寫CALayerPlayground,而後輸入你本身的Organization Name和Identifier。
4.Language選Swift,Devices選Universal。
5.取消選擇Core Data,點擊Next。
6.把項目保存到合適的位置(我的習慣把項目放在用戶目錄下創建的Source文件夾),點擊Create。
好,文件準備就緒,接下來就是建立視圖了:
7.在項目導航欄(Project navigator)中選擇Main.storyboard。
8.選擇View\Assistant Editor\Show Assistant Editor菜單項,若是沒有顯示對象庫(Object Library),請選擇View\Utilities\Show Object Library。
9.而後選擇Editor\Canvas\Show Bounds Rectangles,這樣在向場景添加視圖時就能夠看到輪廓了。
10.把一個視圖(View)從對象庫拖入視圖控制器場景,保持選中狀態,在尺寸檢查器(View\Utilities\Show Size Inspector)中將x和y設爲150,Width和Height設爲300。
11.視圖保持選中,點擊自動佈局工具欄(Storyboard右下角)的Align按鈕,選中Horizontal Center in Container和Vertical Center in Container,數值均爲0,而後點擊Add 2 Constraints。
12.點擊Pin按鈕,選中Width和Height,數值均設爲300,點擊Add 2 Constraints。
最後按住control從剛剛建立的視圖拖到ViewController.swift文件中viewDidLoad()方法的上方,在彈框中將outlet命名爲viewForLayer,如圖:
點擊Connect建立outlet。
將ViewController.swift中的代碼改寫爲:
- import UIKit
-
- class ViewController: UIViewController {
-
- @IBOutlet weak var viewForLayer: UIView!
-
- var l: CALayer {
- return viewForLayer.layer
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- setUpLayer()
- }
-
- func setUpLayer() {
- l.backgroundColor = UIColor.blueColor().CGColor
- l.borderWidth = 100.0
- l.borderColor = UIColor.redColor().CGColor
- l.shadowOpacity = 0.7
- l.shadowRadius = 10.0
- }
-
- }
以前提到iOS中的每一個視圖都擁有一個關聯的圖層,你能夠經過yourView.layer訪問圖層。這段代碼首先建立了一個叫"l"(小寫L)的計算屬性,方便訪問viewForLayer的圖層,可以讓你少寫一些代碼。
這段代碼還調用了setUpLayer方法設置圖層屬性:陰影,藍色背景,紅色粗邊框。你立刻就能夠了解這些東西,不過如今仍是先構建App,在iOS模擬器中運行(我選了iPhone 6),看看自定義的圖層如何。
幾行代碼,效果還不錯吧?仍是那句話,每一個視圖都由圖層支撐,因此你也能夠對App中的任何視圖作出相似修改。咱們繼續深刻。
CALayer基本屬性
CALayer有幾個屬性能夠用來自定外觀,想一想剛纔作的:
把圖層背景色從默認的無色改成藍色
經過把邊框寬度從默認的0改成100來添加邊框
把邊框顏色從默認的黑色改成紅色
最後把陰影透明度從0(全透明)改成0.7,產生陰影效果,此外還把陰影半徑從默認的3改成10。
以上只是CALayer中能夠設置的部分屬性。咱們再試兩個,在setUpLayer()中追加如下代碼:
- l.contents = UIImage(named: "star")?.CGImage
- l.contentsGravity = kCAGravityCenter
CALayer的contents屬性能夠把圖層的內容設爲圖片,這裏咱們要設置一張"星星"的圖片,爲此你須要把圖片添加到項目中,請下載圖片並添加到項目中。
構建,運行,欣賞一下效果:
注意星星居中,這是由於contentsGravity屬性被設爲kCAGravityCenter,如你所想,重心也能夠設爲上、右上、右、右下、下、左下、左、左上。
更改圖層外觀
僅供娛樂,咱們來添加幾個手勢識別器來控制圖層外觀。在Xcode中,向viewForLayer對象上拖一個輕觸手勢識別器(tap gesture recognizer),見下圖:
注:若是你對手勢識別器比較陌生,請參閱Using UIGestureRecognizer with Swift。
以此類推,再添加一個捏合手勢識別器(pinch gesture recognizer)。
而後按住control依次將兩個手勢識別器從Storyboard場景停靠欄拖入ViewController.swift,放在setUpLayer()和類自身的閉合花括號之間。
在彈框中修改鏈接爲Action,命名輕觸識別操做爲tapGestureRecognized,捏合識別操做爲pinchGestureRecognized,例如:
以下改寫tapGestureRecognized(_:):
- @IBAction func tapGestureRecognized(sender: UITapGestureRecognizer) {
- l.shadowOpacity = l.shadowOpacity == 0.7 ? 0.0 : 0.7
- }
當令視圖識別出輕觸手勢時,代碼告知viewForLayer圖層在0.7和0之間切換陰影透明度。
你說視圖?嗯,沒錯,重寫CALayer的hitTest(_:)也能夠實現相同效果,本文後面也會看到這個方法,不過咱們這裏用的方法也有道理:圖層自己並不能響應手勢識別,只能響應點擊測試,因此咱們在視圖上設置了輕觸手勢識別器。
而後以下修改pinchGestureRecognized(_:):
- @IBAction func pinchGestureRecognized(sender: UIPinchGestureRecognizer) {
- let offset: CGFloat = sender.scale < 1 ? 5.0 : -5.0
- let oldFrame = l.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 {
- l.borderWidth -= offset
- l.cornerRadius += (offset / 2.0)
- l.frame = newFrame
- }
- }
此處基於用戶的捏合手勢建立正負偏移值,藉此調整圖層框架大小、邊緣寬度和邊角半徑。
圖層的邊角半徑默認值爲0,意即標準的90度直角。增大半徑會產生圓角,若是想將圖層變成圓形,能夠設邊角半徑爲寬度的一半。
注意:調整邊角半徑並不會裁剪圖層內容(星星圖片),除非圖層的masksToBounds屬性被設爲true。
構建運行,嘗試在視圖中使用輕觸和捏合手勢:
嘿,再好好裝扮一下都能當頭像用了! :]
CALayer體驗
CALayer中的屬性和方法琳琅滿目,此外還有幾個包含特有屬性和方法的子類。
要遍歷如此酷炫的API,Raywenderlich.com導遊先生最好不過了。
接下來,你須要如下材料:
Layer Player App
Layer Player 源代碼
該App包含十種不一樣的CALayer示例,本文後面會依次介紹,十分方便。先來吊吊你們的胃口:
下面在講解每一個示例的同時,我建議在CALayer演示應用中親自動手試驗,還能夠讀讀代碼。不用寫,只要深呼吸,輕鬆閱讀就能夠了。 :]
我相信這些酷炫的示例會啓發您利用不一樣的CALayer爲本身的App錦上添花,但願你們喜歡!
示例 #1:CALayer
前面咱們看過使用CALayer的示例,也就是設置各類屬性。
關於CALayer還有幾點沒提:
圖層能夠包含子圖層。就像視圖能夠包含子視圖,圖層也能夠有子圖層,稍加利用就能打造漂亮的效果!
圖層屬性自帶動畫效果。修改圖層屬性時,存在默認的動畫效果,你也能夠自定義動畫行爲。
圖層是輕量概念。相對視圖而言,圖層更加輕量,所以圖層能夠幫助提高性能。
圖層有大量實用屬性。前面你已經看過幾條了,咱們繼續探索!
剛剛說CALayer圖層有不少屬性,咱們來看一批實用屬性:有些屬性你可能第一次見,但真的很方便!
- let layer = CALayer()
- layer.frame = someView.bounds
-
- layer.contents = UIImage(named: "star")?.CGImage
- layer.contentsGravity = kCAGravityCenter
-
- layer.magnificationFilter = kCAFilterLinear
- layer.geometryFlipped = false
-
- layer.backgroundColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0).CGColor
- layer.opacity = 1.0
- layer.hidden = false
- layer.masksToBounds = false
-
- layer.cornerRadius = 100.0
- layer.borderWidth = 12.0
- layer.borderColor = UIColor.whiteColor().CGColor
-
- layer.shadowOpacity = 0.75
- layer.shadowOffset = CGSize(width: 0, height: 3)
- layer.shadowRadius = 3.0
- someView.layer.addSublayer(layer)
在以上代碼中:
建立一個CALayer實例,並把框架設爲someView邊框。
將圖層內容設爲一張圖片,並使其在圖層內居中,注意賦值的類型是底層的Quartz圖像數據(CGImage)。
使用過濾器,過濾器在圖像利用contentsGravity放大時發揮做用,可用於改變大小(縮放、比例縮放、填充比例縮放)和位置(中心、上、 右上、右等等)。以上屬性的改變沒有動畫效果,另外若是geometryFlipped未設爲true,幾何位置和陰影會上下顛倒。繼續:
把背景色設爲Ray最愛的深綠色。:] 而後讓圖層透明、可見。同時令圖層不要遮罩內容,意思是若是圖層尺寸小於內容(星星圖片),圖像不會被裁減。
圖層邊角半徑設爲圖層寬度的一半,使邊緣變爲圓形,注意圖層顏色賦值類型爲Quartz顏色引用(CGColor)。
建立陰影,設shouldRasterize爲true(後文還會提到),而後將圖層加入視圖結構樹。
結果以下:
CALayer還有兩個附加屬性有助於改善性能:shouldRasterize和drawsAsynchronously。
shouldRasterize默認爲false,設爲true能夠改善性能,由於圖層內容只須要一次渲染。相對畫面中移動但自身外觀不變的對象效果拔羣。
drawsAsynchronously默認值也是false。與shouldRasterize相對,該屬性適用於圖層內容須要反覆重繪的狀況, 此時設成true可能會改善性能,好比須要反覆繪製大量粒子的粒子發射器圖層(能夠參考後面的CAEmitterLayer示例)。
謹記:若是想將已有圖層的shouldRasterize或drawsAsynchronously屬性設爲true,必定要三思然後行,考慮可能形成的影響,對比true與false的性能差別,辨明屬性設置是否有積極效果。設置不當甚至會致使性能大幅降低。
不管如何仍是先回到圖層演示應用,其中有些控件能夠用來調整CALayer的屬性:
調節試試看,感覺一下,利用CALayer能夠實現怎樣的效果。
注:圖層不屬於響應鏈(responder chain),沒法像視圖同樣直接響應觸摸和手勢,咱們在CALayerPlayground中見識過。不過圖層有點擊測試,後面的 CATransformLayer會提到。你也能夠向圖層添加自定義動畫,CAReplicatorLayer中會出現。
示例 #2:CAScrollLayer
CAScrollLayer顯示一部分可滾動圖層,該圖層十分基礎,沒法直接響應用戶的觸摸操做,也不能直接檢查可滾動圖層的邊界,故可避免越界無限滾動。
UIScrollView用的不是CAScrollLayer,而是直接改動圖層邊界。
CAScrollLayer的滾動模式可設爲水平、垂直或者二維,你也能夠用代碼命令視圖滾動到指定位置:
- import UIKit
-
- class ScrollingView: UIView {
- override class func layerClass() -> AnyClass {
- return CAScrollLayer.self
- }
- }
-
- import UIKit
-
- class CAScrollLayerViewController: UIViewController {
- @IBOutlet weak var scrollingView: ScrollingView!
-
- var scrollingViewLayer: CAScrollLayer {
- return scrollingView.layer as CAScrollLayer
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
- scrollingViewLayer.scrollMode = kCAScrollBoth
- }
-
- @IBAction func tapRecognized(sender: UITapGestureRecognizer) {
- var newPoint = CGPoint(x: 250, y: 250)
- UIView.animateWithDuration(0.3, delay: 0, options: .CurveEaseInOut, animations: {
- [unowned self] in
- self.scrollingViewLayer.scrollToPoint(newPoint)
- }, completion: nil)
- }
-
- }
以上代碼:
定義一個繼承UIView的類,重寫layerClass()返回CAScrollLayer,該方法等同於建立一個新圖層做爲子圖層(CALayer示例中作過)。
一個用以方便簡化訪問自定義視圖滾動圖層的計算屬性。
設滾動模式爲二維滾動。
識別出輕觸手勢時,讓滾動圖層在UIView動畫中滾到新建的點。(注:scrollToPoint(_:)和scrollToRect(_:)不會自動使用動畫效果。)
案例研究:若是ScrollingView實例包含大於滾動視圖邊界的圖片視圖,在運行上述代碼並點擊視圖時結果以下:
圖層演示應用中有能夠鎖定滾動方向(水平或垂直)的開關。
如下經驗規律用於決定是否使用CAScrollLayer:
若是想使用輕量級的對象,只需用代碼操做滾動:能夠考慮CAScrollLayer。
若是想讓用戶操做滾動,UIScrollView大概是更好的選擇。要了解更多,請參考咱們的視頻教程。
若是是滾動大型圖片:考慮使用CATiledLayer(見後文)。
示例 #3:CATextLayer
CATextLayer可以對普通文本或屬性字串進行簡單快速的渲染。與UILabel不一樣,CATextLayer沒法指定UIFont,只能使用CTFontRef或CGFontRef。
像下面這樣的代碼徹底能夠掌控文本的字體、字體大小、顏色、對齊、折行(wrap)和截斷(truncation)規則,也有動畫效果:
- let textLayer = CATextLayer()
- textLayer.frame = someView.bounds
-
- var string = ""
- for _ in 1...20 {
- string += "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Fusce auctor arcu quis velit congue dictum. "
- }
-
- textLayer.string = string
-
- let fontName: CFStringRef = "Noteworthy-Light"
- textLayer.font = CTFontCreateWithName(fontName, fontSize, nil)
-
- textLayer.foregroundColor = UIColor.darkGrayColor().CGColor
- textLayer.wrapped = true
- textLayer.alignmentMode = kCAAlignmentLeft
- textLayer.contentsScale = UIScreen.mainScreen().scale
- someView.layer.addSublayer(textLayer)
以上代碼解釋以下:
建立一個CATextLayer實例,令邊界與someView相同。
重複一段文本,建立字符串並賦給文本圖層。
建立一個字體,賦給文本圖層。
將文本圖層設爲折行、左對齊,你也能夠設天然對齊(natural)、右對齊(right)、居中對齊(center)或兩端對齊(justified),按屏幕設置contentsScale屬性,而後把圖層添加到視圖結構樹。
不只是CATextLayer,全部圖層類的渲染縮放係數都默認爲1。在添加到視圖時,圖層自身的contentsScale縮放係數會自動調整, 適應當前畫面。你須要爲手動建立的圖層明確指定contentsScale屬性,不然默認的縮放係數1會在Retina顯示屏上產生部分模糊。
若是建立的文本圖層添加到了方形的someView,效果會像這樣:
你能夠設置截斷(Truncation)屬性,生效時被截斷的部分文本會由省略號代替顯示。默認設定爲無截斷,位置可設爲開頭、末尾或中間截斷:
圖層演示應用中,你能夠爲所欲爲地修改不少CATextLayer屬性:
示例 #4:AVPlayerLayer
AVPlayerLayer是創建在AVFoundation基礎上的實用圖層,持有一個AVPlayer,用來播放音視頻媒體文件(AVPlayerItems),舉例以下:
- override func viewDidLoad() {
- super.viewDidLoad()
- let playerLayer = AVPlayerLayer()
- playerLayer.frame = someView.bounds
-
- let url = NSBundle.mainBundle().URLForResource("someVideo", withExtension: "m4v")
- let player = AVPlayer(URL: url)
-
- player.actionAtItemEnd = .None
- playerLayer.player = player
- someView.layer.addSublayer(playerLayer)
-
- NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidReachEndNotificationHandler:", name: "AVPlayerItemDidPlayToEndTimeNotification", object: player.currentItem)
- }
-
- deinit {
- NSNotificationCenter.defaultCenter().removeObserver(self)
- }
-
- @IBAction func playButtonTapped(sender: UIButton) {
- if playButton.titleLabel?.text == "Play" {
- player.play()
- playButton.setTitle("Pause", forState: .Normal)
- } else {
- player.pause()
- playButton.setTitle("Play", forState: .Normal)
- }
-
- updatePlayButtonTitle()
- updateRateSegmentedControl()
- }
-
- func playerDidReachEndNotificationHandler(notification: NSNotification) {
- let playerItem = notification.object as AVPlayerItem
- playerItem.seekToTime(kCMTimeZero)
- }
上述代碼解釋:
新建一個播放器圖層,設置框架。
使用AV asset資源建立一個播放器。
告知命令播放器在播放完成後中止。其餘選項還有暫停或自動播放下一個媒體資源。
註冊AVPlayer通知,在一個文件播放完畢後發送通知,並在析構函數中刪除做爲觀察者的控制器。
點擊播放按鈕時,觸發控件播放AV asset並設置按鈕文字。
注意這只是個入門示例,在實際項目中每每不會採用文字按鈕控制播放。
AVPlayerLayer和其中建立的AVPlayer會像這樣顯示爲AVPlayerItem實例的第一幀:
AVPlayerLayer還有一些附加屬性:
videoGravity設置視頻顯示的縮放行爲。
readyForDisplay檢測是否準備好播放視頻。
另外一方面,AVPlayer也有很多附加屬性和方法,有一個值得注意的是rate屬性,對於0到1之間的播放速率,0表明暫停,1表明常速播放(1x)。
不過rate屬性的設置是與播放行爲聯動的,也就是說調用pause()方法和把rate設爲0是等價的,調用play()與把rate設爲1也同樣。
那快進、慢動做和反向播放呢?交給AVPlayerLayer把。rate大於1時會令播放器以相應倍速進行播放,例如rate設爲2就是二倍速。
如你所想,rate爲負時會讓播放器以相應倍速反向播放。
然而,在以很是規速率播放以前,AVPlayerItem上會調用適當方法,驗證是否可以以相應速率進行播放:
canPlayFastForward()對應大於1
canPlaySlowForward()對應0到1之間
canPlayReverse()對應-1
canPlaySlowReverse()對應-1到0之間
canPlayFastReverse()對應小於-1
絕大多數視頻都支持以不一樣速率正向播放,能夠反向播放的視頻相對少一些。演示應用也包含了播放控件:
示例 #5:CAGradientLayer
CAGradientLayer簡化了混合兩種或更多顏色的工做,尤爲適用於背景。要配置漸變色,你須要分配一個CGColor數組,以及標識漸變圖層起止點的startPoint和endPoint。
注意:startPoint和endPoint並非明確的點,而是用單位座標空間定義,在繪製時映射到圖層邊界。也就是說x值爲1表示點在圖層右邊緣,y值爲1表示點在圖層下邊緣。
CAGradientLayer包含type屬性,雖然說該屬性只有kCAGradientLayerAxial一個選擇,由數組中的各顏色產生線性過渡漸變。
具體含義是漸變過渡沿startPoint到endPoint的向量A方向產生,設B與A垂直,則各條B平行線上的全部點顏色相同。
此外,locations屬性可使用一個數組(元素取值範圍0到1),指定漸變圖層參照colors順序取用下一個過渡點顏色的位置。
未設定時默認會平均分配過渡點。一旦設定就必須與colors的數量保持一致,不然會出錯。 :[
下面是建立漸變圖層的例子:
- let gradientLayer = CAGradientLayer()
- gradientLayer.frame = someView.bounds
- gradientLayer.colors = [cgColorForRed(209.0, green: 0.0, blue: 0.0),
- cgColorForRed(255.0, green: 102.0, blue: 34.0),
- cgColorForRed(255.0, green: 218.0, blue: 33.0),
- cgColorForRed(51.0, green: 221.0, blue: 0.0),
- cgColorForRed(17.0, green: 51.0, blue: 204.0),
- cgColorForRed(34.0, green: 0.0, blue: 102.0),
- cgColorForRed(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)
func cgColorForRed(red: CGFloat, green: CGFloat, blue: CGFloat) -> AnyObject {
return UIColor(red: red/255.0, green: green/255.0, blue: blue/255.0, alpha: 1.0).CGColor as AnyObject
}
上述代碼建立一個漸變圖層,框架設爲someView邊界,指定顏色數組,設置起止點,添加圖層到視圖結構樹。效果以下:
五彩繽紛,奼紫嫣紅!
圖層演示應用中,你能夠隨意修改起止點、顏色和過渡點:
示例 #6:CAReplicatorLayer
CAReplicatorLayer可以以特定次數複製圖層,能夠用來建立一些很棒的效果。
每一個圖層復件的顏色和位置均可以改動,並且能夠在總複製圖層以後延遲繪製,營造一種動畫效果。還能夠利用深度,創造三維效果。舉個例子
- let replicatorLayer = CAReplicatorLayer()
- replicatorLayer.frame = someView.bounds
-
- replicatorLayer.instanceCount = 30
- replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0)
- replicatorLayer.preservesDepth = false
- replicatorLayer.instanceColor = UIColor.whiteColor().CGColor
-
- replicatorLayer.instanceRedOffset = 0.0
- replicatorLayer.instanceGreenOffset = -0.5
- replicatorLayer.instanceBlueOffset = -0.5
- replicatorLayer.instanceAlphaOffset = 0.0
-
- let angle = Float(M_PI * 2.0) / 30
- replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0)
- someView.layer.addSublayer(replicatorLayer)
-
- let instanceLayer = CALayer()
- let layerWidth: CGFloat = 10.0
- let midX = CGRectGetMidX(someView.bounds) - layerWidth / 2.0
- instanceLayer.frame = CGRect(x: midX, y: 0.0, width: layerWidth, height: layerWidth * 3.0)
- instanceLayer.backgroundColor = UIColor.whiteColor().CGColor
- replicatorLayer.addSublayer(instanceLayer)
-
- let fadeAnimation = CABasicAnimation(keyPath: "opacity")
- fadeAnimation.fromValue = 1.0
- fadeAnimation.toValue = 0.0
- fadeAnimation.duration = 1
- fadeAnimation.repeatCount = Float(Int.max)
-
- instanceLayer.opacity = 0.0
- instanceLayer.addAnimation(fadeAnimation, forKey: "FadeAnimation")
以上代碼:
建立一個CAReplicatorLayer實例,設框架爲someView邊界。
設複製圖層數instanceCount和繪製延遲,設圖層爲2D(preservesDepth = false),實例顏色爲白色。
爲陸續的實例復件設置RGB顏色誤差值(默認爲0,即全部復件保持顏色不變),不過這裏實例初始顏色爲白色,即RGB都爲1.0,因此誤差值設紅色爲0,綠色和藍色爲相同負數會使其逐漸現出紅色,alpha透明度誤差值的變化也與此相似,針對陸續的實例復件。
建立旋轉變換,使得實例復件按一個圓排列。
建立供複製圖層使用的實例圖層,設置框架,使第一個實例在someView邊界頂端水平中心處繪製,另外設置實例顏色,把實例圖層添加到複製圖層。
建立一個透明度由1(不透明)過渡爲0(透明)的淡出動畫。
設實例圖層透明度爲0,使得每一個實例在繪製和改變顏色與alpha前保持透明。
這段代碼會實現這樣的東西:
圖層演示應用中,你能夠改動這些屬性:
示例 #7:CATiledLayer
CATiledLayer以圖塊(tile)爲單位異步繪製圖層內容,對超大尺寸圖片或者只能在視圖中顯示一小部分的內容效果拔羣,由於不用把內容徹底載入內存就能夠看到內容。
處理繪製有幾種方法,一種是重寫UIView,使用CATiledLayer繪製圖塊填充視圖背景,以下:
- import UIKit
-
- class ViewController: UIViewController {
-
- @IBOutlet weak var tiledBackgroundView: TiledBackgroundView!
-
- }
-
- import UIKit
-
- class TiledBackgroundView: UIView {
-
- let sideLength = CGFloat(50.0)
-
- override class func layerClass() -> AnyClass {
- return CATiledLayer.self
- }
-
- required init(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- srand48(Int(NSDate().timeIntervalSince1970))
- let layer = self.layer as CATiledLayer
- let scale = UIScreen.mainScreen().scale
- layer.contentsScale = scale
- layer.tileSize = CGSize(width: sideLength * scale, height: sideLength * scale)
- }
-
- override func drawRect(rect: CGRect) {
- let context = UIGraphicsGetCurrentContext()
- var red = CGFloat(drand48())
- var green = CGFloat(drand48())
- var blue = CGFloat(drand48())
- CGContextSetRGBFillColor(context, red, green, blue, 1.0)
- CGContextFillRect(context, rect)
- }
-
- }
代碼解釋:
tiledBackgroundView位於 (150, 150) ,寬高均爲300。
重寫layerClass(),令該視圖建立的圖層實例爲CATiledLayer。
設置rand48()的隨機數種子,用於在drawRect()中生成隨機顏色。CATiledLayer類型轉換,縮放圖層內容,設置圖塊尺寸,適應屏幕。
重寫drawRect(),以隨機色塊填充視圖。
代碼繪製6×6隨機色塊方格,最終效果以下:
圖層演示應用中除此以外還能夠在圖層背景上繪製軌跡:
在視圖中放大時,上述截圖中的星星圖案會變得模糊:
產生模糊的根源是圖層的細節層次(level of detail,簡稱LOD),CATiledLayer有兩個相關屬性:levelsOfDetail和levelsOfDetailBias。
levelsOfDetail顧名思義,指圖層維護的LOD數目,默認值爲1,每進一級會對前一級分辨率的一半進行緩存,圖層的levelsOfDetail最大值,也就是最底層細節,對應至少一個像素點。
而levelsOfDetailBias指的是該圖層緩存的放大LOD數目,默認爲0,即不會額外緩存放大層次,每進一級會對前一級兩倍分辨率進行緩存。
例如,設上述分塊圖層的levelsOfDetailBias爲5會緩存2x、4x、8x、16x和32x的放大層次,放大的圖層效果以下:
不錯吧?彆着急,還沒講完呢。
CATiledLayer裁刀,買不了吃虧,買不了上當,只要998…(譯註:此處內容稍做本地化處理,原文玩的是1978年美國Ginsu刀具的梗,堪稱詢價型電視購物廣告的萬惡之源。) :]
開個玩笑。CATiledLayer還有一個更實用的功能:異步繪製圖塊,好比在滾動視圖中顯示一張超大圖片。
在用戶滾動畫面時,要讓分塊圖層知道哪些圖塊須要繪製,寫代碼在所不免,不過換來性能提高也值了。
圖層演示應用的UIImage+TileCutter.swift中包含一個UIImage擴展,教程編纂組成員Nick Lockwood在著做iOS Core Animation: Advanced Techniques的一個終端應用程序中利用了這段代碼。
代碼的職責是把原圖片拆分紅指定尺寸的方塊,按行列位置命名圖塊,好比第三行第七列的圖塊windingRoad62.png(索引從零開始)。
有了這些圖塊,咱們能夠自定義一個UIView子類,繪製分塊圖層:
- mport UIKit
-
- class TilingViewForImage: UIView {
-
- let sideLength = CGFloat(640.0)
- let fileName = "windingRoad"
- let cachesPath = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String
-
- override class func layerClass() -> AnyClass {
- return CATiledLayer.self
- }
-
- required init(coder aDecoder: NSCoder) {
- super.init(coder: aDecoder)
- let layer = self.layer as CATiledLayer
- layer.tileSize = CGSize(width: sideLength, height: sideLength)
- }
-
- override func drawRect(rect: CGRect) {
- let firstColumn = Int(CGRectGetMinX(rect) / sideLength)
- let lastColumn = Int(CGRectGetMaxX(rect) / sideLength)
- let firstRow = Int(CGRectGetMinY(rect) / sideLength)
- let lastRow = Int(CGRectGetMaxY(rect) / sideLength)
-
- for row in firstRow...lastRow {
- for column in firstColumn...lastColumn {
- if let tile = imageForTileAtColumn(column, row: row) {
- 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 = CGRectIntersection(bounds, tileRect)
- tile.drawInRect(tileRect)
- }
- }
- }
- }
-
- func imageForTileAtColumn(column: Int, row: Int) -> UIImage? {
- let filePath = "\(cachesPath)/\(fileName)_\(column)_\(row)"
- return UIImage(contentsOfFile: filePath)
- }
-
- }
以上代碼:
建立屬性,分別是圖塊邊長、原圖文件名、供TileCutter擴展保存圖塊的緩存文件夾路徑。
重寫layerClass()返回CATiledLayer。
實現init(_:),把視圖的圖層轉換爲分塊圖層,設置圖塊大小。注意此處沒必要設置contentsScale適配屏幕,由於是直接修改視圖自身的圖層,而不是手動建立子圖層。
重寫drawRect(),按行列繪製各個圖塊。
像這樣,原圖大小的自定義視圖就能夠塞進一個滾動視圖:
多虧CATiledLayer,滾動5120 x 3200的大圖也會這般順滑:
如你所見,快速滾動時繪製圖塊的過程仍是很明顯,你能夠利用更小的分塊(上述例子中分塊爲640 x 640),或者本身建立一個CATiledLayer子類,重寫fadeDuration()返回0:
- class TiledLayer: CATiledLayer {
-
- override class func fadeDuration() -> CFTimeInterval {
- return 0.0
- }
-
- }
示例 #8:CAShapeLayer
CAShapeLayer利用可縮放的矢量路徑進行繪製,繪製速度比使用圖片快不少,還有個好處是不用分別提供常規、@2x和@3x版本的圖片,好用。
另外還有各類屬性,讓你能夠自定線粗、顏色、虛實、線條接合方式、閉合線條是否造成閉合區域,還有閉合區域要填充何種顏色等。舉例以下
- 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()
-
-
- func setUpRWPath() {
- rwPath.moveToPoint(CGPointMake(0.22, 124.79))
- rwPath.addLineToPoint(CGPointMake(0.22, 249.57))
- rwPath.addLineToPoint(CGPointMake(124.89, 249.57))
- rwPath.addLineToPoint(CGPointMake(249.57, 249.57))
- rwPath.addLineToPoint(CGPointMake(249.57, 143.79))
- rwPath.addCurveToPoint(CGPointMake(249.37, 38.25), controlPoint1: CGPointMake(249.57, 85.64), controlPoint2: CGPointMake(249.47, 38.15))
- rwPath.addCurveToPoint(CGPointMake(206.47, 112.47), controlPoint1: CGPointMake(249.27, 38.35), controlPoint2: CGPointMake(229.94, 71.76))
- rwPath.addCurveToPoint(CGPointMake(163.46, 186.84), controlPoint1: CGPointMake(182.99, 153.19), controlPoint2: CGPointMake(163.61, 186.65))
- rwPath.addCurveToPoint(CGPointMake(146.17, 156.99), controlPoint1: CGPointMake(163.27, 187.03), controlPoint2: CGPointMake(155.48, 173.59))
- rwPath.addCurveToPoint(CGPointMake(128.79, 127.08), controlPoint1: CGPointMake(136.82, 140.43), controlPoint2: CGPointMake(129.03, 126.94))
- rwPath.addCurveToPoint(CGPointMake(109.31, 157.77), controlPoint1: CGPointMake(128.59, 127.18), controlPoint2: CGPointMake(119.83, 141.01))
- rwPath.addCurveToPoint(CGPointMake(89.83, 187.86), controlPoint1: CGPointMake(98.79, 174.52), controlPoint2: CGPointMake(90.02, 188.06))
- rwPath.addCurveToPoint(CGPointMake(56.52, 108.28), controlPoint1: CGPointMake(89.24, 187.23), controlPoint2: CGPointMake(56.56, 109.11))
- rwPath.addCurveToPoint(CGPointMake(64.02, 102.25), controlPoint1: CGPointMake(56.47, 107.75), controlPoint2: CGPointMake(59.24, 105.56))
- rwPath.addCurveToPoint(CGPointMake(101.42, 67.57), controlPoint1: CGPointMake(81.99, 89.78), controlPoint2: CGPointMake(93.92, 78.72))
- rwPath.addCurveToPoint(CGPointMake(108.38, 30.65), controlPoint1: CGPointMake(110.28, 54.47), controlPoint2: CGPointMake(113.01, 39.96))
- rwPath.addCurveToPoint(CGPointMake(10.35, 0.41), controlPoint1: CGPointMake(99.66, 13.17), controlPoint2: CGPointMake(64.11, 2.16))
- rwPath.addLineToPoint(CGPointMake(0.22, 0.07))
- rwPath.addLineToPoint(CGPointMake(0.22, 124.79))
- rwPath.closePath()
- }
-
-
- 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
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
-
- setUpRWPath()
- setUpRWLayer()
- someView.layer.addSublayer(rwLayer)
- }
-
- }
代碼解釋:
建立顏色、路徑、圖形圖層對象。
繪製圖形圖層路徑。若是不喜歡編寫生硬的繪圖代碼的話,你能夠嘗試PaintCode這款軟件,能夠利用簡便的工具進行可視化繪製,支持導入現有的矢量圖(SVG)和Photoshop(PSD)文件,並自動生成代碼。
設置圖形圖層。路徑設爲第二步中繪製的CGPath路徑,填充色設爲第一步中建立的CGColor顏色,填充規則設爲非零(non-zero),即默認填充規則。
填充規則共有兩種,另外一種是奇偶(even-odd)。不過示例代碼中的圖形沒有相交路徑,兩種填充規則的結果並沒有差別。
非零規則記從左到右的路徑爲+1,從右到左的路徑爲-1,累加全部路徑值,若總和大於零,則填充路徑圍成的圖形。
從結果上來說,非零規則會填充圖形內部全部的點。
奇偶規則計算圍成圖形的路徑交叉數,若結果爲奇數則填充。這樣講有些晦澀,仍是有圖有真相:
右圖圍成中間五邊形的路徑交叉數爲偶數,故中間沒有填充,而圍成每一個三角的路徑交叉數爲奇數,故三角部分填充顏色。
調用路徑繪製和圖層設置代碼,並把圖層添加到視圖結構樹。
上述代碼繪製raywenderlich.com的圖標:
順便看看使用PaintCode的效果圖:
圖層演示應用中,你能夠隨意修改不少CAShapeLayer屬性:
注:咱們先跳過演示應用中的下一個示例,由於CAEAGLLayer多少顯得有些過期了,iOS 8 Metal框架有更先進的CAMetalLayer。在此推薦iOS 8 Metal入門教程。
示例 #9:CATransformLayer
CATransformLayer不像其餘圖層類同樣把子圖層結構平面化,故適宜繪製3D結構。變換圖層本質上是一個圖層容器,每一個子圖層均可以應用本身的透明度和空間變換,而其餘渲染圖層屬性(如邊寬、顏色)會被忽略。
變換圖層自己不支持點擊測試,由於沒法直接在觸摸點和平面座標空間創建映射,不過其中的子圖層能夠響應點擊測試,例如:
- import UIKit
-
- class ViewController: UIViewController {
-
- @IBOutlet weak var someView: UIView!
-
- let sideLength = CGFloat(160.0)
- var redColor = UIColor.redColor()
- var orangeColor = UIColor.orangeColor()
- var yellowColor = UIColor.yellowColor()
- var greenColor = UIColor.greenColor()
- var blueColor = UIColor.blueColor()
- var purpleColor = UIColor.purpleColor()
- var transformLayer = CATransformLayer()
-
- func setUpTransformLayer() {
- var layer = sideLayerWithColor(redColor)
- transformLayer.addSublayer(layer)
-
- layer = sideLayerWithColor(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 = sideLayerWithColor(yellowColor)
- layer.transform = CATransform3DMakeTranslation(0.0, 0.0, -sideLength)
- transformLayer.addSublayer(layer)
-
- layer = sideLayerWithColor(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 = sideLayerWithColor(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 = sideLayerWithColor(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
- applyRotationForXOffset(16.0, yOffset: 16.0)
- }
-
- func sideLayerWithColor(color: UIColor) -> CALayer {
- let layer = CALayer()
- layer.frame = CGRect(origin: CGPointZero, size: CGSize(width: sideLength, height: sideLength))
- layer.position = CGPoint(x: CGRectGetMidX(someView.bounds), y: CGRectGetMidY(someView.bounds))
- layer.backgroundColor = color.CGColor
- return layer
- }
-
- func degreesToRadians(degrees: Double) -> CGFloat {
- return CGFloat(degrees * M_PI / 180.0)
- }
-
- func applyRotationForXOffset(xOffset: Double, yOffset: Double) {
- let totalOffset = sqrt(xOffset * xOffset + yOffset * yOffset)
- let totalRotation = CGFloat(totalOffset * M_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 rotationTransform = CATransform3DRotate(transformLayer.sublayerTransform, totalRotation,
- xRotationalFactor * currentTransform.m12 - yRotationalFactor * currentTransform.m11,
- xRotationalFactor * currentTransform.m22 - yRotationalFactor * currentTransform.m21,
- xRotationalFactor * currentTransform.m32 - yRotationalFactor * currentTransform.m31)
- transformLayer.sublayerTransform = rotationTransform
- }
-
- override func touchesBegan(touches: NSSet, withEvent event: UIEvent) {
- if let location = touches.anyObject()?.locationInView(someView) {
- for layer in transformLayer.sublayers {
- if let hitLayer = layer.hitTest(location) {
- println("Transform layer tapped!")
- break
- }
- }
- }
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- setUpTransformLayer()
- someView.layer.addSublayer(transformLayer)
- }
-
- }
上述代碼解釋:
建立屬性,分別爲立方體的邊長、每一個面的顏色,還有一個變換圖層。
建立六個面,旋轉後添加到變換圖層,構成立方體,而後設置變換圖層的z軸錨點,旋轉立方體,將其添加到視圖結構樹。
輔助代碼,用來建立指定顏色的面,還有角度和弧度的轉換。在變換代碼中利用弧度轉換函數在某種程度上能夠增長代碼可讀性。 :]
基於指定xy偏移的旋轉,注意變換應用對象設爲sublayerTransform,即變換圖層的子圖層。
監聽觸摸,遍歷變換圖層的子圖層,對每一個圖層進行點擊測試,一旦成功相應當即跳出循環,不用繼續遍歷。
設置變換圖層,添加到視圖結構樹。
注:currentTransform.m##是啥?問得好,是CATransform3D屬性,表明矩陣元素。想學習如上代碼中的矩陣變換,請參考RW教程組成員Rich Turton的三維變換娛樂教學,還有Mark Pospesel的初識矩陣項目。
在250 x 250的someView視圖中運行上述代碼結果以下:
再試試點擊立方體的任意位置,控制檯會輸出「Transform layer tapped!」信息。
圖層演示應用中能夠調整透明度,此外Bill Dudney軌跡球工具, Swift移植版能夠基於簡單的用戶手勢應用三維變換。
示例 #10:CAEmitterLayer
CAEmitterLayer渲染的動畫粒子是CAEmitterCell實例。CAEmitterLayer和CAEmitterCell都包含可調整渲染頻率、大小、形狀、顏色、速率以及生命週期的屬性。示例以下:
- import UIKit
-
- class ViewController: UIViewController {
-
- let emitterLayer = CAEmitterLayer()
- let emitterCell = CAEmitterCell()
-
- func setUpEmitterLayer() {
- emitterLayer.frame = view.bounds
- emitterLayer.seed = UInt32(NSDate().timeIntervalSince1970)
- emitterLayer.renderMode = kCAEmitterLayerAdditive
- emitterLayer.drawsAsynchronously = true
- setEmitterPosition()
- }
-
- func setUpEmitterCell() {
- emitterCell.contents = UIImage(named: "smallStar")?.CGImage
-
- emitterCell.velocity = 50.0
- emitterCell.velocityRange = 500.0
-
- emitterCell.color = UIColor.blackColor().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() {
- emitterLayer.emitterPosition = CGPoint(x: CGRectGetMidX(view.bounds), y: CGRectGetMidY(view.bounds))
- }
-
- func degreesToRadians(degrees: Double) -> CGFloat {
- return CGFloat(degrees * M_PI / 180.0)
- }
-
- override func viewDidLoad() {
- super.viewDidLoad()
-
- setUpEmitterLayer()
- setUpEmitterCell()
- emitterLayer.emitterCells = [emitterCell]
- view.layer.addSublayer(emitterLayer)
- }
-
- override func traitCollectionDidChange(previousTraitCollection: UITraitCollection?) {
- setEmitterPosition()
- }
-
- }
以上代碼解析:
1.建立粒子發射器圖層和粒子胞(Creates an emitter layer and cell.)。
2.按照下方步驟設置粒子發射器圖層:
爲隨機數生成器提供種子,隨機調整粒子胞的某些屬性,如速度。
在圖層背景色和邊界之上按renderMode指定的順序渲染粒子胞。
注:渲染模式默認爲無序(unordered),其餘模式包括舊粒子優先(oldest first),新粒子優先(oldest last),按z軸位置從後至前(back to front)還有疊加式渲染(additive)。
因爲粒子發射器須要反覆重繪大量粒子胞,設drawsAsynchronously爲true會提高性能。
而後藉助第四條中會提到的輔助方法設置發射器位置,這個例子有助於理解把drawsAsynchronously設爲true爲什麼可以提高性能和動畫流暢度。
3.這段代碼設了很多東西。
配置粒子胞,設內容爲圖片(圖片在圖層演示項目中)。
指定初速及其變化量範圍(velocityRange),發射器圖層利用上面提到的隨機數種子建立隨機數生成器,在範圍內產生隨機值(初值+/-變化量範圍),其餘以「Range」結尾的相關屬性的隨機化規則相似。
設顏色爲黑色,使自變色(variance)與默認的白色造成對比,白色造成的粒子亮度太高。
利用隨機化範圍設置顏色,指定自變色範圍,顏色速度值表示粒子胞生命週期內顏色變化快慢。
接下來這幾行代碼指定粒子胞分佈範圍,一個全圓錐。設置粒子胞轉速和發射範圍,發射範圍emissionRange屬性的弧度值決定粒子胞分佈空間。
設粒子胞生命週期爲1秒,默認值爲0,表示粒子胞不會出現。birthRate也相似,以秒爲單位,默認值爲0,爲使粒子胞顯示出來,必須設成正數。
最後設xy加速度,這些值會影響已發射粒子的視角。
4.把角度轉換成弧度的輔助方法,還有設置粒子胞位置爲視圖中點。
5.設置發射器圖層和粒子胞,把粒子胞添加到圖層,而後把圖層添加到視圖結構樹。
6.iOS 8的新方法,處理當前設備形態集(trait collection)的變化,好比設備旋轉。不熟悉形態集的話能夠參閱iOS 8教程。
總算說完了!信息量很大,但相信各位聰明的讀者能夠高效吸取。
上述代碼運行效果以下:
圖層演示應用中,你能夠隨意調節不少屬性:
何去何從?
恭喜,看完十則示例和各類圖層子類,CALayer之旅至此告一段落。
但如今纔剛剛開始!新建一個項目,或者打開已有項目,嘗試利用圖層提高性能或營造酷炫效果!實踐出真知。