作者:Scott Gardner 譯者:TurtleFromMars
原文:CALayer in iOS with Swift: 10 Examples
如你所知,我們在iOS應用中看到的都是視圖(view),包括按鈕視圖、表視圖、滑動條視圖,還有可以容納其他視圖的父視圖等。
但你或許不知道在iOS中支撐起每個視圖的是一個叫做"圖層(layer)"的類,確切地說是CALayer。
本文中您會了解CALayer及其工作原理,還有應用CALayer打造酷炫效果的十則示例,比如繪制矢量圖形、漸變色,甚至是粒子系統。
本文要求讀者熟悉iOS應用開發和Swift語言的基礎知識,包括利用Storyboard構建用戶界面。
注:如果您尚未掌握這些基礎,不必擔心,我們有不少相關教程,例如使用Swift語言編寫iOS應用和iOS學徒。
准備開始
要理解圖層是什麼,最簡便的方式就是"實地考察"。我們這就創建一個簡單的項目,從頭開始玩轉圖層。
准備好寫代碼了嗎?好!啟動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圖層有很多屬性,我們來看一批實用屬性:有些屬性你可能第一次見,但真的很方便!
// 1 let layer = CALayer() layer.frame = someView.bounds // 2 layer.contents = UIImage(named: "star")?.CGImage layer.contentsGravity = kCAGravityCenter // 3 layer.magnificationFilter = kCAFilterLinear layer.geometryFlipped = false // 4 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 // 5 layer.cornerRadius = 100.0 layer.borderWidth = 12.0 layer.borderColor = UIColor.whiteColor().CGColor // 6 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的滾動模式可設為水平、垂直或者二維,你也可以用代碼命令視圖滾動到指定位置:
// In ScrollingView.swift import UIKit class ScrollingView: UIView { // 1 override class func layerClass() -> AnyClass { return CAScrollLayer.self } } // In CAScrollLayerViewController.swift import UIKit class CAScrollLayerViewController: UIViewController { @IBOutlet weak var scrollingView: ScrollingView! // 2 var scrollingViewLayer: CAScrollLayer { return scrollingView.layer as CAScrollLayer } override func viewDidLoad() { super.viewDidLoad() // 3 scrollingViewLayer.scrollMode = kCAScrollBoth } @IBAction func tapRecognized(sender: UITapGestureRecognizer) { // 4 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)規則,也有動畫效果:
// 1 let textLayer = CATextLayer() textLayer.frame = someView.bounds // 2 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 // 3 let fontName: CFStringRef = "Noteworthy-Light" textLayer.font = CTFontCreateWithName(fontName, fontSize, nil) // 4 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() // 1 let playerLayer = AVPlayerLayer() playerLayer.frame = someView.bounds // 2 let url = NSBundle.mainBundle().URLForResource("someVideo", withExtension: "m4v") let player = AVPlayer(URL: url) // 3 player.actionAtItemEnd = .None playerLayer.player = player someView.layer.addSublayer(playerLayer) // 4 NSNotificationCenter.defaultCenter().addObserver(self, selector: "playerDidReachEndNotificationHandler:", name: "AVPlayerItemDidPlayToEndTimeNotification", object: player.currentItem) } deinit { NSNotificationCenter.defaultCenter().removeObserver(self) } // 5 @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() } // 6 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能夠以特定次數復制圖層,可以用來創建一些很棒的效果。
每個圖層復件的顏色和位置都可以改動,而且可以在總復制圖層之後延遲繪制,營造一種動畫效果。還可以利用深度,創造三維效果。舉個例子
// 1 let replicatorLayer = CAReplicatorLayer() replicatorLayer.frame = someView.bounds // 2 replicatorLayer.instanceCount = 30 replicatorLayer.instanceDelay = CFTimeInterval(1 / 30.0) replicatorLayer.preservesDepth = false replicatorLayer.instanceColor = UIColor.whiteColor().CGColor // 3 replicatorLayer.instanceRedOffset = 0.0 replicatorLayer.instanceGreenOffset = -0.5 replicatorLayer.instanceBlueOffset = -0.5 replicatorLayer.instanceAlphaOffset = 0.0 // 4 let angle = Float(M_PI * 2.0) / 30 replicatorLayer.instanceTransform = CATransform3DMakeRotation(CGFloat(angle), 0.0, 0.0, 1.0) someView.layer.addSublayer(replicatorLayer) // 5 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) // 6 let fadeAnimation = CABasicAnimation(keyPath: "opacity") fadeAnimation.fromValue = 1.0 fadeAnimation.toValue = 0.0 fadeAnimation.duration = 1 fadeAnimation.repeatCount = Float(Int.max) // 7 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繪制圖塊填充視圖背景,如下:
// In ViewController.swift import UIKit class ViewController: UIViewController { // 1 @IBOutlet weak var tiledBackgroundView: TiledBackgroundView! } // In TiledBackgroundView.swift import UIKit class TiledBackgroundView: UIView { let sideLength = CGFloat(50.0) // 2 override class func layerClass() -> AnyClass { return CATiledLayer.self } // 3 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) } // 4 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子類,繪制分塊圖層:
import UIKit class TilingViewForImage: UIView { // 1 let sideLength = CGFloat(640.0) let fileName = "windingRoad" let cachesPath = NSSearchPathForDirectoriesInDomains(.CachesDirectory, .UserDomainMask, true)[0] as String // 2 override class func layerClass() -> AnyClass { return CATiledLayer.self } // 3 required init(coder aDecoder: NSCoder) { super.init(coder: aDecoder) let layer = self.layer as CATiledLayer layer.tileSize = CGSize(width: sideLength, height: sideLength) } // 4 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! // 1 let rwColor = UIColor(red: 11/255.0, green: 86/255.0, blue: 14/255.0, alpha: 1.0) let rwPath = UIBezierPath() let rwLayer = CAShapeLayer() // 2 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() } // 3 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() // 4 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! // 1 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() // 2 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) } // 3 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) } // 4 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 } // 5 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() // 6 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 { // 1 let emitterLayer = CAEmitterLayer() let emitterCell = CAEmitterCell() // 2 func setUpEmitterLayer() { emitterLayer.frame = view.bounds emitterLayer.seed = UInt32(NSDate().timeIntervalSince1970) emitterLayer.renderMode = kCAEmitterLayerAdditive emitterLayer.drawsAsynchronously = true setEmitterPosition() } // 3 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 } // 4 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() // 5 setUpEmitterLayer() setUpEmitterCell() emitterLayer.emitterCells = [emitterCell] view.layer.addSublayer(emitterLayer) } // 6 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之旅至此告一段落。
但現在才剛剛開始!新建一個項目,或者打開已有項目,嘗試利用圖層提升性能或營造酷炫效果!實踐出真知。