1-渐变阴影和文本
Posted 颐和园
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了1-渐变阴影和文本相关的知识,希望对你有一定的参考价值。
本文是《Core Graphics 入门教程》的第一讲。学完本文,你将学会如何在自定义控件中用 CG 函数绘制渐变色、阴影以及文本。
ShadowButton
假设我们想定制一个按钮控件叫做 ShadowButton——这不值得大惊小怪,但特别的一点就是,我们准备用 Core Graphics 2D 绘制它的 UI。
首先创建这个 class。首先,我们需要继承 UIControl 而不是 UIView,这样是为了方便使用它的 state 属性并对用户的触摸进行处理。
class ShadowButton: UIControl {
// 1
lazy var outerShadow: NSShadow = {
let outerShadow = NSShadow()
outerShadow.shadowColor = UIColor.black
outerShadow.shadowOffset = CGSize(width: 3.1, height: 3.1)
outerShadow.shadowBlurRadius = 5
return outerShadow
}()
lazy var innerShadow: NSShadow = {
let innerShadow = NSShadow()
innerShadow.shadowColor = UIColor.white
innerShadow.shadowOffset = CGSize(width: 3.1, height: 3.1)
innerShadow.shadowBlurRadius = 9
return innerShadow
}()
lazy var text: Text? = Text(text: "Shadow Button", font: UIFont.systemFont(ofSize: 18), color: .black)
lazy var gradient: CGGradient = {
let deepFillColor = UIColor(red: 0.502, green: 0.502, blue: 0.502, alpha: 1.000)
let lightFillColor = UIColor(red: 0.871, green: 0.871, blue: 0.871, alpha: 1.000)
let gradient = CGGradient(colorsSpace: CGColorSpaceCreateDeviceRGB(), colors: [lightFillColor.cgColor, deepFillColor.cgColor] as CFArray, locations: [0, 1])!
return gradient
}()
var buttonRadius: CGFloat = 11
// 2
override func draw(_ rect: CGRect) {
if isSelected == true {
selectDraw(rect)
}else {
unselectDraw(rect)
}
}
// 3
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isSelected = true
setNeedsDisplay()
}
// 4
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
self.isSelected = false
setNeedsDisplay()
}
// 5
public func selectDraw(_ frame: CGRect) {
let rect = RectanglePainter.insetFrame(frame, delta: outerShadow.shadowBlurRadius)
RectanglePainter.drawGradient(gradient: gradient, frame: rect, cornerRadius: buttonRadius, outerShadow: outerShadow)
RectanglePainter.drawInnerShadow(rect, cornerRadius: buttonRadius, innerShadow: innerShadow)
if text != nil {
TextPainter.drawText(rect, text: text!)
}
}
// 6
public func unselectDraw(_ frame: CGRect) {
let rect = RectanglePainter.insetFrame(frame, delta: outerShadow.shadowBlurRadius)
RectanglePainter.drawGradient(gradient: gradient, frame: rect, cornerRadius: buttonRadius, outerShadow: outerShadow)
if text != nil {
TextPainter.drawText(rect, text: text!)
}
}
}
- 按钮属性,包括圆角、内阴影、外阴影、文本等,方便从外部定制按钮样式。这里使用了懒加载属性的定义,这样在第一次调用这些属性时,这些属性值会自动初始化。请注意 NSShadow、Text 和CGGrandient 这些结构体的构造方式。
- 用 Core Graphics 绘图等最主要方法就是 draw(rect:) 方法,这里我们会判断按钮的 isSelected 状态,并调用对应的方法进行 UI 的绘制。
- touchesBegan 函数,响应用户的触摸动作。这样当用户触摸按钮时会有一个加亮的效果。正常情况控件状态是 unselected 的,但当用户点击它时,我们改变控件的 isSelected 为 true,然后调用 setNeedsDisplay 重新绘制控件——这实际上会触发 draw(rect:) 方法。
- touchesEnded 函数,响应用户的松开动作。这样当用户释放按钮时会有一个加暗的效果。当用户释放它时,我们改变控件的 isSelected 为 false,并调用 setNeedsDisplay 重新绘制控件,这样按钮又恢复原样。
- selectDraw 方法负责 isSelected 为 true 时的绘制。这里调用了两个 Painter 类,这些 Painter 类中封装了 Core Graphics 绘制方法。首先调用 RectanglePainter 绘制一个矩形(同时绘制外阴影),然后绘制矩形的内阴影,最后调用 TextPainter 绘制文本。
- unselectDraw 方法负责 isSelected 为 false 时的绘制。同 selectDraw 方法一样,但不用绘制内阴影(去掉加亮效果)。
RectangleButtonPainter 和 TextPainter负责体力活——绘制矩形和文本。
ShadowButtonPainter
class RectanglePainter {
// 1
public static func insetFrame(_ frame: CGRect, delta: CGFloat) -> CGRect {
return CGRect(x: delta, y: delta, width: frame.width-delta*2, height: frame.height-delta*2)
}
// 2
public static func drawGradient(gradient: CGGradient?, frame: CGRect, cornerRadius: CGFloat?, outerShadow: NSShadow?) {
// 3
let context = UIGraphicsGetCurrentContext()
if let context = context {
// 4
let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius ?? 0)
// 5
context.saveGState()
// 6
if let shadow = outerShadow, let color = shadow.shadowColor as? UIColor {
context.setShadow(offset: shadow.shadowOffset, blur: shadow.shadowBlurRadius, color: color.cgColor)
}
// 7
context.beginTransparencyLayer(auxiliaryInfo: nil)
// 8
path.addClip()
// 9
if let gradient = gradient {
context.drawLinearGradient(gradient, start: CGPoint(x: frame.midX, y: frame.minY), end: CGPoint(x: frame.midX, y: frame.maxY), options: [])
}else {
path.fill()
}
// 10
context.endTransparencyLayer()
context.restoreGState()
}
}
// 11
public static func drawInnerShadow(_ frame: CGRect, cornerRadius: CGFloat?, innerShadow:NSShadow) {
let context = UIGraphicsGetCurrentContext()
if let context = context, let color = innerShadow.shadowColor as? UIColor {
// 12
let path = UIBezierPath(roundedRect: frame, cornerRadius: cornerRadius ?? 0)
context.saveGState()
// 13
context.clip(to: path.bounds)
// 14
context.setAlpha(color.cgColor.alpha)
// 15
context.beginTransparencyLayer(auxiliaryInfo: nil)
// 16
let rectangleOpaqueShadow = color.withAlphaComponent(1)
// 17
context.setShadow(offset: innerShadow.shadowOffset, blur: innerShadow.shadowBlurRadius, color: rectangleOpaqueShadow.cgColor)
// 18
context.setBlendMode(.sourceOut)
context.beginTransparencyLayer(auxiliaryInfo: nil)
// 19
rectangleOpaqueShadow.setFill()
path.fill()
// 20
context.endTransparencyLayer()
context.endTransparencyLayer()
context.restoreGState()
}
}
}
-
insetFrame 方法,计算一个“缩小的”矩形,缩小的范围为 delta 个像素。这个方法在绘制带阴影效果的控件时经常会用到,因为矩形的阴影往往会占据额外的控件,因此我们绘制控件的矩形空间时不能把整个控件的 frame 都占满,要留有足够的控件绘制阴影。
-
drawGradient 方法负责绘制渐变填充+外阴影,需要传入 4 个参数:渐变色 gradient、矩形范围 frame、圆角半径 cornerRadius、外阴影 outerShadow。
-
首先获取当前上下文 context,这是最重要的东西,在 CG 绘图中我们总是需要它,没有它我们什么都干不了。
-
构造一个圆角的矩形,作为按钮的形状(不包含阴影)。调用贝塞尔曲线的构造方法创建一个圆角矩形。
-
CG 绘图经常涉及到对 context 的状态改变。由于 context 对象是全局的,在我们进行操作之前,最好对 context 原来对状态进行保存,等操作完毕再恢复,这样就不至于因为一次绘制影响了后续的绘制。状态的保存使用 saveGState 函数,状态的恢复使用 restoreGState 函数。
-
设置 context 的阴影,这样当我们开始填充渐变矩形时,顺便就会绘制所设定的 shadow。当然,如果 outerShadow 参数为 nil,我们就什么都不干。
-
开始新的透明图层,透明图层类似于 ps 中图层的概念,而且带有 alpha 通道。方便你组合不同的图形。
-
然后路径裁剪。这样填充内容不会超出路径的内部。
-
在路径内部绘制渐变色。
-
结束透明图层,恢复 context 图形状态。
-
drawInnerShadow 负责绘制高光(内阴影)。它需要 3 个参数:矩形范围 frame,圆角半径 corderRadius,内阴影 innerShadow。
-
构造一个圆角的矩形,作为高光部分的形状。调用贝塞尔曲线的构造方法创建一个圆角矩形。
-
路径剪切。
-
设置 context 的 alpha 值,使得当前绘制的 alpha 值等同于 innerShadow 的 color 的 alpha 值(之前这个值是 1)。
-
然后设置一个透明图层。
-
设置内阴影的颜色,但是需要将内阴影颜色的 alpha 去掉,不管原来是多少都设置为 1。因为我们的内阴影颜色 alpha 值本来也是 1,我们可以把 14 一行去掉,将 15 行改为:
rectangleOpaqueShadow = color
效果也是一样的。不过这里是为了避免万一出现 alpha 不为 1 的情况,这时候需要我们手动将内阴影的 alpha 调整为 1,否则效果会出乎我们意料。
-
用这个 alpha 1 的内阴影颜色设置 context 的阴影。
-
设置混合模式为 source out。混合模式的使用比较复杂,它在填充时并不是直接填充颜色,而是将颜色值进行一定的计算,计算成新的颜色值后用于填充。
-
用计算出的不透明的内阴影颜色填充路径。
-
结束透明图层,恢复 context。
绘制文本
文本比较简单:
private func drawText(_ rect: CGRect, color: UIColor?) {
if let color = color, let string = text?.text, let font = text?.font {
// 1
let paragraphStyle = NSMutableParagraphStyle()
paragraphStyle.alignment = .center
// 2
let fontAttributes = [
.font: font,
.foregroundColor: color,
.paragraphStyle: paragraphStyle,
] as [NSAttributedString.Key: Any]
// 3
let height: CGFloat = string.boundingRect(with: CGSize(width: rect.width, height: CGFloat.infinity), options: .usesLineFragmentOrigin, attributes: fontAttributes, context: nil).height
context?.saveGState()
context?.clip(to: rect)
// 4
string.draw(in: CGRect(x: rect.minX, y: rect.minY + (rect.height - height) / 2, width: rect.width, height: height), withAttributes: fontAttributes)
context?.restoreGState()
}
}
- 设置段落居中(垂直居中)
- 设置字体属性。
- 计算文本高度。
- 绘制文本。
测试
在 viewDidLoad 方法中加上:
let button = ShadowButton(frame: CGRect(x: 100,y: 200,width: 200,height: 50))
button.gradient = gradient()
button.buttonRadius = 11
button.innerShadow = innerShadow()
button.outerShadow = outerShadow()
button.text = text("Hello")
button.backgroundColor = .white
addSubview(button)
效果如下:
以上是关于1-渐变阴影和文本的主要内容,如果未能解决你的问题,请参考以下文章
css(字体,文本,边距,边框,阴影,背景,渐变,多重背景,列表)