Swift图形绘制与Core Graphics进阶
Swift 图形绘制基础
理解图形上下文
在 Swift 中进行图形绘制,首先要理解图形上下文(Graphics Context)的概念。图形上下文是一个保存绘图状态和参数的对象,它就像是一块画布,我们在上面进行各种图形绘制操作。Core Graphics 框架为我们提供了与图形上下文交互的能力。
在 iOS 开发中,通常在视图的 draw(_ rect: CGRect)
方法中获取当前的图形上下文。例如:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 在这里可以对 context 进行操作,开始绘制图形
}
这里通过 UIGraphicsGetCurrentContext()
获取当前视图的图形上下文。如果获取失败(例如在不支持图形绘制的环境中调用),则直接返回。
基本图形绘制
- 绘制线条 绘制线条需要定义起点和终点,并设置线条的样式。以下是绘制一条简单直线的代码示例:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 设置线条颜色
UIColor.blue.setStroke()
// 设置线条宽度
context.setLineWidth(2.0)
// 移动到起点
context.move(to: CGPoint(x: 50, y: 50))
// 绘制到终点
context.addLine(to: CGPoint(x: 150, y: 150))
// 渲染线条
context.strokePath()
}
在这段代码中,首先设置了线条颜色为蓝色,并设置线条宽度为 2 个点。然后通过 move(to:)
方法移动到起点 (50, 50)
,接着使用 addLine(to:)
方法定义线条的终点 (150, 150)
。最后调用 strokePath()
方法将路径(这里就是这条直线)渲染到图形上下文中。
- 绘制矩形 绘制矩形相对简单,Core Graphics 提供了直接绘制矩形的方法。示例代码如下:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 设置填充颜色
UIColor.green.setFill()
// 定义矩形
let rectangle = CGRect(x: 100, y: 100, width: 100, height: 100)
// 填充矩形
context.fill(rectangle)
}
在这个例子中,设置填充颜色为绿色,定义了一个矩形 CGRect(x: 100, y: 100, width: 100, height: 100)
,然后使用 fill(_:)
方法填充这个矩形到图形上下文中。
- 绘制圆形 绘制圆形需要使用路径来定义。以下是绘制圆形的代码示例:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 设置线条颜色
UIColor.red.setStroke()
// 设置线条宽度
context.setLineWidth(3.0)
let center = CGPoint(x: 150, y: 150)
let radius: CGFloat = 50
// 开始一个新的路径
context.beginPath()
// 添加圆形路径
context.addArc(center: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: false)
// 渲染路径(绘制轮廓)
context.strokePath()
}
这里通过 addArc(center:radius:startAngle:endAngle:clockwise:)
方法添加一个圆形路径。圆心为 (150, 150)
,半径为 50,起始角度为 0,结束角度为 2π
(表示完整的圆),并且绘制方向为逆时针。最后使用 strokePath()
方法绘制圆形的轮廓。
Core Graphics 进阶技巧
路径的高级操作
- 路径的组合
在实际应用中,常常需要将多个路径组合在一起。例如,绘制一个带有缺口的圆形。首先绘制一个完整的圆形路径,然后再绘制一个矩形路径,通过
addPath(_:)
方法将矩形路径添加到圆形路径中,并设置合适的运算规则来实现缺口效果。
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 设置线条颜色
UIColor.black.setStroke()
// 设置线条宽度
context.setLineWidth(2.0)
let center = CGPoint(x: 150, y: 150)
let radius: CGFloat = 50
// 开始一个新的路径
context.beginPath()
// 添加圆形路径
context.addArc(center: center, radius: radius, startAngle: 0, endAngle: .pi * 2, clockwise: false)
let notchRect = CGRect(x: 130, y: 150, width: 40, height: 20)
let notchPath = UIBezierPath(rect: notchRect)
// 将矩形路径添加到圆形路径中,并设置运算规则为减去
context.addPath(notchPath.cgPath)
context.setBlendMode(.clear)
context.fillPath()
context.setBlendMode(.normal)
context.strokePath()
}
在这段代码中,先绘制了圆形路径,然后创建了一个表示缺口的矩形路径 notchPath
。通过 addPath(_:)
将矩形路径添加到圆形路径中,并设置混合模式为 .clear
来减去矩形部分,再恢复混合模式为 .normal
并绘制轮廓,从而得到带有缺口的圆形。
- 路径的变形 Core Graphics 允许对路径进行各种变形操作,如平移、旋转和缩放。这些操作通过变换矩阵来实现。以下是对一个矩形路径进行旋转的示例:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 设置填充颜色
UIColor.yellow.setFill()
let rectangle = CGRect(x: 100, y: 100, width: 100, height: 100)
let rectanglePath = UIBezierPath(rect: rectangle)
let center = CGPoint(x: rectangle.midX, y: rectangle.midY)
context.translateBy(x: center.x, y: center.y)
context.rotate(by: .pi / 4)
context.translateBy(x: -center.x, y: -center.y)
context.addPath(rectanglePath.cgPath)
context.fillPath()
}
在这个例子中,首先创建了一个矩形路径 rectanglePath
。然后获取矩形的中心 center
,通过 translateBy(x:y:)
方法将坐标系统平移到矩形中心,接着使用 rotate(by:)
方法将坐标系统旋转 π/4
弧度(45 度),再将坐标系统平移回原来的位置。最后将变形后的矩形路径添加到图形上下文中并填充。
渐变填充
- 线性渐变 线性渐变是从一个点到另一个点按照线性方向进行颜色过渡。使用 Core Graphics 绘制线性渐变需要创建一个渐变对象,并设置起始点和结束点以及渐变的颜色数组。
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let startColor = UIColor.red.cgColor
let endColor = UIColor.blue.cgColor
let colors: [CGColor] = [startColor, endColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil)!
let startPoint = CGPoint(x: 0, y: 0)
let endPoint = CGPoint(x: rect.width, y: rect.height)
context.drawLinearGradient(gradient, start: startPoint, end: endPoint, options: [])
}
在这段代码中,定义了起始颜色 red
和结束颜色 blue
,创建了一个颜色空间 colorSpace
和一个渐变对象 gradient
。通过 drawLinearGradient(_:start:end:options:)
方法在视图的矩形区域内绘制从左上角到右下角的线性渐变。
- 径向渐变 径向渐变是从一个中心点向四周按照径向方向进行颜色过渡。以下是绘制径向渐变的示例代码:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let startColor = UIColor.yellow.cgColor
let endColor = UIColor.green.cgColor
let colors: [CGColor] = [startColor, endColor]
let colorSpace = CGColorSpaceCreateDeviceRGB()
let gradient = CGGradient(colorsSpace: colorSpace, colors: colors as CFArray, locations: nil)!
let center = CGPoint(x: rect.midX, y: rect.midY)
let startRadius: CGFloat = 0
let endRadius: CGFloat = min(rect.width, rect.height) / 2
context.drawRadialGradient(gradient, startCenter: center, startRadius: startRadius, endCenter: center, endRadius: endRadius, options: [])
}
这里定义了从黄色到绿色的径向渐变,以视图矩形的中心为渐变中心,起始半径为 0,结束半径为矩形宽度和高度较小值的一半。通过 drawRadialGradient(_:startCenter:startRadius:endCenter:endRadius:options:)
方法绘制径向渐变。
阴影效果
为图形添加阴影可以增强图形的立体感和视觉效果。在 Core Graphics 中,通过设置图形上下文的阴影属性来实现阴影效果。
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 设置填充颜色
UIColor.orange.setFill()
let rectangle = CGRect(x: 100, y: 100, width: 100, height: 100)
// 设置阴影属性
context.setShadow(offset: CGSize(width: 5, height: 5), blur: 3, color: UIColor.black.cgColor)
context.fill(rectangle)
}
在这个例子中,为绘制的矩形设置了阴影。阴影的偏移量为 (5, 5)
(向右下方偏移 5 个点),模糊半径为 3,阴影颜色为黑色。通过这种方式,绘制的矩形就会带有一个明显的阴影效果。
文本绘制
基本文本绘制
在 Swift 中使用 Core Graphics 绘制文本,需要创建一个字体对象,并设置文本的绘制属性。以下是在视图中绘制简单文本的示例:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let text = "Hello, Core Graphics!"
let font = UIFont.systemFont(ofSize: 20)
let attributes: [NSAttributedString.Key : Any] = [
.font: font,
.foregroundColor: UIColor.purple
]
let textSize = text.size(withAttributes: attributes)
let x = (rect.width - textSize.width) / 2
let y = (rect.height - textSize.height) / 2
text.draw(at: CGPoint(x: x, y: y), withAttributes: attributes)
}
在这段代码中,定义了要绘制的文本 Hello, Core Graphics!
,选择了系统字体并设置大小为 20。通过 NSAttributedString.Key
定义了文本的字体和前景色属性。计算出文本在视图中的居中位置,并使用 draw(at:withAttributes:)
方法将文本绘制到图形上下文中。
文本样式与排版
- 文本对齐方式
可以通过设置
NSAttributedString.Key.alignment
属性来改变文本的对齐方式。例如,将文本设置为右对齐:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let text = "Right - aligned Text"
let font = UIFont.systemFont(ofSize: 20)
let attributes: [NSAttributedString.Key : Any] = [
.font: font,
.foregroundColor: UIColor.brown,
.alignment: NSTextAlignment.right
]
let textSize = text.size(withAttributes: attributes)
let x = rect.width - textSize.width - 10
let y = (rect.height - textSize.height) / 2
text.draw(at: CGPoint(x: x, y: y), withAttributes: attributes)
}
在这个示例中,通过设置 NSTextAlignment.right
使文本右对齐,并相应地调整了文本的 x
坐标位置。
- 多行文本排版
对于多行文本,需要使用
NSAttributedString
和NSTextContainer
等类来进行排版。以下是一个简单的多行文本绘制示例:
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
let text = "This is a multi - line text. It can be used to demonstrate how to layout multiple lines of text in Core Graphics."
let font = UIFont.systemFont(ofSize: 18)
let attributes: [NSAttributedString.Key : Any] = [
.font: font,
.foregroundColor: UIColor.gray
]
let attributedString = NSAttributedString(string: text, attributes: attributes)
let textContainer = NSTextContainer(size: CGSize(width: rect.width - 20, height: rect.height))
let layoutManager = NSLayoutManager()
layoutManager.addTextContainer(textContainer)
let textStorage = NSTextStorage(attributedString: attributedString)
textStorage.addLayoutManager(layoutManager)
let origin = CGPoint(x: 10, y: 10)
layoutManager.drawGlyphs(forGlyphRange: NSRange(location: 0, length: attributedString.length), at: origin)
}
在这段代码中,创建了一个 NSAttributedString
对象,并设置了字体和颜色属性。然后创建了一个 NSTextContainer
和 NSLayoutManager
,将文本容器添加到布局管理器中,并将布局管理器添加到文本存储中。最后通过布局管理器将多行文本绘制到指定的原点位置。
图形绘制性能优化
减少不必要的绘制
- 使用脏矩形
在 iOS 开发中,视图的
draw(_ rect: CGRect)
方法中的rect
参数表示需要重绘的区域,也就是脏矩形(Dirty Rectangle)。尽量只在这个脏矩形区域内进行绘制,避免对整个视图进行不必要的重绘。例如,如果只是视图的某个小部分发生了变化,可以通过计算得到脏矩形,并在draw(_ rect: CGRect)
方法中根据这个脏矩形来限制绘制范围。
// 假设某个操作导致视图的一小部分需要重绘
let dirtyRect = CGRect(x: 100, y: 100, width: 50, height: 50)
// 通知视图重绘脏矩形区域
setNeedsDisplay(dirtyRect)
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 只在脏矩形区域内绘制
if rect.intersects(dirtyRect) {
// 进行图形绘制操作
UIColor.green.setFill()
context.fill(dirtyRect)
}
}
在这个例子中,通过 setNeedsDisplay(_:)
方法指定了需要重绘的脏矩形区域。在 draw(_ rect: CGRect)
方法中,通过判断当前传入的 rect
是否与脏矩形相交,来决定是否在该区域内进行绘制,从而减少不必要的绘制操作。
- 缓存绘制结果
如果某些图形元素在视图生命周期内不会频繁变化,可以将其绘制结果缓存起来,避免每次重绘时都重新绘制。例如,可以使用
UIImage
来缓存一个复杂图形的绘制结果。
class MyView: UIView {
private var cachedImage: UIImage?
override func draw(_ rect: CGRect) {
if let cachedImage = cachedImage {
cachedImage.draw(in: rect)
return
}
guard let context = UIGraphicsGetCurrentContext() else { return }
// 进行复杂图形的绘制
UIColor.blue.setFill()
context.fill(rect)
// 缓存绘制结果
UIGraphicsBeginImageContextWithOptions(rect.size, false, 0)
guard let contextForImage = UIGraphicsGetCurrentContext() else { return }
UIColor.blue.setFill()
contextForImage.fill(rect)
cachedImage = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()
cachedImage?.draw(in: rect)
}
}
在这个自定义视图类 MyView
中,首先检查是否已经有缓存的图像 cachedImage
。如果有,则直接将其绘制到视图上。如果没有,则进行复杂图形的绘制,并将绘制结果缓存为 UIImage
,下次重绘时就可以直接使用缓存的图像,提高绘制性能。
优化路径和图形操作
- 简化路径 复杂的路径会增加绘制的计算量。尽量简化路径,避免过多的节点和不必要的曲线。例如,在绘制一个不规则形状时,如果可以用多个简单图形组合来近似表示,就不要使用过于复杂的单一路径。
// 复杂路径示例
let complexPath = UIBezierPath()
complexPath.move(to: CGPoint(x: 50, y: 50))
complexPath.addCurve(to: CGPoint(x: 150, y: 150), controlPoint1: CGPoint(x: 70, y: 80), controlPoint2: CGPoint(x: 130, y: 120))
complexPath.addLine(to: CGPoint(x: 200, y: 100))
// 更多复杂的路径操作
// 简化后的路径示例
let simplePath1 = UIBezierPath(rect: CGRect(x: 50, y: 50, width: 50, height: 50))
let simplePath2 = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 50, height: 50))
// 更多简单路径组合
// 在绘制时,使用简化后的路径
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
UIColor.red.setFill()
simplePath1.fill()
simplePath2.fill()
}
在这个对比示例中,展示了复杂路径和简化路径的不同。在实际应用中,通过合理地使用简单路径组合来代替复杂路径,可以显著提高绘制性能。
- 减少透明度和混合模式的使用 透明度和混合模式的计算会消耗更多的性能。尽量避免在频繁绘制的图形上使用透明度和复杂的混合模式。如果必须使用,尝试将透明度和混合模式应用到较大的、不频繁变化的图形元素上。
// 减少透明度和混合模式使用示例
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 不透明的图形绘制
UIColor.green.setFill()
context.fill(CGRect(x: 50, y: 50, width: 100, height: 100))
// 尽量避免频繁在小图形上使用透明度和混合模式
// 如果需要,应用到较大的、不频繁变化的图形上
UIColor.blue.withAlphaComponent(0.5).setFill()
context.fill(CGRect(x: 150, y: 150, width: 200, height: 200))
}
在这个例子中,先绘制了一个不透明的绿色矩形,然后在较大的蓝色矩形上应用了透明度。这样可以在一定程度上减少透明度计算对性能的影响。
通过以上对 Swift 图形绘制与 Core Graphics 进阶的详细介绍,从基础图形绘制到各种进阶技巧,再到性能优化,希望能帮助开发者在实际项目中更好地利用这些知识来创建高质量、高性能的图形界面。无论是简单的界面元素绘制还是复杂的图形动画实现,Core Graphics 都提供了强大而灵活的工具,开发者可以根据具体需求进行深入探索和应用。