在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
本文译自:How To Make a Custom Control in Swift 用户界面控件是所有应用程序重要的组成部分之一。它们以图形组件的方式呈现给用户,用户可以通过它们与应用程序进行交互。苹果提供了一套控件,例如 然而,有时候你希望界面做得稍微的与众不同,那么此时苹果提供的这些控件就无法满足你的需求。 自定义控件,除了是自己构建二外,与苹果提供的,没什么差别。也就是说,自定义控件不存在于 UIKit 框架。自定义控件跟苹果提供的标准控件一样,应该是通用,并且多功能的。你也会发现,互联网上有一些积极的开发者乐意分享他们自定义的控件。 本文中,你将实现一个自己的 RangeSlider 自定义控件。这个控件是一个两端都可以滑动的,也就是说,你可以通过该控件获得最小值和最大值。你将会接触到这样一些概念:对现有控件的扩展,设计和实现 自定义控件的 API,甚至还能学到如何分享你的自定义控件到开发社区中。 注意:本文截稿时,我们还不会贴出关于 iOS 8 beta 版本的截图。所有文中涉及到的截图都是在iOS 8之前的版本中得到的,不过结果非常类似。 目录: 开始假设你在开发一个应用程序,该程序提供搜索商品价格列表。通过这个假象的应用程序允许用户对搜索结果进行过滤,以获得一定价格范围的商品。你可能会提供这样一个用户界面:两个 你可以通过创建一个 最好的办法是将构建一个新的尽可能通用的 UI 控件,这样就能在任意的合适场合中重用。这也是自定义控件的本质。 启动 Xcode, 最后,选择一个保存工程的地方并单击 首先,我们需要做出决定的就是创建自定义控件需要继承自哪个类,或者对哪个类进行扩展。 位了使自定义控件能够在应用程序中使用,你的类必须是 如果你注意观察苹果的 注意:iOS 中 UI 组件的完整类继承图,请看 UIKit Framework 参考。
在 Project Navigator 中右键单击 虽然编码非常让人愉悦,不过你可能也希望尽快看到自定义控件在屏幕中熏染出来的模样!在写自定义控件相关的任何代码之前,你应该先把这个控件添加到 view controller中,这样就可以实时观察控件的演进程度。 打开 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 设计相关的内容,现在只需要紧跟就行! 添加默认的控件属性打开 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、 这两种方法都有利有弊,下面来看看:
通过图片的方式来构建的控件,给使用控件的人提供了非常大的灵活度。开发者可以改变每一个像素,以及控件的详细外观,不过这需要非常熟练的图形设计技能 - 并且通过代码非常难以对控件做出修改。
使用 Core Graphics,可以把控件的所有特征都参数化,例如颜色、边框厚度和弧度 - 几乎每一个可视元素都通过绘制完成!这种方法运行开发者对控件做出任意调整,以适配相应的需求。 本文中,你将学到第二种技术 - 利用 Core Graphics 来熏染控件。 主要:有趣的时,苹果建议在他们提供的控件中使用图片。这可能是苹果知道每个控件的大小,他们不希望程序中出现太多的定制。也就是说,他们希望所有的应用程序,都具有相似的外观和体验。 打开 1 import QuartzCore 将下面的属性添加到 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 - 接下来就是控件默认的一些图形属性。 在 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 中,然后通过 最后, 接下来,override一下 1 override var frame: CGRect { 2 didSet { 3 updateLayerFrames() 4 } 5 }
当 frame 发生变化时,属性观察者会更新 layer frame。这一步是必须的,因为当控件初始化时,传入的 frame 并不是最终的 frame,就像 编译并运行程序,可以看到滑块初具形状!看起来,如下图所示: 还记得吗,红色是整个控件的背景色。蓝色是滑块的轨迹,绿色 thumb 是两个代表两端的值。 现在控件看起来有形状了,不过几乎所有的控件都提供了相关方法,让用户与之交互。 针对本文中的控件,用户必须能够通过拖拽 2 个 thumb 来设置控件的范围。你将处理这些交互,并通过控件更新 UI 和暴露的属性。 添加交互逻辑本文的交互逻辑需要存储那个 thumb 被拖拽了,并将效果反应到 UI 中。控件的 layer 是放置该逻辑的最佳位置。 跟之前一样,在 Xcode 中创建一个新的 用下面的代码替换掉 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,避免循环引用。 打开 1 let lowerThumbLayer = RangeSliderThumbLayer() 2 let upperThumbLayer = RangeSliderThumbLayer() 还是在 1 lowerThumbLayer.rangeSlider = self 2 upperThumbLayer.rangeSlider = self
上面的代码简单的将 layer 的 编译并运行程序,界面看起来没有什么变化。 现在你已经有了 slider 的thumb layer - RangeSliderThumbLayer,然后需要给控件添加拖拽 thumb 的功能。 添加触摸处理打开 1 var previousLocation = CGPoint() 这个属性用来跟踪记录用户的触摸位置。 那么你该如何来跟踪控件的各种触摸和 release 时间呢?
在自定义控件中,我们将 override 3 个 将下面的方法添加到 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 了,就继续跟踪触摸事件。 现在,有了初始的触摸事件,我们需要处理用户在屏幕上移动的事件了。 将下面的方法添加到 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 }
下面我们根据注释,来分析一下
至此,已经编写了移动滑块的代码 - 不过我们还要处理触摸和拖拽事件的结束。 将下面方法添加到 1 override func endTrackingWithTouch(touch: UITouch!, withEvent event: UIEvent!) { 2 lowerThumbLayer.highlighted = false 3 upperThumbLayer.highlighted = false 4 }
上面的代码简单的将两个 thumb 还原位 non-highlighted 状态。 编译并运行程序,尝试移动滑块!现在你应该可以移动 thumb 了。 你可能注意到当在移动滑块时,可以在控件之外的范围对其拖拽,然后手指回到控件内,也不会丢失跟踪。其实这在小屏幕的设备上,是非常重要的一个功能。 值改变的通知现在你已经有一个可以交互的控件了 - 用户可以对其进行操作,以设置范围的大小值。但是如何才能把这些值的改变通知调用者:控件有新的值了呢? 这里有多种模式可以实现值改变的通知: 面对这么多的通知方式,那么我们该怎么选择呢? 如果你研究过
这两种模式关键不同点如下:
我们的 slider 控件不会有大量的状态变化,也不需要提供大量的通知。唯一真正改变的就是控件的 upper 和 lower 值。 基于这样的情况,使用 target-action 模式是最好的。这也是为什么在本文开头的时候告诉你为什么这个控件要继承自 slider 的值是在 打开 1 sendActionsForControlEvents(.ValueChanged) 上面的这行代码就能将值改变事件通知给任意的订阅者 target。 现在我们应该对这个事件进行订阅,并当事件来了以后,作出相应的处理。 打开 1 rangeSlider.addTarget(self, action: "rangeSliderValueChanged:", forControlEvents: .ValueChanged) 通过上面的代码,每次 slider 发送 将下面的代码添加到 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 移动的轨迹图形。 跟之前一样,给工程添加另外一个继承自 打开刚刚添加的文件 1 import UIKit 2 import QuartzCore 3 4 class RangeSliderTrackLayer: CALayer { 5 weak var rangeSlider: RangeSlider? 6 }
上面的代码添加了一个到 slider 控件的引用,跟之前 thumb layer 做的一样。 打开 1 let trackLayer = RangeSliderTrackLayer() 接下来,找到 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 - 并没有再用那可怕的颜色了!然后将 下面还有一个事情需要做,就是将 1 rangeSlider.backgroundColor = UIColor.redColor() 编译并运行程序,看到什么了呢? 什么东西都没有?这是正确的! 不要烦恼 - 我们只不过移除掉了在 layer 中花哨的测试颜色。控件依旧存在 - 只不过现在是白色的! 由于许多开发者希望能够通过编码对控件做各种配置,以使其外观能够效仿一些流行的程序,所以我们给 slider 添加一些属性,运行开发者对其外观做出一些定制。 打开 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
这些颜色属性的目的非常容易理解,但是 接下来,打来 这个 layer 用来渲染两个 thumb 滑动的轨迹。目前它继承自 为了绘制轨迹,需要实现方法 注意:要想深入学习 Core Graphics,建议阅读 Core Graphics 101 教程。 将下面这个方法添加到 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 被完美的渲染出来!如下图所示: 给暴露出来的属性设置不同的值,观察一下它们是如何反应到控件渲染中的。 如果你对 接下来我们使用相同的方法来绘制 thumb layer。 打开 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 是高亮的 - 也就是被移动状态 - 那么就绘制微弱的灰色阴影效果。 在运行之前,还有最后一件事情要做。按照下面的代码对 1 var highlighted: Bool = false { 2 didSet { 3 setNeedsDisplay() 4 } 5 } 这里,定义了一个属性观察者,这样当每次 highlighted 属性修改时,相应的 layer 都会得到重绘。这会使得触摸事件发生时,填充色发生轻微的变动。 再次编译并运行程序,这下看起来会非常的有形状,如下图所示: 不难发现,用 Core Graphics 来绘制控件是非常值得做的。使用 Core Graphics 可以做出比通过图片渲染方法更通用的控件。 处理控件属性的改变那么到现在,还有什么事情要做呢?控件现在看起来已经非常的华丽了,它的外观是通用的,并且也支持 target-action 通知。 貌似已经做完了? 思考一下,如果当控件熏染之后,如果通过代码对 slider 的属性做了修改,会发生什么?例如,你希望修改一下 slider 的默认值,或者修改一下 track highlight,表示出一个有效范围。 目前,还没有任何代码来观察属性的设置情况。我们需要将其添加到控件中。我们需要实现属性观察者,来更新控件的 frame 或者重绘控件。打开 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 }
一般情况,我们需要根据依赖的属性,调用 现在,找到 1 CATransaction.begin() 2 CATransaction.setDisableActions(true) 并将下面的代码添加到方法的尾部: 1 CATransaction.commit() 上面的代码将整个 frame 的更新封装到一个事物处理中,这样可以让界面重绘变得流畅。同样还明确的把 layer 中的动画禁用掉,跟之前一样,这样 layer frame 的更新会变得即时。 由于现在每当 upper 和 lower 值发生变动时, frame 会自动更新了,所以,找到 1 // 3. Update the UI 2 C |
请发表评论