MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Swift图形绘制与Core Graphics进阶

2021-03-287.1k 阅读

Swift 图形绘制基础

理解图形上下文

在 Swift 中进行图形绘制,首先要理解图形上下文(Graphics Context)的概念。图形上下文是一个保存绘图状态和参数的对象,它就像是一块画布,我们在上面进行各种图形绘制操作。Core Graphics 框架为我们提供了与图形上下文交互的能力。

在 iOS 开发中,通常在视图的 draw(_ rect: CGRect) 方法中获取当前的图形上下文。例如:

override func draw(_ rect: CGRect) {
    guard let context = UIGraphicsGetCurrentContext() else { return }
    // 在这里可以对 context 进行操作,开始绘制图形
}

这里通过 UIGraphicsGetCurrentContext() 获取当前视图的图形上下文。如果获取失败(例如在不支持图形绘制的环境中调用),则直接返回。

基本图形绘制

  1. 绘制线条 绘制线条需要定义起点和终点,并设置线条的样式。以下是绘制一条简单直线的代码示例:
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() 方法将路径(这里就是这条直线)渲染到图形上下文中。

  1. 绘制矩形 绘制矩形相对简单,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(_:) 方法填充这个矩形到图形上下文中。

  1. 绘制圆形 绘制圆形需要使用路径来定义。以下是绘制圆形的代码示例:
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,结束角度为 (表示完整的圆),并且绘制方向为逆时针。最后使用 strokePath() 方法绘制圆形的轮廓。

Core Graphics 进阶技巧

路径的高级操作

  1. 路径的组合 在实际应用中,常常需要将多个路径组合在一起。例如,绘制一个带有缺口的圆形。首先绘制一个完整的圆形路径,然后再绘制一个矩形路径,通过 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 并绘制轮廓,从而得到带有缺口的圆形。

  1. 路径的变形 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 度),再将坐标系统平移回原来的位置。最后将变形后的矩形路径添加到图形上下文中并填充。

渐变填充

  1. 线性渐变 线性渐变是从一个点到另一个点按照线性方向进行颜色过渡。使用 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:) 方法在视图的矩形区域内绘制从左上角到右下角的线性渐变。

  1. 径向渐变 径向渐变是从一个中心点向四周按照径向方向进行颜色过渡。以下是绘制径向渐变的示例代码:
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:) 方法将文本绘制到图形上下文中。

文本样式与排版

  1. 文本对齐方式 可以通过设置 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 坐标位置。

  1. 多行文本排版 对于多行文本,需要使用 NSAttributedStringNSTextContainer 等类来进行排版。以下是一个简单的多行文本绘制示例:
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 对象,并设置了字体和颜色属性。然后创建了一个 NSTextContainerNSLayoutManager,将文本容器添加到布局管理器中,并将布局管理器添加到文本存储中。最后通过布局管理器将多行文本绘制到指定的原点位置。

图形绘制性能优化

减少不必要的绘制

  1. 使用脏矩形 在 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 是否与脏矩形相交,来决定是否在该区域内进行绘制,从而减少不必要的绘制操作。

  1. 缓存绘制结果 如果某些图形元素在视图生命周期内不会频繁变化,可以将其绘制结果缓存起来,避免每次重绘时都重新绘制。例如,可以使用 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,下次重绘时就可以直接使用缓存的图像,提高绘制性能。

优化路径和图形操作

  1. 简化路径 复杂的路径会增加绘制的计算量。尽量简化路径,避免过多的节点和不必要的曲线。例如,在绘制一个不规则形状时,如果可以用多个简单图形组合来近似表示,就不要使用过于复杂的单一路径。
// 复杂路径示例
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()
}

在这个对比示例中,展示了复杂路径和简化路径的不同。在实际应用中,通过合理地使用简单路径组合来代替复杂路径,可以显著提高绘制性能。

  1. 减少透明度和混合模式的使用 透明度和混合模式的计算会消耗更多的性能。尽量避免在频繁绘制的图形上使用透明度和复杂的混合模式。如果必须使用,尝试将透明度和混合模式应用到较大的、不频繁变化的图形元素上。
// 减少透明度和混合模式使用示例
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 都提供了强大而灵活的工具,开发者可以根据具体需求进行深入探索和应用。