版本记录
版本号 | 时间 |
---|---|
V1.0 | 2019.02.14 星期四 |
前言
开始
首先看下写作环境
Swift 4.2, iOS 12, Xcode 10
Core Graphics是iOS开发人员容易避免的框架之一。 它有点模糊,语法不是超现代的,Apple并没有像WWDC那样给予它尽可能多的爱! 另外,只需使用图像即可轻松避免使用它。
但是,Core Graphics是一个非常强大的工具! 你可以摆脱平面设计师的束缚,使用强大的CG剑自己创造出令人惊叹的UI美。
在本教程中,您将学习如何仅使用Core Graphics
创建可自定义,可重复使用的光泽按钮。 难道你没有听说过skeuomorphism
的风格吗?
在此过程中,您将学习如何绘制圆角矩形,如何轻松地为Core Graphics绘图着色以及如何创建渐变和光泽效果。
自定义UIButton有很多选项,从完整的自定义UIButton
类到较小的扩展。 但是在这次讨论中缺少的是一个详细的核心图形教程,用于自定义按钮,从头到尾。 这很简单;你可以使用它来获得你想要的应用程序的确切外观。
现在是时候开始吧!
打开启动项目:CoolButton Starter
。
项目结构非常简单,仅包含选择Single View App Xcode
模板时创建的文件。 唯一的两个major exceptions
是:
-
Assets.xcassets
目录中的几个图像。 - 您可以在
Main.storyboard
中找到ViewController
的预先设计的UI。
现在转到File ▸ New ▸ File…
,选择iOS▸CocoaTouch Class
,然后单击Next
。
在下一个菜单中,输入CoolButton
作为类名。 在子类字段中,键入UIButton
。 对于语言,请选择Swift
。 现在单击Next
,然后单击Create
。
打开CoolButton.swift
并用以下内容替换类定义:
class CoolButton: UIButton {
var hue: CGFloat {
didSet {
setNeedsDisplay()
}
}
var saturation: CGFloat {
didSet {
setNeedsDisplay()
}
}
var brightness: CGFloat {
didSet {
setNeedsDisplay()
}
}
}
在这里,您将创建三个属性,用于自定义颜色的色调,饱和度和亮度(hue, saturation and brightness)
。
设置属性后,将触发对setNeedsDisplay
的调用,以强制UIButton
在用户更改颜色时重绘按钮。
现在,将以下代码粘贴到CoolButton
的底部,就在最后一个花括号之前:
required init?(coder aDecoder: NSCoder) {
self.hue = 0.5
self.saturation = 0.5
self.brightness = 0.5
super.init(coder: aDecoder)
self.isOpaque = false
self.backgroundColor = .clear
}
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
let color = UIColor(hue: hue,
saturation: saturation,
brightness: brightness,
alpha: 1.0)
context.setFillColor(color.cgColor)
context.fill(bounds)
}
在这里,您初始化变量并使用预先配置的颜色填充按钮,以确保一切从一开始就正常工作。
Configuring the Button’s UI
打开Main.storyboard
以配置UI。 有一个视图控制器包含三个滑块 - 一个用于色调,一个用于饱和度,一个用于亮度。
您错过了UIButton
,因此请将其添加到屏幕顶部。 转到Object Library
,键入UIButton
,然后将其拖到屏幕中。
一些自动布局的时间到了! 按住Ctrl键并从按钮拖动到视图,然后在“安全区域”中选择Center Horizontally
。
按住Ctrl键并从按钮拖动到hue label
,然后选择Vertical Spacing
。
现在,将按钮的大小调整为您喜欢的大小。 按住Ctrl键并从按钮向左或向右拖动,然后选择Width
。
按住control键,从按钮向上或向下拖动并选择Height
。
注意:如果通过拖动设置宽度和高度约束有困难,请选择按钮并单击画布右下角的
Add New Contraints
按钮。 它看起来有点像星球大战中的领带战士。
接下来,通过双击并按Delete键删除显示Button
的文本。
仍然选中该按钮,转到屏幕右侧的Inspectors
侧栏,然后单击Identity inspector
。 在Custom Class ▸ Class
中,输入CoolButton
以使您的按钮成为CoolButton
类的实例。
按钮就位后,您就可以开始将UI连接到代码了!
Making Your Button Functional
打开ViewController.swift
并使用以下内容替换类定义:
class ViewController: UIViewController {
@IBOutlet weak var coolButton: CoolButton!
@IBAction func hueValueChanged(_ sender: Any) { }
@IBAction func saturationValueChanged(_ sender: Any) { }
@IBAction func brightnessValueChanged(_ sender: Any) { }
}
在这里,您将声明对刚刚在故事板中创建的按钮的引用。 您还要声明配置滑块的值更改时发生的回调。
现在,再次打开Main.storyboard
并在顶部栏中选择Assistant Editor
以并排显示ViewController.swift
和Main.storyboard
。
按住control键从View Controller Scene ▸ ViewController
将按钮拖动到左侧并将其连接到coolButton outlet
。
类似的,按住control键从View Controller Scene ▸ ViewController
拖出slider,并选择appropriate value-changed
回调。
接下来,切换到ViewController.swift
并实现滑块的值更改回调:
@IBAction func hueValueChanged(_ sender: Any) {
guard let slider = sender as? UISlider else { return }
coolButton.hue = CGFloat(slider.value)
}
@IBAction func saturationValueChanged(_ sender: Any) {
guard let slider = sender as? UISlider else { return }
coolButton.saturation = CGFloat(slider.value)
}
@IBAction func brightnessValueChanged(_ sender: Any) {
guard let slider = sender as? UISlider else { return }
coolButton.brightness = CGFloat(slider.value)
}
默认情况下,UISliders
的范围为0.0到1.0。 这非常适合您的色调,饱和度和亮度值,范围也从0.0到1.0,因此您可以直接设置它们。
经过所有这些工作,它终于建立并运行了! 如果一切正常,您可以使用滑块来填充各种颜色的按钮:
Drawing Rounded Rectangles
确实,您可以轻松创建方形按钮,但按钮样式比芝加哥的天气变化更快!
事实上,由于我们最初发布本教程,方形按钮和圆角矩形按钮在按钮选美中的第一位来回翻转,所以知道如何制作这两个版本是个好主意。
您还可以说,通过简单地改变UIView
的corner radius
来创建圆角矩形非常容易,但那里的乐趣在哪里?用这种方式做得更加满足,或者说是疯狂。
制作圆角矩形的一种方法是使用CGContextAddArc API
绘制弧。使用该API,您可以在每个角落绘制弧线并绘制线条以连接它们。但这很麻烦,需要很多几何形状。
幸运的是,有一种更简单的方法!你不需要做太多的数学运算,它可以很好地绘制圆角矩形。这是CGContextAddArcToPoint API
。
1. Using the CGContextAddArcToPoint API
当您使用矩形时,您知道要绘制的每个弧的切线 - 它们只是矩形的边缘! 您可以根据矩形的圆角来指定半径 - 圆弧越大,圆角越圆。
关于这个函数的另一个好处是,如果路径中的当前点没有设置为告诉弧开始绘制的位置,它将从当前点到路径的开头绘制一条线。 因此,您可以将其用作快捷方式,只需几次调用即可绘制圆角矩形。
2. Drawing Your Arcs
由于您要在此Core Graphics
教程中创建一堆圆角矩形,并且希望代码尽可能可重用,因此请为所有绘图方法创建单独的文件。
转到File ▸ New ▸ File…
,然后选择iOS ▸ Swift File
。 按Next
,将其命名为Drawing
,然后单击Create
。
现在,使用以下内容替换import Foundation
:
import UIKit
import CoreGraphics
extension UIView {
func createRoundedRectPath(for rect: CGRect, radius: CGFloat) -> CGMutablePath {
let path = CGMutablePath()
// 1
let midTopPoint = CGPoint(x: rect.midX, y: rect.minY)
path.move(to: midTopPoint)
// 2
let topRightPoint = CGPoint(x: rect.maxX, y: rect.minY)
let bottomRightPoint = CGPoint(x: rect.maxX, y: rect.maxY)
let bottomLeftPoint = CGPoint(x: rect.minX, y: rect.maxY)
let topLeftPoint = CGPoint(x: rect.minX, y: rect.minY)
// 3
path.addArc(tangent1End: topRightPoint,
tangent2End: bottomRightPoint,
radius: radius)
path.addArc(tangent1End: bottomRightPoint,
tangent2End: bottomLeftPoint,
radius: radius)
path.addArc(tangent1End: bottomLeftPoint,
tangent2End: topLeftPoint,
radius: radius)
path.addArc(tangent1End: topLeftPoint,
tangent2End: topRightPoint,
radius: radius)
// 4
path.closeSubpath()
return path
}
}
上面的代码为UIView
类型的所有内容创建了一个全局扩展,因此您不仅可以将它用于UIButton
。
该代码还指示createRoundedRectPath(for:radius :)
按以下顺序绘制圆角矩形:
以下是代码中发生的情况的细分:
- 1) 您移动到顶线部分的中心。
- 2) 您将每个角声明为局部常量。
- 3) 您将每个弧添加到路径:
- i) 首先,在右上角添加一个圆弧。 在绘制弧之前,
CGPathAddArcToPoint
将从矩形中间的当前位置到弧的开始绘制一条线。 - ii) 同样,您可以为右下角和连接线添加弧。
- iii) 然后为左下角和连接线添加弧。
- iv) 最后,为左上角和连接线添加弧。
- i) 首先,在右上角添加一个圆弧。 在绘制弧之前,
- 4) 最后,使用
closeSubpath()
连接弧的终点和起点。
3. Making Your Button Rounded
好的,现在是时候让这个方法起作用了! 打开CoolButton.swift
并用以下内容替换draw(_ :)
:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
// 1
let outerColor = UIColor(
hue: hue, saturation: saturation, brightness: brightness, alpha: 1.0)
let shadowColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5)
// 2
let outerMargin: CGFloat = 5.0
let outerRect = rect.insetBy(dx: outerMargin, dy: outerMargin)
// 3
let outerPath = createRoundedRectPath(for: outerRect, radius: 6.0)
// 4
if state != .highlighted {
context.saveGState()
context.setFillColor(outerColor.cgColor)
context.setShadow(offset: CGSize(width: 0, height: 2),
blur: 3.0, color: shadowColor.cgColor)
context.addPath(outerPath)
context.fillPath()
context.restoreGState()
}
}
下面进行细分:
- 1) 您定义了两种颜色。
- 2) 然后你使用
insetBy(dx:dy :)
得到一个稍小的矩形(每边5个像素),你将绘制圆角矩形。 你把它变小了,这样你就有空间在外面画一个阴影。 - 3) 接下来,您调用刚刚编写的函数
createRoundedRectPath(for:radius :)
,为圆角矩形创建路径。 - 4) 最后,设置填充颜色和阴影,添加上下文的路径,并调用
fillPath()
以使用当前颜色填充它。
注意:如果您的按钮当前未突出显示,您只想运行代码;例如,它没有被点击。
构建并运行应用程序;如果一切正常,您应该看到以下内容:
Adding a Gradient
好吧,按钮开始看起来很不错,但你可以做得更好! 添加渐变怎么样?
将以下函数添加到Drawing.swift
,以使其普遍适用于任何UIView
:
func drawLinearGradient(
context: CGContext, rect: CGRect, startColor: CGColor, endColor: CGColor) {
// 1
let colorSpace = CGColorSpaceCreateDeviceRGB()
// 2
let colorLocations: [CGFloat] = [0.0, 1.0]
// 3
let colors: CFArray = [startColor, endColor] as CFArray
// 4
let gradient = CGGradient(
colorsSpace: colorSpace, colors: colors, locations: colorLocations)!
// More to come...
}
看起来并不多,但这个函数做了很多!
- 1) 您需要的第一件事是获得一个用于绘制渐变的颜色空间。
注意:您可以使用色彩空间做很多事情,但99%的时间您只需要一个标准的设备相关RGB色彩空间。因此,只需使用函数
CGColorSpaceCreateDeviceRGB()
来获取所需的引用。
- 2) 接下来,设置一个数组,跟踪渐变范围内每种颜色的位置。值0表示渐变的开始,1表示渐变的结束。您只有两种颜色,并且您希望第一种颜色位于开头,第二种颜色位于结尾,因此您传入0和1。
注意:如果需要,可以在渐变中使用三种或更多颜色,并且可以设置渐变中每种颜色的开始位置。这对某些效果很有用。
-
3) 之后,使用传递给函数的颜色创建一个数组。为方便起见,您在此处使用普通旧数组,但您需要将其转换为
CFArray
,因为这就是API所需要的。 -
4) 然后使用
CGGradient(colorsSpace:colors:locations :)
创建渐变,传入颜色空间,颜色数组和先前创建的位置。
你现在有一个渐变引用,但它实际上还没有绘制任何东西。它只是指向稍后实际绘制时使用的信息的指针。
通过在drawLinearGradient(context:rect:startColor:endColor :)
的末尾添加以下内容来完成该函数:
// 5
let startPoint = CGPoint(x: rect.midX, y: rect.minY)
let endPoint = CGPoint(x: rect.midX, y: rect.maxY)
context.saveGState()
// 6
context.addRect(rect)
// 7
context.clip()
// 8
context.drawLinearGradient(
gradient, start: startPoint, end: endPoint, options: [])
context.restoreGState()
- 5) 您要做的第一件事是计算要绘制渐变的起点和终点。您只需将其设置为矩形的
top middle
到bottom middle
的直线。
其余的代码可以帮助您在提供的矩形中绘制渐变,关键函数是drawLinearGradient(_:start:end:options :)
。
1. Constraining a Gradient to a Sub-area
然而,关于该函数的奇怪之处在于它用渐变填充整个绘图区域 - 没有办法将其设置为仅用渐变填充子区域!
好吧,没有裁剪(clipping)
,那就是!
裁剪是Core Graphics
的一个很棒的功能,可以将绘图限制为任意形状。您所要做的就是将形状添加到上下文中,但是不要像通常那样填充它,而是调用clip()
。现在,您已将所有未来的绘图限制在该区域!
这就是你在这里做的:
- 6) 您将矩形添加到上下文中。
- 7) 剪辑到该区域。
- 8) 然后调用
drawLinearGradient(_:start:end:options :)
,传入之前设置的所有变量。
那么关于saveGState()/ restoreGState()
的这些东西究竟是什么呢?
好吧,Core Graphics
是一个状态机。您可以配置所需的一组状态,例如颜色和线条粗细,然后执行操作以实际绘制它们。这意味着一旦你设置了某些东西,它就会一直保持这种状态,直到你改回来。
好吧,你刚刚修剪到一个区域,所以除非你做了一些事情,否则你再也无法在那个区域之外画画了!
这就是saveGState()/ restoreGState()
可以帮到的地方。
通过这些,您可以将上下文的当前设置保存到堆栈中,然后在完成后再将其pop回到原来的位置。
就是这样,现在尝试一下吧!
打开CoolButton.swift
并将其添加到draw(_ :)
的底部:
// Outer Path Gradient:
// 1
let outerTop = UIColor(hue: hue, saturation: saturation,
brightness: brightness, alpha: 1.0)
let outerBottom = UIColor(hue: hue, saturation: saturation,
brightness: brightness * 0.8, alpha: 1.0)
// 2
context.saveGState()
context.addPath(outerPath)
context.clip()
drawLinearGradient(context: context, rect: outerRect,
startColor: outerTop.cgColor, endColor: outerBottom.cgColor)
context.restoreGState()
建立并运行;你应该看到这样的东西:
- 1) 首先,定义顶部和底部颜色。
- 2) 然后,通过在堆栈上保存当前图形状态,添加路径,剪切它,绘制渐变并再次恢复状态来绘制渐变。
很好,你的按钮看起来很漂亮! 一些额外的pizazz
怎么样?!
Adding a Gloss Effect
现在是时候让这个按钮闪亮了,因为skeuomorphism
应该永远不会过时!
但是对于我可怜的眼睛,你可以通过应用渐变alpha
蒙版获得一个相当好看的光泽效果近似,这更容易理解和编码。 所以你要去这么做。
这是你可以全面应用于UIView
的东西,所以将以下函数添加到Drawing.swift
中的UIView
扩展:
func drawGlossAndGradient(
context: CGContext, rect: CGRect, startColor: CGColor, endColor: CGColor) {
// 1
drawLinearGradient(
context: context, rect: rect, startColor: startColor, endColor: endColor)
let glossColor1 = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.35)
let glossColor2 = UIColor(red: 1.0, green: 1.0, blue: 1.0, alpha: 0.1)
let topHalf = CGRect(origin: rect.origin,
size: CGSize(width: rect.width, height: rect.height/2))
drawLinearGradient(context: context, rect: topHalf,
startColor: glossColor1.cgColor, endColor: glossColor2.cgColor)
}
此函数基本上是从开始到结束颜色在矩形上绘制渐变,然后在上半部分添加光泽。 下面进行细分:
- 1) 要绘制渐变,请调用之前编写的函数。
- 2) 要绘制光泽,然后在其上绘制另一个渐变,从很透明(白色,0.35 alpha)到非常透明(白色,0.1 alpha)。
看看它的外观。 回到CoolButton.swift
并在draw(_ :)
中做一个小改动。 替换此行,这是draw(_ :)
中的倒数第二行:
drawLinearGradient(context: context, rect: outerRect,
startColor: outerTop.cgColor, endColor: outerBottom.cgColor)
使用
drawGlossAndGradient(context: context, rect: outerRect,
startColor: outerTop.cgColor, endColor: outerBottom.cgColor)
如果您无法发现差异,您只需将drawLinearGradient(context:rect:startColor:endColor :)
更改为drawGlossAndGradient(context:rect:startColor:endColor :)
,即Drawing.swift
中新添加的方法。
构建并运行,您的按钮现在应如下所示:
噢,有光泽!
Styling the Button
现在为超细,挑剔的细节。 如果您正在制作3D按钮,那么您可以全力以赴。 要做到这一点,你需要一个斜角。
要创建斜角类型效果,请添加一条内部路径,该路径的渐变略微不同于外部路径。 将它添加到CoolButton.swift
中的draw(_ :)
底部:
// 1: Inner Colors
let innerTop = UIColor(
hue: hue, saturation: saturation, brightness: brightness * 0.9, alpha: 1.0)
let innerBottom = UIColor(
hue: hue, saturation: saturation, brightness: brightness * 0.7, alpha: 1.0)
// 2: Inner Path
let innerMargin: CGFloat = 3.0
let innerRect = outerRect.insetBy(dx: innerMargin, dy: innerMargin)
let innerPath = createRoundedRectPath(for: innerRect, radius: 6.0)
// 3: Draw Inner Path Gloss and Gradient
context.saveGState()
context.addPath(innerPath)
context.clip()
drawGlossAndGradient(context: context,
rect: innerRect, startColor: innerTop.cgColor, endColor: innerBottom.cgColor)
context.restoreGState()
在这里,您使用insetBy(dx:dy :)
再次缩小矩形,然后获得一个圆角矩形并在其上运行渐变。 构建并运行,您将看到一个微妙的改进:
Highlighting the Button
你的按钮看起来很酷,但它不像一个按钮。 没有指示用户是否按下了按钮。
要处理此问题,您需要覆盖触摸事件以告诉您的按钮重新显示自身,因为在用户选择它之后可能需要更新。
将以下内容添加到CoolButton.swift
:
@objc func hesitateUpdate() {
setNeedsDisplay()
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesBegan(touches, with: event)
setNeedsDisplay()
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesMoved(touches, with: event)
setNeedsDisplay()
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesCancelled(touches, with: event)
setNeedsDisplay()
perform(#selector(hesitateUpdate), with: nil, afterDelay: 0.1)
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
setNeedsDisplay()
perform(#selector(hesitateUpdate), with: nil, afterDelay: 0.1)
}
构建并运行项目,当你点击按钮时,你会发现存在差异 - 突出显示和斜角消失。
但是你可以通过draw(_:)
来改善效果:
当用户按下按钮时,整个按钮应该变暗。
您可以通过为名为actualBrightness
的亮度创建临时变量,然后根据按钮的状态适当调整它来实现此目的:
var actualBrightness = brightness
if state == .highlighted {
actualBrightness -= 0.1
}
然后,在draw(_ :)
内,用actualBrightness
替换所有亮度实例。
总而言之,draw(_ :)
函数现在看起来像这样。 它有点长,但重复是值得的:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else {
return
}
var actualBrightness = brightness
if state == .highlighted {
actualBrightness -= 0.1
}
let outerColor = UIColor(
hue: hue, saturation: saturation, brightness: actualBrightness, alpha: 1.0)
let shadowColor = UIColor(red: 0.2, green: 0.2, blue: 0.2, alpha: 0.5)
let outerMargin: CGFloat = 5.0
let outerRect = rect.insetBy(dx: outerMargin, dy: outerMargin)
let outerPath = createRoundedRectPath(for: outerRect, radius: 6.0)
if state != .highlighted {
context.saveGState()
context.setFillColor(outerColor.cgColor)
context.setShadow(
offset: CGSize(width: 0, height: 2), blur: 3.0, color: shadowColor.cgColor)
context.addPath(outerPath)
context.fillPath()
context.restoreGState()
}
// Outer Path Gloss & Gradient
let outerTop = UIColor(hue: hue, saturation: saturation,
brightness: actualBrightness, alpha: 1.0)
let outerBottom = UIColor(hue: hue, saturation: saturation,
brightness: actualBrightness * 0.8, alpha: 1.0)
context.saveGState()
context.addPath(outerPath)
context.clip()
drawGlossAndGradient(context: context, rect: outerRect,
startColor: outerTop.cgColor, endColor: outerBottom.cgColor)
context.restoreGState()
// Inner Path Gloss & Gradient
let innerTop = UIColor(hue: hue, saturation: saturation,
brightness: actualBrightness * 0.9, alpha: 1.0)
let innerBottom = UIColor(hue: hue, saturation: saturation,
brightness: actualBrightness * 0.7, alpha: 1.0)
let innerMargin: CGFloat = 3.0
let innerRect = outerRect.insetBy(dx: innerMargin, dy: innerMargin)
let innerPath = createRoundedRectPath(for: innerRect, radius: 6.0)
context.saveGState()
context.addPath(innerPath)
context.clip()
drawGlossAndGradient(context: context, rect: innerRect,
startColor: innerTop.cgColor, endColor: innerBottom.cgColor)
context.restoreGState()
}
构建并运行;点击它时按钮应该看起来很棒!
后记
本篇主要讲述了如何制作Glossy效果的按钮,感兴趣的给个赞或者关注~~~