更新通知:这篇引导教程由Mikael Konutgan使用iOS 8和Swift语言重新制作,在Xcode6和7上测试通过。原始教程是由Colin Eberhardt团队制作的。
用户界面控件是许多应用的重要组成部分。使用这些控件,可以让用户查看应用的内容或与他们的应用进行交互。苹果提供了一个控件集,像UITextField, UIButton 和 UISwitch。灵活使用这些工具箱中已经存在的控件,可以让你创建各种各样的用户界面。
但是,有的时候你可能需要做一些与众不同的事情;库中的控件已经不够用了。
自定义控件只不过是你自己创建的控件;也就是一个不来自于UIKit框架的控件。自定义控件,就像标准控件一样,应该具有通用和可定制的特性。而且,你会找到一个积极且充满活力的开发者社区喜欢分享他们的自定义控件。
在这个教程里,你将实现你自己的RangeSlider控件。这个控件就像一个双头滑块,可以用来选择最小和最大值。你会接触到关于如何扩展已经存在控件的思想,然后设计和实现你的控件API,甚至在开发者社区中共享你的控件。
开始我们的自定义之旅吧!
注意:在我们制作教程时,iOS 8仍处於测试阶段,所以我们不能给他截图。所有的截图都取自之前的iOS版本,但你的实际运行结果是没有差别的。
开始的工作
话说你开发了一个可以搜索房地产销售信息的应用。这个应用允许用户过滤搜索结果,来将搜索结果控制在一定的价格范围内。 你可以提供一个呈现出一对UISlider控件的交互界面,一个设置最低价格,另一个设置最高价格。尽管如此,这个交互界面不能真正帮助使用者形象的了解价格范围。如果设计成一个滑动条和连个滑动钮,这样可以更好的表达出他们想要搜索的价格高低范围。 你可以子类化UIView来创建一个范围滑动器,并且创建一个定制的视图来显示价格区间。这对于你的App是好用的,但如果将他用在其他类型的App上那就是一种煎熬。
让这个新的自定义组件变的更通用将会是个好的想法,这样他就可以在任何需要他的环境内使用。这是自定义控件的精髓所在。
启动Xcode。选择File/New/Project,然后选择iOS/Application/Single View Application模板,点击Next。在接下来的屏幕里,在product name中输入CustomSliderExample,选择你想使用的Organization Name和Organization Identifier,确定Swift语言被选中,iPhone被选为Device,Use Core Data没有被选中。
最后选择一个保存路径,然后点击Create。
我们要做的第一个决策就是通过对一个已经存在的控件进行继承或扩展来实现新的控件。
为了在现有的UI上使用,你的控件必须继承于UIView。
如果你查看苹果的UIKit参考手册,你会看到许多的控件像 UILabel 和 UIWebView 是直接继承于UIView的。尽管如此,还是有一些棘手的事情,像 UIButton 和UISwitch 是继承于 UIControl ,像如下的层级关系所示:
注意:如果你想查看一个完整的UI组件类层级示意图,请阅读UIKit Framework Reference。
UIControl 使用的是target-action pattern机制,这是一种用于通知用户信息改变的机制。 UIControl 也具有很多的属性来表示当前的控制状态。在这个自定义控件中将使用target-action pattern,所以 UIControl 将担当重要的起始点。
在项目导航中右击CustomSliderExample组然后选择New File…,选择iOS/Source/Cocoa Touch Class模板点击Next。类取名为RangeSlider,在Subclass of中输入 UIControl 并且确保语言选择为Swift。点击下一步然后选择Create以使用默认位置来保存新类。
尽管写代码是件漂亮的事,你可能想看看你的控件在实际屏幕上显示的效果来了解项目的进展!在你写其他代码之前,你可以先将控件添加到view controller中以便我们随时查看控件制作的进展程度。
打开ViewController.swift替换如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import UIKitclass
ViewController: UIViewController {
let rangeSlider = RangeSlider(frame: CGRectZero)
override func viewDidLoad() {
super
.viewDidLoad()
rangeSlider.backgroundColor = UIColor.redColor()
view.addSubview(rangeSlider)
}
override func viewDidLayoutSubviews() {
let margin: CGFloat = 20.0
let width = view.bounds.width - 2.0 * margin
rangeSlider.frame = CGRect(x: margin, y: margin + topLayoutGuide.length,
width: width, height: 31.0)
}}
|
上面的代码简单的创建了一个指定过大小的全新控件的实例并且将实例添加到了视图内。控件的背景颜色已经被设置成红色这样可以使其和应用的背景形成鲜明的对比。如果你不将背景设置成红色,你的控件将不容易被找到,你将怀疑你的控件去哪了!:]
运行app;你将看到和下图一样的画面:
在你向控件中添加可视元素之前,你需要几个属性,以便跟踪控件当前所设置的信息。这将形成你的控件的应用程序接口,简称API。
注意:你的控件的API定义了你打算开放给其他开发人员的方法和属性。你将在这篇文章的稍后学习一些API设计技巧——所以,现在就应该注意这些!
为控件添加默认的属性
打开RangeSlider.swift替换如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
import UIKitclass
RangeSlider: UIControl {
var
minimumValue = 0.0
var
maximumValue = 1.0
var
lowerValue = 0.2
var
upperValue = 0.8}
|
这四个属性足够你描述控件的状态了,最大最小值用于表示范围,这些值由上限下限值进行调整。
设计好的控件需要一些默认的属性值,否则你的控件会看起来有一些怪!如果你照做的话会好很多。
现在我们可以设计控件上的交互元素了;也就是说,滑块以表示最大最小值,并且可以在滑条上滑动。
Images vs. CoreGraphics
你可以使用以下两种方式来在屏幕上绘制你的控件:
Images – 为控件创建图片来表示多种多样的形式
CoreGraphics – 使用层和CoreGraphics混合使用来渲染控件
每个技术都有优缺点,如下所示:
Images — 使用图片来创建控件可能是最容易做到的事 – 只要你知道如何绘制!:]如果你需要让你的开发队友能够修改控件的外观,你只需要将图片设为 UIImage 的开放属性就可以了。
使用图片可以使开发人员更灵活的使用控件。开发这可以修改每一个像素和没一个你想要表现的细节,但这需要很好的平面设计技能 – 因为在代码中修改图片很困难。
Core Graphics — 使用Core Graphics构建以为这你需要自己编写渲染代码,这需要花费更多的努力。但是,这种技术允许你创建更加灵活的API。
使用Core Graphics,你可以用参数表示控件的每一个特征,不如颜色,边框宽度,和弯曲度 – 几乎所有的视觉元素都可以直接绘制!这种方法允许开发人员对控件进行完整的定制以适应他们的需要。
这个教程将使用第二种技术 – 使用Core Graphics进行渲染。
注意:有趣的是,苹果公司倾向于在控件中直接使用图片。这可能是因为他们知道控件的实际大小,且不需要做太多的自定义修改。毕竟,他们希望的结果是所有的应用看起来都差不多。
打开RangeSlider.swift在 import UIKit 的下面加入以下代码:
1
|
import QuartzCore
|
在我们刚才添加代码的下部添加如下代码:
1
2
3
4
5
6
7
8
9
10
11
|
let trackLayer = CALayer()
let lowerThumbLayer = CALayer()
let upperThumbLayer = CALayer()
var
thumbWidth: CGFloat {
return
CGFloat(bounds.height)
}
|
trackLayer, lowerThumbLayer, 和 upperThumbLayer 这三个层将用于滑条控件的渲染。 thumbWidth 将在布局属性中被使用。
接下来是添加一些控件自身的默认属性。
在RangeSlider类中,添加一个初始化方法和其他的辅助方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
override init(frame: CGRect) {
super
.init(frame: frame)
trackLayer.backgroundColor = UIColor.blueColor().CGColor
layer.addSublayer(trackLayer)
lowerThumbLayer.backgroundColor = UIColor.greenColor().CGColor
layer.addSublayer(lowerThumbLayer)
upperThumbLayer.backgroundColor = UIColor.greenColor().CGColor
layer.addSublayer(upperThumbLayer)
updateLayerFrames()
}
required init(coder: NSCoder) {
super
.init(coder: coder)
}
func updateLayerFrames() {
trackLayer.frame = bounds.rectByInsetting(dx: 0.0, dy: bounds.height / 3)
trackLayer.setNeedsDisplay()
let lowerThumbCenter = CGFloat(positionForValue(lowerValue))
lowerThumbLayer.frame = CGRect(x: lowerThumbCenter - thumbWidth / 2.0, y: 0.0,
width: thumbWidth, height: thumbWidth)
lowerThumbLayer.setNeedsDisplay()
let upperThumbCenter = CGFloat(positionForValue(upperValue))
upperThumbLayer.frame = CGRect(x: upperThumbCenter - thumbWidth / 2.0, y: 0.0,
width: thumbWidth, height: thumbWidth)
upperThumbLayer.setNeedsDisplay()
}
func positionForValue(value: Double) -> Double {
let widthDouble = Double(thumbWidth)
return
Double(bounds.width - thumbWidth) * (value - minimumValue) /
(maximumValue - minimumValue) + Double(thumbWidth / 2.0)
}
|
初始化方法简单的创建了三个层,然后作为子层添加到控件层中,然后 updateLayerFrames ,这样,层的布局就能自动选择合适的大小了!:]
最后,positionForValue 根据一个值计算出控件在屏幕中的位置,并且使用一个简单的比例缩放最大最小值,计算控件中的位置。
接下来,重写frame方法,实现一个观察者属性,添加到RangeSlider.swift后面:
1
2
3
4
5
6
7
8
|
override
var
frame: CGRect {
didSet {
updateLayerFrames()
}
}
|
这个观察者会在布局发生改变时更新层的布局位置。这是必须的,但就像在ViewController.swift中一样,在初始化中被执行,但这并不是最终的布局。
运行应用;你的滑动条已经成形了!他看起来应该如下所示:
记住,红色的是整个控件的背景。蓝色是轨道,绿色是两个滑动的钮,对应最大最小值。
你的控件已经可以显示了,但几乎苹果上所有的控件都可以和用户进行交互。
对于你的控件,用户应该可以拖拽两个滑块来调解范围。你将控制他们进行交互,并且开放更新UI和更新属性的接口。
添加交互逻辑
交互必须储存滑块拖动的位置,并在UI中显示出来。控件层是最适合放这些逻辑的地方。
像以前一样,在Xcode中创建一个新Cocoa Touch Class,将他命名为 RangeSliderThumbLayer ,继承于 CALayer 。
将以下代码添加到RangeSliderThumbLayer.swift中:
1
2
3
4
5
6
7
8
9
|
import UIKit
import QuartzCoreclass
RangeSliderThumbLayer: CALayer {
var
highlighted =
false
weak
var
rangeSlider: RangeSlider?
}
|
这里添加了两个属性:一个用于表明滑块是否高亮显示,另一个是父控件的引用。自从RangeSlider有两个滑块层,他们需要持有两个背景控件的弱引用,以避免被内存管理系统回收。
打开RangeSlider.swift修改 lowerThumbLayer 和 upperThumbLayer 属性的类型,替换成如下代码。
1
2
3
|
let lowerThumbLayer = RangeSliderThumbLayer()
let upperThumbLayer = RangeSliderThumbLayer()
|
还是在RangeSlider.swift,找到 init 添加如下代码:
1
2
3
|
lowerThumbLayer.rangeSlider = self
upperThumbLayer.rangeSlider = self
|
上面的代码设置了自己的父节点弱引用 rangeSlider 属性。
运行项目;检查应用是否按预想的方式执行。
现在你已经使用 RangeSliderThumbLayer 创建了滑块层,你需要添加让用户可以拖动滑块移动的功能。
添加触摸控制
打开RangeSlider.swift在属性区域添加如下属性:
1
|
var
previousLocation = CGPoint()
|
这个属性将被用于追踪触摸位置。
如何跟踪变化的触摸点和释放事件?
UIControl 提供了跟踪触摸点的多种方法。继承于 UIControl 的类可以重写这些方法并在里面增加自己的交互代码。
在你的自定义控件中,你将重写三个方法 UIControl: beginTrackingWithTouch, continueTrackingWithTouch 和 endTrackingWithTouch 。
添加如下方法在RangeSlider.swift:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
override func beginTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
previousLocation = touch.locationInView(self)
// Hit test the thumb layers
if
lowerThumbLayer.frame.contains(previousLocation) {
lowerThumbLayer.highlighted =
true
}
else
if
upperThumbLayer.frame.contains(previousLocation) {
upperThumbLayer.highlighted =
true
}
return
lowerThumbLayer.highlighted || upperThumbLayer.highlighted
}
|
这个方法将在用户第一次触摸屏幕时被调用。
首先,他将触摸事件转换为控件的坐标系。接下来,他将检查两个滑块已查看时候触摸到了滑块上。上面方法的返回值通知 UIControl 父类哪个后来的触点需要被跟踪。
如果滑块是高亮的,则跟踪轨迹事件。
现在你有一个初始的触摸事件,你将需要在手指在屏幕上移动是做一些处理。
添加如下方法到RangeSlider.swift:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
|
func boundValue(value: Double, toLowerValue lowerValue: Double, upperValue: Double) -> Double {
return
min(max(value, lowerValue), upperValue)}override func continueTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) -> Bool {
let location = touch.locationInView(self)
// 1. Determine by how much the user has dragged
let deltaLocation = Double(location.x - previousLocation.x)
let deltaValue = (maximumValue - minimumValue) * deltaLocation / Double(bounds.width - bounds.height)
previousLocation = location
// 2. Update the values
if
lowerThumbLayer.highlighted {
lowerValue += deltaValue
lowerValue = boundValue(lowerValue, toLowerValue: minimumValue, upperValue: upperValue)
}
else
if
upperThumbLayer.highlighted {
upperValue += deltaValue
upperValue = boundValue(upperValue, toLowerValue: lowerValue, upperValue: maximumValue)
}
// 3. Update the UI
CATransaction.begin()
CATransaction.setDisableActions(
true
)
updateLayerFrames()
CATransaction.commit()
return
true
}
|
boundValue 会将值限制在一定范围内。使用这个助手方法比读一大堆 min / max 方便多了。
这里是 continueTrackingWithTouch 的几个关键点,这里是详细注释:
首先你计算一个位置的变化量,计算用户的手指移动的像素。然后你依据控件的最大最小值来缩放你的移动量。
这里通过手指一动哪个滑块确定改变最大值还是最小值。
这个部分设置了 CATransaction 类的 disabledActions 标志。这将确保改变立刻生效,而不使用动画。最后调用 updateLayerFrames 设置滑钮到当前位置。
你已经编写了移动滑钮的代码 – 但你还需要编写触摸和拖拽的处理事件。
添加如下方法到RangeSlider.swift:
1
2
3
4
5
6
|
override func endTrackingWithTouch(touch: UITouch, withEvent event: UIEvent) {
lowerThumbLayer.highlighted =
false
upperThumbLayer.highlighted =
false
}
|
上面的代码将恢复滑钮到非高亮显示的状态。
运行项目,然后试用你闪亮的滑动条!你应该可以随意的拽动绿色的按钮。
请注意,在你拖动滑条时,你可以将手指拖动到空间范围以外的地方,然后返回控件位置依然不会失去对按钮的控制。这是一个对小屏低分辨率设备尤为重要的特性 – 或者说对于广大手指们来说!:)
改变通知消息
现在你已经有了一个用户可以控制上界和下界的用户交互控件。但你如何将这些更改通知给应用,以让应用知道新设置的值?
有很多模式可以实现发出修改消息:NSNotification, Key-Value-Observing (KVO),委托模式,目标操作模式和其他。有很多的选择!
怎么做?
如果你看过UIKit的控件,你将发现他们都不使用 NSNotification 也不鼓励使用KVO,所以为了能和UIKit达到一致性你应该避免使用这两个方法。另外两种方法委托和目标则在UIKit中广泛使用。
以下是对委托和目标的详细描述:
委托模式 – 委托者模式中你将提供一个包含多个方法的协议用于一定范围内的消息通知。控件有一个属性,通常命名为 delegate ,用于接受使用这种协议的类。一个经典的例子是UITableView使用了 UITableViewDelegate 协议。注意这些控件都值接受一种协议实例。一个委托方法有许多参数,所以你可以使用方法传递尽可能多的信息。
目标操作模式 – 目标操作模式是 UIControl 的基类。当一个控件的状态发生改变时,将用 UIControlEvents 的枚举值提醒目标发生了什么动作。你可以给一个控件提供多个目,可以创建自定义事件(具体请查看 UIControlEventApplicationReserved ),自定义事件的数量限定为4个。控件多做不能在事件中传递更多的信息。所以他不能处理包含巨量信息的事件。
下面是关于两个模式的不同点:
多路广播 – 目标操作模式可以实现多路通知,而委托模式只限制在一个委托实例上。
灵活性 – 你在委托模式中定义了自己的协议,以为着你可以通过它控制巨量的信息。目标操作不能提供额外的信息,客户端只能在接受到事件后自查。
你的范围滑条中提供的消息中是没有大量的状态信息的。消息中其实仅有最大和最小值。
在这种情况下,目标操作模式完美的做到了这一点。这就是为什么在教程的一开始要求你继承UIControl的原因之一!
啊哈!现在感觉不错!:]
滑条值的更新在 continueTrackingWithTouch:withEvent: ,所以这里也是你添加通知代码的地方。
打开RangeSlider.swift,定位到 continueTrackingWithTouch ,在“ return true ”前面添加:
1
|
sendActionsForControlEvents(.ValueChanged)
|
这样做你就完全可以响应定制目标的改变!
现在你已经有你的消息处理了,你现在应该将他挂载到你的应用中。
打开ViewController.swift在 viewDidLoad 尾部添加如下代码:
1
|
rangeSlider.addTarget(self, action:
"rangeSliderValueChanged:"
, forControlEvents: .ValueChanged)
|
上面的代码将在滑条每次发送 UIControlEventValueChanged 动作时调用 rangeSliderValueChanged 方法。
现在在ViewController.swift中添加如下代码:
1
2
3
4
|
func rangeSliderValueChanged(rangeSlider: RangeSlider) {
println(
"Range slider value changed: (\(rangeSlider.lowerValue) \(rangeSlider.upperValue))"
)
}
|
这段代码简单的将滑块数值的改变发送到控制台输出上。
运行应用,左右移动滑条。你将在控制台上看到空间输出的值,截图如下:
1
2
3
4
5
6
7
8
9
10
11
|
Range slider value changed: (0.117670682730924 0.390361445783134)
Range slider value changed: (0.117670682730924 0.38835341365462)
Range slider value changed: (0.117670682730924 0.382329317269078)
Range slider value changed: (0.117670682730924 0.380321285140564)
Range slider value changed: (0.119678714859438 0.380321285140564)
Range slider value changed: (0.121686746987952 0.380321285140564)
|
估计你已经看够了由多种颜色拼成的滑条的UI。他看起来就像一个愤怒的水果沙拉!
是时候给控件整整容了!
使用Core Graphics修改你的控件
首先,首先你需要更新滑钮滑动的轨道。
增加另一个以CALayer为父类的类像之前一样,这次起名为 RangeSliderTrackLayer 。
打开这个类,将内容替换为如下内容:
1
2
3
4
|
import UIKitimport QuartzCoreclass RangeSliderTrackLayer: CALayer {
weak
var
rangeSlider: RangeSlider?
}
|
代码添加了一个指向背景的弱引用,就像之前的滑钮层一样。
打开 RangeSlider.swift,定位到 trackLayer 属性并修改为一个新的类,如下:
1
|
let trackLayer = RangeSliderTrackLayer()
|
现在寻找init方法,并替换如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
init(frame: CGRect) {
super
.init(frame: frame)
trackLayer.rangeSlider = self
trackLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(trackLayer)
lowerThumbLayer.rangeSlider = self
lowerThumbLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(lowerThumbLayer)
upperThumbLayer.rangeSlider = self
upperThumbLayer.contentsScale = UIScreen.mainScreen().scale
layer.addSublayer(upperThumbLayer)
}
|
代码确保了滑钮从滑道的引用添加正确 – 之前可怕的背景颜色不再被使用了。:]设置 contentsScale 工厂匹配设备将确保在视网膜屏幕上也能按正确的比例显示。
这项的内容有点多一些 – 在控件中删除红色的背景。
打开ViewController.swift,定位到viewDidLoad方法中的如下位置然后删除它:
1
|
rangeSlider.backgroundColor = UIColor.redColor()
|
运行。。。你会看到什么呢?
你什么都没看到?那就对了!
很好?这有什么好的?之前所做的一切都白费了?!?!
别着急 – 你刚才删除了层上用于测试的颜色。你的控件一直都在这里 – 但是你的控件使用的是一个空白的画布!
因为大多数开发者喜欢可以定制控件的显示风格,所以你需要在滑条里添加一些属性以实现可以改变视觉效果的控件。
打开RangeSlider.swift,将下面的内容添加到你之前添加代码的位置:
1
2
3
4
5
6
7
|
var
trackTintColor = UIColor(white: 0.9, alpha: 1.0)
var
trackHighlightTintColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0)
var
thumbTintColor = UIColor.whiteColor()
var
curvaceousness : CGFloat = 1.0
|
使用可变颜色的目的是非常简单的。还有曲线?好的,这一点就有趣了 – 你很快就会了解他!:]
下一步,打开RangeSliderTrackLayer.swift。
这个层是用于渲染两个滑钮下面的滑道的。他继承于 CALayer ,他只是渲染一个固定的颜色。
为了绘制这条轨迹,你需要实现 drawInContext :并且使用Core Graphics APIs来执行渲染。
注意:想深入学习Core Graphics的只是,强烈推荐学习Core Graphics 101系列引导教程的内容,这里如果讨论关于Core Graphics的话题就超出了本教程的范围了。
在 RangeSliderTrackLayer 添加如下方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
|
override func drawInContext(ctx: CGContext!) {
if
let slider = rangeSlider {
// Clip
let cornerRadius = bounds.height * slider.curvaceousness / 2.0
let path = UIBezierPath(roundedRect: bounds, cornerRadius: cornerRadius)
CGContextAddPath(ctx, path.CGPath)
// Fill the track
CGContextSetFillColorWithColor(ctx, slider.trackTintColor.CGColor)
CGContextAddPath(ctx, path.CGPath)
CGContextFillPath(ctx)
// Fill the highlighted range
CGContextSetFillColorWithColor(ctx, slider.trackHighlightTintColor.CGColor)
let lowerValuePosition = CGFloat(slider.positionForValue(slider.lowerValue))
let upperValuePosition = CGFloat(slider.positionForValue(slider.upperValue))
let rect = CGRect(x: lowerValuePosition, y: 0.0, width: upperValuePosition - lowerValuePosition, height: bounds.height)
CGContextFillRect(ctx, rect)
}
}
|
一旦轨道的轮廓被裁减,背景就会填充上。之后高亮的部分也被填充。
运行一下,看看你的新的轨道,很自豪吧!他看起来应该如下所示:
尝试着改变可以变化的值看看会对渲染产生什么影响。
如果你一直怀疑 curvaceousness 是做什么的,尝试改变他试一下!
你将使用相同的方式绘制按钮层。
打开RangeSliderThumbLayer.swift在属性声明部分的下部添加如下内容:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
|
override func drawInContext(ctx: CGContext!) {
if
let slider = rangeSlider {
let thumbFrame = bounds.rectByInsetting(dx: 2.0, dy: 2.0)
let cornerRadius = thumbFrame.height * slider.curvaceousness / 2.0
let thumbPath = UIBezierPath(roundedRect: thumbFrame, cornerRadius: cornerRadius)
// Fill - with a subtle shadow
let shadowColor = UIColor.grayColor()
CGContextSetShadowWithColor(ctx, CGSize(width: 0.0, height: 1.0), 1.0, shadowColor.CGColor)
CGContextSetFillColorWithColor(ctx, slider.thumbTintColor.CGColor)
CGContextAddPath(ctx, thumbPath.CGPath)
CGContextFillPath(ctx)
// Outline
CGContextSetStrokeColorWithColor(ctx, shadowColor.CGColor)
CGContextSetLineWidth(ctx, 0.5)
CGContextAddPath(ctx, thumbPath.CGPath)
CGContextStrokePath(ctx)
if
highlighted {
CGContextSetFillColorWithColor(ctx, UIColor(white: 0.0, alpha: 0.1).CGColor)
CGContextAddPath(ctx, thumbPath.CGPath)
CGContextFillPath(ctx)
}
}
}
|
一旦路径定义成按钮的轮廓,轮廓将被填充。注意看那细微的影子,这给人一种按钮在轨道上徘徊的立体感。边界之后被渲染。最后,如果按钮高亮 – 正打算移动 – 在他的附近就会出现一个微妙的阴影。
最后的事情是在运行前。将 highlighted 属性改为如下所示:
1
2
3
4
5
6
7
8
|
var
highlighted: Bool =
false
{
didSet {
setNeedsDisplay()
}
}
|
这里,你定义了一个属性观察者,这样在高亮这个属性改变时层会被重绘。这样在触摸事件激活时颜色会发生些微的改变。
再次运行;你将会看到漂亮的轮廓,和下面的截图类似:
你可以轻易的发现使用Core Graphics渲染你的控件产生了很多额外的效果,这是非常值得的。使用Core Graphics可以比用图片渲染更轻易的进行控制。
操控控件属性的改变
现在还剩什么? 现在的控件看起来华丽而俗气,视觉效果可以改变,并且支持目标操作模式的消息通知。
看起来已经完成了 – 或者没完成?
想一下在渲染完成后,滑条属性会被设置成什么。举例来说,如果你想通过设置属性来改变滑条的显示范围,或者通过设置轨迹高亮来确定一个可用的范围。
目前没有进行属性的观察者设置。你需要在控件中添加如下功能。这里你需要实现观察属性,以实现更新控件的轮廓或重新绘制。打开RangeSlider.swift并且用下面的代码改变属性描述:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
|
var
minimumValue: Double = 0.0 {
didSet {
updateLayerFrames()
}
}
var
maximumValue: Double = 1.0 {
didSet {
updateLayerFrames()
}
}
var
lowerValue: Double = 0.2 {
didSet {
updateLayerFrames()
}
}
var
upperValue: Double = 0.8 {
didSet {
updateLayerFrames()
}
}
var
trackTintColor: UIColor = UIColor(white: 0.9, alpha: 1.0) {
didSet {
trackLayer.setNeedsDisplay()
}
}
var
trackHighlightTintColor: UIColor = UIColor(red: 0.0, green: 0.45, blue: 0.94, alpha: 1.0) {
didSet {
trackLayer.setNeedsDisplay()
}
}
var
thumbTintColor: UIColor = UIColor.whiteColor() {
didSet {
lowerThumbLayer.setNeedsDisplay()
upperThumbLayer.setNeedsDisplay()
}
}
var
curvaceousness: CGFloat = 1.0 {
didSet {
|