如何用 Swift 語言構建一個自定控件

?

本文譯自:How To Make a Custom Control in Swift

用戶界面控件是所有應用程序重要的組成部分之一。它們以圖形組件的方式呈現給用戶,用戶可以通過它們與應用程序進行交互。蘋果提供了一套控件,例如 UITextFieldUIButtonUISwitch。通過工具箱中的這些已有控件,我們可以創建各式各樣的用戶界面。

然而,有時候你希望界面做得稍微的與眾不同,那么此時蘋果提供的這些控件就無法滿足你的需求。

自定義控件,除了是自己構建二外,與蘋果提供的,沒什么差別。也就是說,自定義控件不存在于 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 NameOrganization Identifier,然后,一定要確保選中 Swift 語言,iPhone 選中,Use Core Data 不要選。

最后,選擇一個保存工程的地方并單擊 Create

首先,我們需要做出決定的就是創建自定義控件需要繼承自哪個類,或者對哪個類進行擴展。

位了使自定義控件能夠在應用程序中使用,你的類必須是 UIView 的一個子類。

如果你注意觀察蘋果的 UIKit 參考,會發現框架中的許多控件,例如 UILabelUIWebView 都是直接繼承自 UIView 的。然而,也有極少數,例如 UIButtonUISwitch 是繼承自 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,修改一下 lowerThumbLayerupperThumbLayer 兩個屬性的類型,用下面的代碼替換掉它們的定義:

?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, continueTrackingWithTouchendTrackingWithTouch

將下面的方法添加到 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 方法都做了些什么:

  1. 首先計算出位置增量,這個值決定著用戶手指移動的數值。然后根據控件的最大值和最小值,對這個增量做轉換。
  2. 根據用戶滑動滑塊的距離,修正一下 upper 或 lower 值。
  3. 設置 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 了。

你可能注意到當在移動滑塊時,可以在控件之外的范圍對其拖拽,然后手指回到控件內,也不會丟失跟蹤。其實這在小屏幕的設備上,是非常重要的一個功能。

值改變的通知

現在你已經有一個可以交互的控件了 - 用戶可以對其進行操作,以設置范圍的大小值。但是如何才能把這些值的改變通知調用者:控件有新的值了呢?

這里有多種模式可以實現值改變的通知: NSNotificationKey-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 控件了,可能你還希望構建自己的自定義控件。如果你做了,可以在本文的評論中分享一下 - 我們非常想看到你的創作!(分享了)

本文轉載自(破船之家)

轉載于:https://www.cnblogs.com/chenyihang/p/5640079.html

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/287138.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/287138.shtml
英文地址,請注明出處:http://en.pswp.cn/news/287138.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【ArcGIS遇上Python】ArcGIS Python獲取Shapefile矢量數據字段名稱

借助PyCharm環境,在不打開ArcGIS的情況下,編寫Python代碼,獲取矢量數據的所有字段。 import arcpyshp C:\data\out\Export_Output.shp fields arcpy.ListFields(shp) for f in fields:print f.name‘,’f.type運行結果: C:\Pyt…

《聰明人和傻子和程序員》

本文借鑒自魯迅雜文《聰明人和傻子和奴才》,如有雷同,純屬巧合。有個程序員特別喜歡尋人訴苦,只要一點事,就喜歡訴苦。有一日,他遇到一個聰明人。“大佬。”他悲哀的說,“我們公司待遇越來越差了&#xff0…

c語言 case語句用法,switch ... case語句的用法[組圖]

switch ... case語句的用法[組圖]08-13欄目:技術TAG:switch case語句switch case語句當情況大于或等于4種的時候就用switch ... case語句copyright jhua.orgswitch(表達式) copyright jhua.org{ https://www.jhua.orgcase 常量1: 語句體1&am…

《看聊天記錄都學不會C#?太菜了吧》(5)C# 中可以用中文名變量?

本系列文章將會以通俗易懂的對話方式進行教學,對話中將涵蓋了新手在學習中的一般問題。此系列將會持續更新,包括別的語言以及實戰都將使用對話的方式進行教學,基礎編程語言教學適用于零基礎小白,之后實戰課程也將會逐步更新。 若…

Android之TabLayout和ViewPager組合跳轉到指定頁面

1 問題 TabLayout和ViewPager組合跳轉到具體一個頁面 2 解決辦法 viewPager?.setCurrentItem(index) index為0說明是第一頁,如果是1的話就是第二頁,以此類推。

【ArcGIS遇上Python】ArcGIS Python中文編碼問題案例詳解

前面的文章《ArcGIS Python獲取Shapefile矢量數據字段名稱》我們已經學會了如何用 Python 獲取中文路徑下的shp數據的所有字段,英文沒有問題,但是如果你輸出中文路徑下的數據字段, 就有可能會碰到中文編碼問題。 Python 文件中如果未指定編碼,在執行過程會出現報錯: impo…

gRPC編碼初探(java)

背景:gRPC是一個高性能、通用的開源RPC框架,其由Google主要面向移動應用開發并基于HTTP/2協議標準而設計,基于ProtoBuf(Protocol Buffers)序列化協議開發,且支持眾多開發語言。gRPC提供了一種簡單的方法來精確地定義服務和為iOS、…

WPF 基礎控件之 RadioButton 樣式

其他基礎控件1.Window2.Button3.CheckBox4.ComboBox5.DataGrid 6.DatePicker7.Expander8.GroupBox9.ListBox10.ListView11.Menu12.PasswordBox13.TextBox14.ProgressBarRadioButton 實現下面的效果1)RadioButton來實現動畫;Border嵌套 Ellipse并設置Sca…

