?
本文譯自:How To Make a Custom Control in Swift
用戶界面控件是所有應用程序重要的組成部分之一。它們以圖形組件的方式呈現給用戶,用戶可以通過它們與應用程序進行交互。蘋果提供了一套控件,例如 UITextField
,UIButton
,UISwitch
。通過工具箱中的這些已有控件,我們可以創建各式各樣的用戶界面。
然而,有時候你希望界面做得稍微的與眾不同,那么此時蘋果提供的這些控件就無法滿足你的需求。
自定義控件,除了是自己構建二外,與蘋果提供的,沒什么差別。也就是說,自定義控件不存在于 UIKit 框架。自定義控件跟蘋果提供的標準控件一樣,應該是通用,并且多功能的。你也會發現,互聯網上有一些積極的開發者樂意分享他們自定義的控件。
本文中,你將實現一個自己的 RangeSlider 自定義控件。這個控件是一個兩端都可以滑動的,也就是說,你可以通過該控件獲得最小值和最大值。你將會接觸到這樣一些概念:對現有控件的擴展,設計和實現 自定義控件的 API,甚至還能學到如何分享你的自定義控件到開發社區中。
注意:本文截稿時,我們還不會貼出關于 iOS 8 beta 版本的截圖。所有文中涉及到的截圖都是在iOS 8之前的版本中得到的,不過結果非常類似。
目錄:
- 開始
- Images vs. CoreGraphics
- 添加默認的控件屬性
- 添加交互邏輯
- 添加觸摸處理
- 值改變的通知
- 結合 Core Graphics 對控件進行修改
- 處理控件屬性的改變
- 何去何從?
開始
假設你在開發一個應用程序,該程序提供搜索商品價格列表。通過這個假象的應用程序允許用戶對搜索結果進行過濾,以獲得一定價格范圍的商品。你可能會提供這樣一個用戶界面:兩個 UISlider
控件,一個用于設置最低價格,另外一個設置最高價格。然而,這樣的設計,不能夠讓用戶很好的感知價格的范圍。要是能夠提供一個 slider,兩端可以分別設置用于搜索的最高和最低的價格范圍,就更好了。
你可以通過創建一個 UIView
的子類,然后為可視的價格范圍定做一個 view。這對于應用程序內部來說,是 ok的,但是要想移植到別的程序中,就需要花更多的精力了。
最好的辦法是將構建一個新的盡可能通用的 UI 控件,這樣就能在任意的合適場合中重用。這也是自定義控件的本質。
啟動 Xcode,File/New/Project
,選中 iOS/Application/Single View Application
模板,然后點擊 Next
。在接下來的界面中,輸入 CustomSliderExample
當做工程名,然后是 Organization Name
和 Organization Identifier
,然后,一定要確保選中 Swift
語言,iPhone
選中,Use Core Data
不要選。
最后,選擇一個保存工程的地方并單擊 Create
。
首先,我們需要做出決定的就是創建自定義控件需要繼承自哪個類,或者對哪個類進行擴展。
位了使自定義控件能夠在應用程序中使用,你的類必須是 UIView
的一個子類。
如果你注意觀察蘋果的 UIKit
參考,會發現框架中的許多控件,例如 UILabel
和 UIWebView
都是直接繼承自 UIView
的。然而,也有極少數,例如 UIButton
和 UISwitch
是繼承自 UIControl
的,如下繼承圖所示:
注意:iOS 中 UI 組件的完整類繼承圖,請看 UIKit Framework 參考。
UIControl
實現了 target-action 模式
,這是一種將變化通知訂閱者的機制。UIControl
同樣還有一些與控件狀態相關的屬性。在本文中的自定義空間中,將使用到 target-action 模式,所以從 UIControl
開始繼承使用將是一個非常好的切入點。
在 Project Navigator 中右鍵單擊 CustomSliderExample
,選擇 New File…
,然后選擇 iOS/Source/Cocoa Touch Class
模板,并單擊 Next
。將類命名位 RangeSlider
,在 Subclass of
字段中輸入 UIControl
,并確保語言是 Swift
。然后單擊 Next
,并在默認存儲位置中 Create
出新的類。
雖然編碼非常讓人愉悅,不過你可能也希望盡快看到自定義控件在屏幕中熏染出來的模樣!在寫自定義控件相關的任何代碼之前,你應該先把這個控件添加到 view controller中,這樣就可以實時觀察控件的演進程度。
打開 ViewController.swift
,用下面的內容替換之:
1 import UIKit 2 3 class ViewController: UIViewController { 4 let rangeSlider = RangeSlider(frame: CGRectZero) 5 6 override func viewDidLoad() { 7 super.viewDidLoad() 8 9 rangeSlider.backgroundColor = UIColor.redColor() 10 view.addSubview(rangeSlider) 11 } 12 13 override func viewDidLayoutSubviews() { 14 let margin: CGFloat = 20.0 15 let width = view.bounds.width - 2.0 * margin 16 rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length, 17 width: width, height: 31.0) 18 } 19 }
?
上面的代碼根據指定的 frame 實例化了一個全新的控件,然后將其添加到 view 中。為了在應用程序背景中凸顯出控件,我們將控件的背景色被設置位了紅色。如果不把控件的背景色設置為紅色,那么控件中什么都沒有,可能會想,控件去哪里了!:]
編譯并運行程序,將看到如下類似界面:
在開始給控件添加可視元素之前,應該先定義幾個屬性,用以在控件中記錄下各種信息。這也是開始應用程序編程接口 (API) 的開始。
注意:控件中定義的方法和屬性是你決定用來暴露給別的開發者使用的。稍后你將看到 API 設計相關的內容,現在只需要緊跟就行!
添加默認的控件屬性
打開 RangeSlider.swift
,用下面的代碼替換之:
1 import UIKit 2 3 class RangeSlider: UIControl { 4 var minimumValue = 0.0 5 var maximumValue = 1.0 6 var lowerValue = 0.2 7 var upperValue = 0.8 8 }
上面定義的四個屬性用來描述控件的狀態,提供最大值和最小值,以及有用戶設置的 upper 和 lower 兩個值。
好的控件設計,應該提供一些默認的屬性值,否則將你的控件繪制到屏幕中時,看起來會有點奇怪。
現在是時候開始做控件的交互元素了,我們分別用兩個 thumbs 表示高和低兩個值,并且讓這兩個 thumbs 能夠滑動。
Images vs. CoreGraphics
在屏幕中渲染控件有兩種方法:
1、Images
- 為控件構建不同的圖片,這些圖片代表控件的各種元素。
2、Core Graphics
- 利用 layers 和 Core Graphics 組合起來熏染控件。
這兩種方法都有利有弊,下面來看看:
Images
- 利用圖片來構建控件是最簡單的一種方法 - 只要你知道如何繪制圖片!:] 如果你想要讓開發者能夠修改控件的外觀,那么你應該將這些圖片以 UIImage 屬性的方式暴露出去。
通過圖片的方式來構建的控件,給使用控件的人提供了非常大的靈活度。開發者可以改變每一個像素,以及控件的詳細外觀,不過這需要非常熟練的圖形設計技能 - 并且通過代碼非常難以對控件做出修改。
Core Graphics
- 利用 Core Graphics 構建控件意味著你必須自己編寫渲染控件的代碼,這就需要付出更多的代價。不過,這種方法可以創建更加靈活的 API。
使用 Core Graphics,可以把控件的所有特征都參數化,例如顏色、邊框厚度和弧度 - 幾乎每一個可視元素都通過繪制完成!這種方法運行開發者對控件做出任意調整,以適配相應的需求。
本文中,你將學到第二種技術 - 利用 Core Graphics 來熏染控件。
主要:有趣的時,蘋果建議在他們提供的控件中使用圖片。這可能是蘋果知道每個控件的大小,他們不希望程序中出現太多的定制。也就是說,他們希望所有的應用程序,都具有相似的外觀和體驗。
打開 RangeSlider.swift
將下面的 import 添加到文件的頂部,也就是 import UIKit
下面:
?1 import QuartzCore?
將下面的屬性添加到 RangeSlider
中,也就是我們剛剛定義的那行代碼下面:
1 let trackLayer = CALayer() 2 let lowerThumbLayer = CALayer() 3 let upperThumbLayer = CALayer() 4 5 var thumbWidth: CGFloat { 6 return CGFloat(bounds.height) 7 }
?
這里有 3 個 layer - trackLayer
, lowerThumbLayer
, 和 upperThumbLayer
- 用來熏染滑塊控件的不同組件。thumbWidth
用來布局使用。
接下來就是控件默認的一些圖形屬性。
在 RangeSlider
類中,添加一個 初始化方法,以及一個 helper 方法:
1 override init(frame: CGRect) { 2 super.init(frame: frame) 3 4 trackLayer.backgroundColor = UIColor.blueColor().CGColor 5 layer.addSublayer(trackLayer) 6 7 lowerThumbLayer.backgroundColor = UIColor.greenColor().CGColor 8 layer.addSublayer(lowerThumbLayer) 9 10 upperThumbLayer.backgroundColor = UIColor.greenColor().CGColor 11 layer.addSublayer(upperThumbLayer) 12 13 updateLayerFrames() 14 } 15 16 required init(coder: NSCoder) { 17 super.init(coder: coder) 18 } 19 20 func updateLayerFrames() { 21 trackLayer.frame = bounds.rectByInsetting(dx: 0.0, dy: bounds.height / 3) 22 trackLayer.setNeedsDisplay() 23 24 let lowerThumbCenter = CGFloat(positionForValue(lowerValue)) 25 26 lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0, 27 width: thumbWidth, height: thumbWidth) 28 lowerThumbLayer.setNeedsDisplay() 29 30 let upperThumbCenter = CGFloat(positionForValue(upperValue)) 31 upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0, 32 width: thumbWidth, height: thumbWidth) 33 upperThumbLayer.setNeedsDisplay() 34 } 35 36 func positionForValue(value: Double) -> Double { 37 let widthDouble = Double(thumbWidth) 38 return Double(bounds.width - thumbWidth) * (value - minimumValue) / 39 (maximumValue - minimumValue) + Double(thumbWidth / 2.0) 40 }
?
初始化方法簡單的創建了 3 個 layer,并將它們以 children 的身份添加到控件的 root layer 中,然后通過 updateLayerFrames
對這些 layer 的位置進行更新定位! :]
最后,positionForValue
方法利用一個簡單的比例,對控件的最小和最大值的范圍做了一個縮放,將值映射到屏幕中確定的一個位置。
接下來,override一下 frame
,通過將下面的代碼添加到 RangeSlider.swift
中,實現對屬性的觀察:
?1 override var frame: CGRect { 2 didSet { 3 updateLayerFrames() 4 } 5 }?
?
當 frame 發生變化時,屬性觀察者會更新 layer frame。這一步是必須的,因為當控件初始化時,傳入的 frame 并不是最終的 frame,就像 ViewController.swift
中的。
編譯并運行程序,可以看到滑塊初具形狀!看起來,如下圖所示:
還記得嗎,紅色是整個控件的背景色。藍色是滑塊的軌跡,綠色 thumb 是兩個代表兩端的值。
現在控件看起來有形狀了,不過幾乎所有的控件都提供了相關方法,讓用戶與之交互。
針對本文中的控件,用戶必須能夠通過拖拽 2 個 thumb 來設置控件的范圍。你將處理這些交互,并通過控件更新 UI 和暴露的屬性。
添加交互邏輯
本文的交互邏輯需要存儲那個 thumb 被拖拽了,并將效果反應到 UI 中。控件的 layer 是放置該邏輯的最佳位置。
跟之前一樣,在 Xcode 中創建一個新的 Cocoa Touch Class
,命名為 RangeSliderThumbLayer
,繼承自 CALayer
。
用下面的代碼替換掉 RangeSliderThumbLayer.swift
文件中的內容:
1 import UIKit 2 import QuartzCore 3 4 class RangeSliderThumbLayer: CALayer { 5 var highlighted = false 6 weak var rangeSlider: RangeSlider? 7 }
?
上面的代碼中簡單的添加了兩個屬性:一個表示這個 thumb 是否 高亮 (highlighted),另外一個引用回父 range slider。由于 RangeSlider 有兩個 thumb layer,所以將這里的引用設置位 weak,避免循環引用。
打開 RangeSlider.swift
,修改一下 lowerThumbLayer
和 upperThumbLayer
兩個屬性的類型,用下面的代碼替換掉它們的定義:
?1 let lowerThumbLayer = RangeSliderThumbLayer() 2 let upperThumbLayer = RangeSliderThumbLayer()?
還是在 RangeSlider.swift
中,找到 init
,將下面的代碼添加進去:
?1 lowerThumbLayer.rangeSlider = self 2 upperThumbLayer.rangeSlider = self?
?
上面的代碼簡單的將 layer 的 rangeSlider
屬性設置為 self
。
編譯并運行程序,界面看起來沒有什么變化。
現在你已經有了 slider 的thumb layer - RangeSliderThumbLayer,然后需要給控件添加拖拽 thumb 的功能。
添加觸摸處理
打開 RangeSlider.swift
,將下面這個屬性添加進去:
?1 var previousLocation = CGPoint()?
這個屬性用來跟蹤記錄用戶的觸摸位置。
那么你該如何來跟蹤控件的各種觸摸和 release 時間呢?
UIControl
提供了一些方法來跟蹤觸摸。UIControl
的子類可以 override 這些方法,以實現自己的交互邏輯。
在自定義控件中,我們將 override 3 個 UIControl
關鍵的方法:beginTrackingWithTouch
, continueTrackingWithTouch
和 endTrackingWithTouch
。
將下面的方法添加到 RangeSlider.swift
中:
1 override func beginTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) -> Bool { 2 previousLocation = touch.locationInView(self) 3 4 // Hit test the thumb layers 5 if lowerThumbLayer.frame.contains(previousLocation) { 6 lowerThumbLayer.highlighted = true 7 } else if upperThumbLayer.frame.contains(previousLocation) { 8 upperThumbLayer.highlighted = true 9 } 10 11 return lowerThumbLayer.highlighted || upperThumbLayer.highlighted 12 }
?
當首次觸摸控件時,會調用上面的方法。
代碼中,首先將觸摸事件的坐標轉換到控件的坐標空間。然后檢查每個 thumb,是否觸摸位置在其上面。方法中返回的值將決定 UIControl 是否繼續跟蹤觸摸事件。
如果任意一個 thumb 被 highlighted 了,就繼續跟蹤觸摸事件。
現在,有了初始的觸摸事件,我們需要處理用戶在屏幕上移動的事件了。
將下面的方法添加到 RangeSlider.swift
中:
1 func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double { 2 return min(max(value, lowerValue), upperValue) 3 } 4 5 override func continueTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) -> Bool { 6 let location = touch.locationInView(self) 7 8 // 1. Determine by how much the user has dragged 9 let deltaLocation = Double(location.x - previousLocation.x) 10 let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height) 11 12 previousLocation = location 13 14 // 2. Update the values 15 if lowerThumbLayer.highlighted { 16 lowerValue += deltaValue 17 lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue) 18 } else if upperThumbLayer.highlighted { 19 upperValue += deltaValue 20 upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue) 21 } 22 23 // 3. Update the UI 24 CATransaction.begin() 25 CATransaction.setDisableActions(true) 26 27 updateLayerFrames() 28 29 CATransaction.commit() 30 31 return true 32 }
?
boundValue
會將傳入的值控制在某個確定的范圍。通過這個方法比嵌套調用 min/max
更容易理解。
下面我們根據注釋,來分析一下 continueTrackingWithTouch
方法都做了些什么:
- 首先計算出位置增量,這個值決定著用戶手指移動的數值。然后根據控件的最大值和最小值,對這個增量做轉換。
- 根據用戶滑動滑塊的距離,修正一下 upper 或 lower 值。
- 設置
CATransaction
中的disabledActions
。這樣可以確保每個 layer 的frame 立即得到更新,并且不會有動畫效果。最后,調用updateLayerFrames
方法將 thumb 移動到正確的位置。
至此,已經編寫了移動滑塊的代碼 - 不過我們還要處理觸摸和拖拽事件的結束。
將下面方法添加到 RangeSlider.swift
中:
1 override func endTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) { 2 lowerThumbLayer.highlighted = false 3 upperThumbLayer.highlighted = false 4 }
?
上面的代碼簡單的將兩個 thumb 還原位 non-highlighted 狀態。
編譯并運行程序,嘗試移動滑塊!現在你應該可以移動 thumb 了。
你可能注意到當在移動滑塊時,可以在控件之外的范圍對其拖拽,然后手指回到控件內,也不會丟失跟蹤。其實這在小屏幕的設備上,是非常重要的一個功能。
值改變的通知
現在你已經有一個可以交互的控件了 - 用戶可以對其進行操作,以設置范圍的大小值。但是如何才能把這些值的改變通知調用者:控件有新的值了呢?
這里有多種模式可以實現值改變的通知: NSNotification
,Key-Value-Observing
(KVO), delegate
模式,target-action
模式等。有許多選擇!
面對這么多的通知方式,那么我們該怎么選擇呢?
如果你研究過 UIKit
控件,會發現它們并沒有使用 NSNotification
,也不鼓勵使用 KVO。所以為了保持與 UIKit 的一致性,我們可以先排除這兩種方法。另外的兩種模式:delegate 和 target-action 被廣泛用于 UIKit 中。
Delegate 模式
- delegate 模式需要提供一個 protocol,里面有一些用于通知的方法。控件中有一個屬性,一般命名位 delegate,它可以是任意實現該協議的類。經典的一個示例就是 UITableView
提供了 UITableViewDelegate
protocol。注意,控件只接受單個 delegate 實例。一個 delegate 方法可以使用任意的參數,所以可以給這樣的方法傳遞盡可能多的信息。
Target-action 模式
- UIControl
基類已經提供了 target-action 模式。當控件狀態發生了改變,target 會獲得相應 action 的通知,該 action 是在 UIControlEvents
枚舉值做定義的。我們可以給控件的 action 提供多個 target,另外還可以創建自定義事件 (查閱 UIControlEventApplicationReserved
),自定義事件的數量不得超過 4 個。控件 action 針對某個事件,無法傳送任意的信息,所以當事件觸發時,不能用它來傳遞額外的信息。
這兩種模式關鍵不同點如下:
- 多播 (Multicast) - target-action 模式可以對改變事件進行多播通知,而 delegate 模式只能綁定到單個 delegate 實例上。
- 靈活 (Flexibility) - 在 delegate 模式中,你可以定義自己的 protocol,這就意味著你可以控制信息的傳遞量。而 target-action 是無法傳遞額外信息的,客戶端只能在收到事件后,自行查詢信息。
我們的 slider 控件不會有大量的狀態變化,也不需要提供大量的通知。唯一真正改變的就是控件的 upper 和 lower 值。
基于這樣的情況,使用 target-action 模式是最好的。這也是為什么在本文開頭的時候告訴你為什么這個控件要繼承自 UIControl
。
slider 的值是在 continueTrackingWithTouch:withEvent:
方法中進行更新的,所以這個方法也是添加通知代碼的地方。
打開 RangeSlider.swift
,定位到 continueTrackingWithTouch
方法,然后將下面的代碼添加到 return true
語句前面:
?1 sendActionsForControlEvents(.ValueChanged)?
上面的這行代碼就能將值改變事件通知給任意的訂閱者 target。
現在我們應該對這個事件進行訂閱,并當事件來了以后,作出相應的處理。
打開 ViewController.swift
,將下面這行代碼添加到 viewDidLoad
尾部:
?1 rangeSlider.addTarget(self, action: "rangeSliderValueChanged:", forControlEvents: .ValueChanged)?
通過上面的代碼,每次 slider 發送 UIControlEventValueChanged
action 時,都會調用 rangeSliderValueChanged
方法。
將下面的代碼添加到 ViewController.swift
中:
1 func rangeSliderValueChanged(rangeSlider: RangeSlider) { 2 println("Range slider value changed: (\(rangeSlider.lowerValue) \(rangeSlider.upperValue))") 3 }
?
當 slider 值發生變化是,上面這個方法簡單的將 slider 的值打印出來。
編譯并運行程序,并移動一下 slider,可以在控制臺中看到控件的值,如下所示:
1 Range slider value changed: (0.217687089658687 0.68610299780487) 2 Range slider value changed: (0.217687089658687 0.677356642119739) 3 Range slider value changed: (0.217687089658687 0.661807535688662) 4 Range slider value changed: (0.217687089658687 0.64625847374385) 5 Range slider value changed: (0.217687089658687 0.631681214268632) 6 Range slider value changed: (0.217687089658687 0.621963056113908) 7 Range slider value changed: (0.217687089658687 0.619047604218864) 8 Range slider value changed: (0.217687089658687 0.61613215232382)
?
看到 控件五顏六色的,你可能不高心,它開起來就像水果沙拉一樣!
現在是時候給控件換換面目了!
結合 Core Graphics 對控件進行修改
首先,首選更新一下slider thumb 移動的軌跡圖形。
跟之前一樣,給工程添加另外一個繼承自 CALayer
的子類,命名為 RangeSliderTrackLayer
。
打開剛剛添加的文件 RangeSliderTrackLayer.swift
,然后用下面的內容替換之:
1 import UIKit 2 import QuartzCore 3 4 class RangeSliderTrackLayer: CALayer { 5 weak var rangeSlider: RangeSlider? 6 }
?
上面的代碼添加了一個到 slider 控件的引用,跟之前 thumb layer 做的一樣。
打開 RangeSlider.swift
文件,找到 trackLayer
屬性,用剛剛創建的這個類對其實例化,如下所示:
?1 let trackLayer = RangeSliderTrackLayer()?
接下來,找到 init
并用下面的代碼替換之:
1 init(frame: CGRect) { 2 super.init(frame: frame) 3 4 trackLayer.rangeSlider = self 5 trackLayer.contentsScale = UIScreen.mainScreen().scale 6 layer.addSublayer(trackLayer) 7 8 lowerThumbLayer.rangeSlider = self 9 lowerThumbLayer.contentsScale = UIScreen.mainScreen().scale 10 layer.addSublayer(lowerThumbLayer) 11 12 upperThumbLayer.rangeSlider = self 13 upperThumbLayer.contentsScale = UIScreen.mainScreen().scale 14 layer.addSublayer(upperThumbLayer) 15 }
?
上面的代碼確保新的 track layer 引用到 range slider - 并沒有再用那可怕的顏色了!然后將 contentsScale
因子設置位與設備的屏幕一樣,這樣可以確保所有的內容在 retina 顯示屏中沒有問題。
下面還有一個事情需要做,就是將 viewDidLoad
中的如下代碼移除掉:
?1 rangeSlider.backgroundColor = UIColor.redColor()?
編譯并運行程序,看到什么了呢?
什么東西都沒有?這是正確的!
不要煩惱 - 我們只不過移除掉了在 layer 中花哨的測試顏色。控件依舊存在 - 只不過現在是白色的!
由于許多開發者希望能夠通過編碼對控件做各種配置,以使其外觀能夠效仿一些流行的程序,所以我們給 slider 添加一些屬性,運行開發者對其外觀做出一些定制。
打開 RangeSlider.swift
,將下面的屬性添加到已有屬性下面:
1 var trackTintColor = UIColor(white: 0.9, alpha: 1.0) 2 var trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) 3 var thumbTintColor = UIColor.whiteColor() 4 5 var curvaceousness : CGFloat = 1.0
?
這些顏色屬性的目的非常容易理解,但是 curvaceousness
?這個屬性在這里有點趣味 - 稍后你將發現其用途!
接下來,打來 RangeSliderTrackLayer.swift
。
這個 layer 用來渲染兩個 thumb 滑動的軌跡。目前它繼承自 CALayer
,僅僅是繪制一個單一顏色。
為了繪制軌跡,需要實現方法 drawInContext:
,并利用 Core Pgraphics APIs 來進行渲染。
注意:要想深入學習 Core Graphics,建議閱讀 Core Graphics 101 教程。
將下面這個方法添加到 RangeSliderTrackLayer
中:
1 override func drawInContext(ctx: CGContext!) { 2 if let slider = rangeSlider { 3 // Clip 4 let cornerRadius = bounds.height * slider.curvaceousness / 2.0 5 let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius) 6 CGContextAddPath(ctx, path.CGPath) 7 8 // Fill the track 9 CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor) 10 CGContextAddPath(ctx, path.CGPath) 11 CGContextFillPath(ctx) 12 13 // Fill the highlighted range 14 CGContextSetFillColorWithColor(ctx, slider.trackHighlightTintColor.CGColor) 15 let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue)) 16 let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue)) 17 let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height) 18 CGContextFillRect(ctx, rect) 19 } 20 }
?
一旦 track 形狀確定,控件的背景色就會被填充,另外高亮范圍也會被填充。
編譯并運行程序,會看到新的 track layer 被完美的渲染出來!如下圖所示:
給暴露出來的屬性設置不同的值,觀察一下它們是如何反應到控件渲染中的。
如果你對 curvaceousness
做什么的還存在疑惑,那么試著修改一下它看看!
接下來我們使用相同的方法來繪制 thumb layer。
打開 RangeSliderThumbLayer.swift
,然后將下面的方法添加到屬性聲明的下方:
1 override func drawInContext(ctx: CGContext!) { 2 if let slider = rangeSlider { 3 let thumbFrame = bounds.rectByInsetting(dx: 2.0, dy: 2.0) 4 let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0 5 let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius) 6 7 // Fill - with a subtle shadow 8 let shadowColor = UIColor.grayColor() 9 CGContextSetShadowWithColor(ctx, CGSize(width: 0.0, height: 1.0), 1.0, shadowColor.CGColor) 10 CGContextSetFillColorWithColor(ctx, slider.thumbTintColor.CGColor) 11 CGContextAddPath(ctx, thumbPath.CGPath) 12 CGContextFillPath(ctx) 13 14 // Outline 15 CGContextSetStrokeColorWithColor(ctx, shadowColor.CGColor) 16 CGContextSetLineWidth(ctx, 0.5) 17 CGContextAddPath(ctx, thumbPath.CGPath) 18 CGContextStrokePath(ctx) 19 20 if highlighted { 21 CGContextSetFillColorWithColor(ctx, UIColor(white: 0.0, alpha: 0.1).CGColor) 22 CGContextAddPath(ctx, thumbPath.CGPath) 23 CGContextFillPath(ctx) 24 } 25 } 26 }
?
一旦定義好了 thumb 的形狀路徑,就會將其形狀填充好。注意繪制微弱的陰影看起來的效果就是 thumb 上方的軌跡。接下來是繪制邊框。最后,如果 thumb 是高亮的 - 也就是被移動狀態 - 那么就繪制微弱的灰色陰影效果。
在運行之前,還有最后一件事情要做。按照下面的代碼對 highlighted
屬性的定義做出修改:
?1 var highlighted: Bool = false { 2 didSet { 3 setNeedsDisplay() 4 } 5 }?
這里,定義了一個屬性觀察者,這樣當每次 highlighted 屬性修改時,相應的 layer 都會得到重繪。這會使得觸摸事件發生時,填充色發生輕微的變動。
再次編譯并運行程序,這下看起來會非常的有形狀,如下圖所示:
不難發現,用 Core Graphics 來繪制控件是非常值得做的。使用 Core Graphics 可以做出比通過圖片渲染方法更通用的控件。
處理控件屬性的改變
那么到現在,還有什么事情要做呢?控件現在看起來已經非常的華麗了,它的外觀是通用的,并且也支持 target-action 通知。
貌似已經做完了?
思考一下,如果當控件熏染之后,如果通過代碼對 slider 的屬性做了修改,會發生什么?例如,你希望修改一下 slider 的默認值,或者修改一下 track highlight,表示出一個有效范圍。
目前,還沒有任何代碼來觀察屬性的設置情況。我們需要將其添加到控件中。我們需要實現屬性觀察者,來更新控件的 frame 或者重繪控件。打開 RangeSlider.swift
,按照下面的代碼對屬性的聲明作出修改:
1 var minimumValue: Double = 0.0 { 2 didSet { 3 updateLayerFrames() 4 } 5 } 6 7 var maximumValue: Double = 1.0 { 8 didSet { 9 updateLayerFrames() 10 } 11 } 12 13 var lowerValue: Double = 0.2 { 14 didSet { 15 updateLayerFrames() 16 } 17 } 18 19 var upperValue: Double = 0.8 { 20 didSet { 21 updateLayerFrames() 22 } 23 } 24 25 var trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) { 26 didSet { 27 trackLayer.setNeedsDisplay() 28 } 29 } 30 31 var trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) { 32 didSet { 33 trackLayer.setNeedsDisplay() 34 } 35 } 36 37 var thumbTintColor: UIColor = UIColor.whiteColor() { 38 didSet { 39 lowerThumbLayer.setNeedsDisplay() 40 upperThumbLayer.setNeedsDisplay() 41 } 42 } 43 44 var curvaceousness: CGFloat = 1.0 { 45 didSet { 46 trackLayer.setNeedsDisplay() 47 lowerThumbLayer.setNeedsDisplay() 48 upperThumbLayer.setNeedsDisplay() 49 } 50 }
?
一般情況,我們需要根據依賴的屬性,調用 setNeedsDisplay
方法將對于的 layer 進行重新處理。setLayerFrames
方法會對控件的布局作出調整。
現在,找到 updateLayerFrames
,然后將下面的代碼添加到該方法的頂部:
?1 CATransaction.begin() 2 CATransaction.setDisableActions(true)?
并將下面的代碼添加到方法的尾部:
?1 CATransaction.commit()?
上面的代碼將整個 frame 的更新封裝到一個事物處理中,這樣可以讓界面重繪變得流暢。同樣還明確的把 layer 中的動畫禁用掉,跟之前一樣,這樣 layer frame 的更新會變得即時。
由于現在每當 upper 和 lower 值發生變動時, frame 會自動更新了,所以,找到 continueTrackingWithTouch
方法,并將下面的代碼刪除掉:
1 // 3. Update the UI 2 CATransaction.begin() 3 CATransaction.setDisableActions(true) 4 5 updateLayerFrames() 6 7 CATransaction.commit()
?
上面的這些代碼就能夠確保屬性變化時,能夠反應到 slider 控件中。
為了確保代碼無誤,我們需要寫點測試 case 進行測試。
打開 ViewController.swift
,并將下面代碼添加到 viewDidLoad:
尾部:
1 let time = dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)) 2 dispatch_after(time, dispatch_get_main_queue()) { 3 self.rangeSlider.trackHighlightTintColor = UIColor.redColor() 4 self.rangeSlider.curvaceousness = 0.0 5 }
?
上面的代碼會在暫停 1 秒鐘之后,對控件的一些屬性做出更新。其中將 track highlight 的顏色修改為紅色,并修改了 slider 和 thumb 的形狀。
編譯并運行程序,一秒鐘之后,你看到 slider 由:
變為:
很容易不是嗎?
上面剛剛添加到 view controller 中的代碼,演示了一個非常有趣,而又經常被忽略的內容 - 對開發的自定義控件做充分的測試。當你在開發一個自定義控件時,你需要負責對所有的屬性和外觀做出驗證。這里有一個好的方法就是創建不同的按鈕和滑塊 (它們連接到控件的不同屬性) 對控件做出測試。這樣,你就可以實時修改控件的屬性,并實時觀察到它們的結果。
何去何從?
現在我們的 range slider 控件已經完成開發,并可以在程序中使用了!你可以在這里下載到完整的工程(方便的話給個小小的star...)。
不過,創建通用性自定義控件的一個關鍵好處就是你可以將其用于不同的工程 - 并且分享給別的開發者使用。
準備好了嗎?
實際上還沒有。在分享自定義控件之前,還有一些事情需要考慮:
希望通過本文的學習,你已經能愉悅的創建 slider 控件了,可能你還希望構建自己的自定義控件。如果你做了,可以在本文的評論中分享一下 - 我們非常想看到你的創作!(分享了)
本文轉載自(破船之家)