Quartz 2D CGPattern学习笔记
阅读原文时间:2023年07月08日阅读:1

CGPattern是在图形上下文中重复绘制的一系列绘制操作。你可以像使用颜色一样使用图案。当使用CGPattern进行绘制时,Quartz将页面划分为一组图案单元格,每个单元格的大小为CGPattern初始化时bounds的大小,并使用您提供的回调来绘制每个单元格。下图为一个简单的CGPattern绘制的图案:

白色部分为图形绘制的上下文 ,带蓝色边框的区域为CGPattern绘制的单元格,它是根据指定规则平铺在图形上下文中的。它总是从上下文的左上角(坐标系原点为左上角时)开始绘制,然后按照一定规则平铺满整个图形上下文。

上面图案的绘制具体代码如下:

override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }

    context.saveGState() // 保存上下文状态,以便绘制完图案后恢复上下文原来的状态(即:这里保存之前的状态)

    // 初始化一个回调函数  
    // 参数  
    // 1:版本号,一般传0即可;  
    // 2:一个绘制图案的函数(该函数有两个参数,1:CGPattern初始化时第一个参数info;2:绘制图案的上下文)  
    // 3:释放CGPattern初始化时第一个参数info指针的回调函数  
    var callbacks = CGPatternCallbacks(version: 0, drawPattern: { (info, ctx) in

        // 这里获取CGPattern初始化时传入的图案单元格的size(注意:这里不能用takeRetainedValue(),这样会导致info资源回收,而在下次回调中报错)  
        let size = Unmanaged<NSValue>.fromOpaque(info!).takeUnretainedValue().cgSizeValue  
        let bounds = CGRect(origin: .zero, size: size)  
        // 设置画笔宽度  
        ctx.setLineWidth(2)  
        // 设置画笔颜色  
        ctx.setStrokeColor(UIColor.red.cgColor)  
        // 设置填充色  
        ctx.setFillColor(UIColor.green.cgColor)  
        // 添加一个矩形框  
        ctx.addRect(bounds)  
        // 绘制边框并填充颜色  
        ctx.drawPath(using: .fillStroke)  
    }) { info in  
        // 需要在这里将CGPattern初始化时传入info指针内存回收  
        Unmanaged<NSValue>.fromOpaque(info!).release()  
    }

    // 图案单元格的间隙  
    let space: CGFloat = 10  
    // 当前上下文中,水平方向上单元格的个数(用于计算单元格的size)  
    let cellCount: CGFloat = 5  
    let drawBounds = bounds // 当前上下文画布矩形  
    // 计算图案单元格尺寸  
    let cellSize = (drawBounds.width - space \* (cellCount - 1)) / cellCount  
    let size = CGSize(width: cellSize, height: cellSize)  
    // 初始化CGPattern  
    // 参数  
    // 1:用于向绘制函数中传递的info,这里我们将图案的单元格size作为info(这里使用passRetained,是为了保持NSValue的引用计数)  
    // 2:设置图案单元格的尺寸  
    // 3:突然单元格的变换矩阵  
    // 4:单元格水平方向的间距  
    // 5:单元格垂直方向的间距  
    // 6:平铺模式  
    // 7:图案单元格是否为彩色的(当我们绘制的突然需要颜色时,设置为true)  
    // 8:绘制图案的回调  
    guard let pattern = CGPattern(info: Unmanaged<NSValue>.passRetained(NSValue(cgSize: size)).toOpaque(),  
                                  bounds: CGRect(x: 0, y: 0, width: size.width, height: size.height),  
                                  matrix: .identity,  
                                  xStep: size.width + space,  
                                  yStep: size.height + space,  
                                  tiling: .noDistortion,  
                                  isColored: true,  
                                  callbacks: &callbacks) else {  
        print("pattern create fail")  
        return  
    }  
    // 创建颜色空间,采用patternBaseSpace初始化(注意:如果isColored=true,这里一般设置为nil即可)  
    guard let colorSpace = CGColorSpace(patternBaseSpace: nil) else {  
        print("colorSpace create fail")  
        return  
    }  
    // 设置填充的颜色空间(这里必须设置,否则图案将无法正常绘制填充色)  
    context.setFillColorSpace(colorSpace)  
    var alpha: CGFloat = 1  
    // 设置填充图案CGPattern,用于填充绘制。第二个参数:指定了图案绘制中用到的颜色透明度  
    context.setFillPattern(pattern, colorComponents: &alpha)  
    // 调用填充绘制,这里会采用上面的pattern进行绘制  
    context.fill(drawBounds)  
    context.restoreGState() // 恢复上下文状态  
}

具体细节已在代码注释中说明,但是这里我要着重说明一下xStep和yStep这两个参数。我们用一张图来理解一下:

从图中可知,xStep是距离前一个图案单元格水平原点的距离,yStep是距离前一个图案单元格垂直原点的距离。因此,如果xStep小于单元格的宽度,那么前后两个单元格在水平方向将会重叠,yStep同样如此。

我们可以在回调函数中任意绘制我们的图案,不局限在绘制矩形或线条,向下面这样:

下面是具体绘制的部分代码:

var callbacks = CGPatternCallbacks(version: 0, drawPattern: { (info, ctx) in

// 这里获取CGPattern初始化时传入的图案单元格的size(注意:这里不能用takeRetainedValue(),这样会导致info资源回收,而在下次回调中报错)  
let size = Unmanaged<NSValue>.fromOpaque(info!).takeUnretainedValue().cgSizeValue

// 设置填充色  
ctx.setFillColor(UIColor.red.cgColor)

let psize = min(size.width, size.height)  
var r: Double, theta: Double  
r = 0.8 \* psize / 2  
theta = 2 \* Double.pi \* (2.0 / 5.0)  
ctx.translateBy(x: psize/2, y: psize/2)  
ctx.move(to: .init(x: 0, y: r))  
for k in 1..<5 {  
    ctx.addLine(to: .init(x: r \* sin(Double(k) \* theta), y: r \* cos(Double(k) \* theta)))  
}  
ctx.closePath()  
ctx.fillPath()  

}) { info in
// 需要在这里将CGPattern初始化时传入info指针内存回收
Unmanaged.fromOpaque(info!).release()
}