如何用 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.mzph.cn/news/287138.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《看聊天记录都学不会C#?太菜了吧》(5)C# 中可以用中文名变量?

本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题。此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将会逐步更新。 若…

【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#?太菜了吧》(6)多晦涩的专业术语原来都会那么简单

本系列文章将会以通俗易懂的对话方式进行教学,对话中将涵盖了新手在学习中的一般问题。此系列将会持续更新,包括别的语言以及实战都将使用对话的方式进行教学,基础编程语言教学适用于零基础小白,之后实战课程也将会逐步更新。 若…

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 为例,做出对比,让大家对编…

【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…

十分钟如何学会C语言?掌握规律举一反三考试提50分!

前言 上周写了一篇 20 分钟学会 C 语言与Python的文章——《一万字一篇文20分钟学会C语言和Python,十四年编程经验老鸟传授经验之道》,之后见粉丝转了一个话题“十分钟如何学会C语言”,我就在想是否能够十分钟呢?答案是可以的&am…

c语言在win8系统不兼容,Win8系统中存在不兼容软件如何解决?

最近有刚升级Win8系统的用户反映,FastStone Capture截图软件在Win7系统中可以兼容,正常打开,可是在Win8系统中就不能兼容了,这让用户非常烦恼。那么,Win8系统中存在不兼容软件如何解决呢?下面,我…

Python 3.6出现报错解决方案:No Python 3.6 installation was detected,无法卸载Python

卸载Python 3.6时错误提示,No Python 3.6 installation was detected。 解决办法是,先右键→更改→Repair。 然后再卸载,完成!

MASA Auth - 权限设计

权限术语Subject:用户,用户组Action:对Object的操作,如增删改查等Object:权限作用的对象,也可以理解为资源Effect:规则的作用,如允许,拒绝Condition:生效条件…

【必懂】C语言水仙花数题解

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我,若你是真心学习可以送你书籍,指导你学习,给予你目标方向的学习路线,无套路,博客为证。 前言 本专栏内容将会以轻松、简单的方式完成习题的解答,用…

【ArcGIS风暴】ArcGIS 10.8中计算体积的方法总结

ArcGIS 10.8提供了表面体积和面体积两种计算体积的方法。 一、表面体积 用途:用于计算表面和参考平面之间区域的面积和体积。 Situation 1:参考面以上 Situation 2:参考面以下 Python脚本: import arcpy from arcpy import envarcpy.CheckOutExtension("3D")…

.NET7:更细致的时间

当年在做go时,很羡慕它的时间有微秒,纳秒,在做性能优化时,能很小颗粒度的查看引入方法执行的时间,当时.net的DateTime只有毫秒(虽然也有别的办法获取)。现在,在最新的.NET7 Preview4…

案例:无人测量船水库水下地形测量及库容量计算

本文讲解利用南方方洲号无人船,该系统可用于水下地形地貌测绘、水库库容测量、水文勘测、疏浚检测、水环境监测等领域。 一、无人船水深测量 1、水岸线范围的获取 水岸线有助于布设航线,获取方式有两种: (1)无人船获取 对于

小米android系统耗电量大,小米手机耗电快的解决方法,亲测有效~

原标题:小米手机耗电快的解决方法,亲测有效~各位机友大家好,据小安观察,平台故障报修的小米手机用户还会蛮多的。那么今天就来讲讲小米手机的一些问题和解决方法,如果你感觉手机耗电较快,按照以下方式排查&…

【小白必懂】C语言最大、最小公约数题解

若是大一学子或者是真心想学习刚入门的小伙伴可以私聊我,若你是真心学习可以送你书籍,指导你学习,给予你目标方向的学习路线,无套路,博客为证。 前言 本专栏内容将会以轻松、简单的方式完成习题的解答,用…

如何为微服务选择正确的消息队列

微服务及消息队列简史自从 Peter Rodgers 博士 2005 年在 Web Services Edge 会议上首次提出 Micro-Web-Services 一词后,IT 行业慢慢地从单体架构转向了微服务。2009 年,Netflix 决定把其单体架构拆分为微服务。2010 年,Best Buy 开始把它们…