對歸并排序進行c語言編程實現,歸并排序及C語言實現

排序系列之(1)歸并排序及C語言實現有很多算法在結構上是遞歸的:為了解決一個給定的問題,算法需要一次或多次遞歸的調用其本身來解決相關的問題。這些算法通常采用分治策略:將原問題劃分成n個規模較小而結構與原問題相似的子問題;遞…

Android之提示錯誤Can not perform this action after onSaveInstanceState

1 問題 主頁面3個Fragment,在第三個Fragment里面開啟了Activity之后,然后想跳到第一個Fragment代碼如下 /*** 展示Fragment*/private fun showFragment(fragment: Fragment) {if (currentFragment !== fragment) {val transaction: FragmentTransaction = supportFragmentMa…

《看聊天記錄都學不會C#?太菜了吧》(6)多晦澀的專業術語原來都會那么簡單

本系列文章將會以通俗易懂的對話方式進行教學,對話中將涵蓋了新手在學習中的一般問題。此系列將會持續更新,包括別的語言以及實戰都將使用對話的方式進行教學,基礎編程語言教學適用于零基礎小白,之后實戰課程也將會逐步更新。 若…

【Python可視化】利用Numpy繪制各種統計圖表

NumPy簡介 NumPy(Numerical Python) 是 Python 語言的一個擴展程序庫,支持大量的維度數組與矩陣運算,此外也針對數組運算提供大量的數學函數庫。 NumPy 的前身 Numeric 最早是由 Jim Hugunin 與其它協作者共同開發,2005 年,Travis Oliphant 在 Numeric 中結合了另一個同性質…

這個設計原則,你認同嗎?

前言我們都知道依賴注入的方式常見的主要有三種構造函數注入屬性注入接口注入在大名鼎鼎的Spring框架中大量使用屬性注入的方式,屬性注入的方式寫起來那是真的爽;而在Asp.NetCore中則不支持屬性注入,如果不使用第三方庫,我們就只能…

Android之提示Unable to instantiate fragment***MyLikeFragment .could not find Fragment constructor

1 問題 java.lang.RuntimeException: Unable to start activity ComponentInfo{com.appsinnova.android.keepdrop/com.appsinnova.android.keepdrop.account.favorite.activity.MyLikeActivity}: androidx.fragment.app.Fragment$InstantiationException: Unable to instantiat…

SQLServer2008-鏡像數據庫實施手冊(雙機)SQL-Server2014同樣適用

SQL Server2008R2-鏡像數據庫實施手冊(雙機)SQL Server2014同樣適用 一、配置主備機 1、 服務器基本信息 主機名稱為:HOST_A,IP地址為:192.168.1.155 備機名稱為:HOST_B,IP地址為:192.168.1.156 二、主備實…

一萬字一篇文20分鐘學會C語言和Python,十四年編程經驗老鳥傳授經驗之道

前言 昨天在直播中有粉絲問我如何快速的對編程語言入門,我想這個問題是有必要讓大家知道的,相必也有很多新手對于如何快速完成編程語言的入門學習很感興趣,本篇文將會使用 C 語言以及 Python 為例,做出對比,讓大家對編…

C語言基于dag的基本塊優化,基于dag的基本塊優化參考.docx

基于dag的基本塊優化參考基于DAG的基本塊優化1.實驗目的與任務了解基本塊的DAG表示及其應用,掌握局部優化的基本方法。2.實驗要求設計一個轉換程序,把由四元式序列表示的基本塊轉換為DAG,并在構造DAG的過程中&#xff…

【Python可視化】Windows 10系統上Pyecharts安裝教程

簡單的Python庫,如Numpy,可以直接在PyCharm中自動下載并安裝。 同添加Python環境變量一樣,需要先添加pip環境變量。pip位于C:\Python27\ArcGIS10.8\Scripts路徑下。 WinR→cmd: 安裝完成!

使用.Net分析.Net達人挑戰賽參與情況

背景C#是我2012年在大學課程中接觸的,.NET Framework 我也一直使用至今。從2014年.NET 開源,2019年發布.NET Core 3 的時候,公司剛好有 Nvidia Jetson 平臺 Linux 嵌入式設備的開發任務,.NET 又剛是適用于 Windows, Linux, 和 mac…

Android之RecycleView實現指定范圍的拖動效果

1 問題 在RecycleView里面實現指定位置的拖動效果,(這里是實現線性布局的,不是網格布局的) @Overridepublic boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder, RecyclerView.ViewHolder target) {Log.i(TAG, "onMove viewHolder.getA…