Core Graphics是iOS平台上功能强大的2D绘图框架,几乎所有自定义UI控件的绘制都离不开它。与使用UIKit的现有控件相比,通过Core Graphics绘制自定义控件能够实现更灵活的视觉效果,例如复杂的渐变、阴影、路径裁剪以及动画过渡。但是在实际开发中,很多开发者容易在绘图状态管理、渐变与阴影的组合使用上遇到问题,导致绘制结果异常或性能下降。本文将以构建一个圆形进度指示器为例,深入剖析Core Graphics中的上下文状态栈、渐变填充与阴影绘制三大核心主题。

理解Core Graphics上下文与状态栈
在Core Graphics中,所有的绘图操作都发生在一个称为图形上下文(Graphics Context)的对象中,通常通过CGContextRef表示。这个上下文保存了当前的绘图状态,包括颜色、线宽、变换矩阵、裁剪区域、混合模式等。当你修改其中任意一项时,后续的绘图操作都会受到这些状态的影响。如果不小心修改了全局状态而没有恢复,就可能导致不可预料的绘制结果,比如后续的图形被意外填充了错误的颜色或者被裁剪掉。
为了解决状态管理的问题,Core Graphics提供了状态栈(Graphics State Stack)机制。你可以使用CGContextSaveGState函数将当前上下文状态压入栈中,之后进行的任何修改都只影响当前状态。当不再需要这些修改时,调用CGContextRestoreGState从栈中弹出之前保存的状态,从而恢复到保存时的状态。这类似于编程中的“保存现场”和“恢复现场”,可以避免状态污染。
下面是一个简单的Swift代码示例,演示了如何使用状态栈来绘制两个不同颜色的矩形,同时确保状态互不干扰。注意代码中CGContextSaveGState和CGContextRestoreGState的配对使用,这是保证状态管理正确的关键。
override func draw(_ rect: CGRect) {
guard let context = UIGraphicsGetCurrentContext() else { return }
// 保存初始状态
context.saveGState()
// 设置红色填充并绘制第一个矩形
context.setFillColor(UIColor.red.cgColor)
context.fill(CGRect(x: 20, y: 20, width: 100, height: 100))
// 恢复初始状态
context.restoreGState()
// 此时颜色恢复为默认黑色,绘制第二个矩形
context.fill(CGRect(x: 150, y: 20, width: 100, height: 100))
}在上面的代码中,第一个矩形使用了红色填充,而第二个矩形使用了默认的黑色填充。这是因为在绘制第一个矩形之前保存了状态,修改填充色后绘制,然后立即恢复,所以第二个矩形不会受到红色的影响。如果不使用状态栈,第二个矩形也会被填充为红色,除非手动将颜色改回。状态栈在绘制复杂控件时尤为重要,它允许你在局部范围内修改绘图属性,同时不影响其他部分的绘制逻辑。
使用CGGradient实现渐变填充
渐变填充是自定义控件中常见的视觉效果,可以用于背景、进度条、按钮高亮等场景。Core Graphics通过CGGradient对象来定义渐变,支持线性渐变(Linear Gradient)和径向渐变(Radial Gradient)两种类型。创建CGGradient对象需要指定颜色空间(Color Space)、颜色组件数组、位置数组以及颜色数量。颜色组件数组是一个CGFloat数组,每个颜色由四个分量(红、绿、蓝、透明度)表示,多个颜色依次排列;位置数组则定义了每个颜色在渐变中的相对位置,取值范围为0.0到1.0。
在Swift中,可以使用CGGradient的初始化方法,例如CGGradient(colorsSpace:colorComponents:locations:count:)。需要注意的是,颜色组件数组和位置数组需要以UnsafePointer的形式传递,通常使用数组的withUnsafeBufferPointer方法获取指针。渐变创建完成后,使用CGContextDrawLinearGradient或CGContextDrawRadialGradient函数将其绘制到上下文中。
以下代码展示了如何创建一个从蓝色到紫色的线性渐变,并填充一个矩形区域。为了简化指针操作,代码中使用了withUnsafeBufferPointer来获取数组的指针。
func drawLinearGradient(in context: CGContext, rect: CGRect) {
// 定义颜色组件:蓝色 (0.0, 0.0, 1.0, 1.0) 和 紫色 (0.5, 0.0, 0.5, 1.0)
let colorComponents: [CGFloat] = [
0.0, 0.0, 1.0, 1.0, // 蓝色
0.5, 0.0, 0.5, 1.0 // 紫色
]
// 定义渐变位置:起点0.0,终点1.0
let locations: [CGFloat] = [0.0, 1.0]
// 获取颜色空间
let colorSpace = CGColorSpaceCreateDeviceRGB()
// 创建渐变对象
var gradient: CGGradient?
colorComponents.withUnsafeBufferPointer { compPtr in
locations.withUnsafeBufferPointer { locPtr in
gradient = CGGradient(colorSpace: colorSpace,
colorComponents: compPtr.baseAddress!,
locations: locPtr.baseAddress!,
count: locations.count)
}
}
guard let gradient = gradient else { return }
// 绘制线性渐变
let startPoint = CGPoint(x: rect.minX, y: rect.minY)
let endPoint = CGPoint(x: rect.minX, y: rect.maxY)
context.drawLinearGradient(gradient,
start: startPoint,
end: endPoint,
options: [.drawsBeforeStartLocation, .drawsAfterEndLocation])
}径向渐变的使用方式类似,但需要指定圆心、半径以及渐变的起止位置。径向渐变常用于模拟光源效果,例如圆形按钮的高光。在实际项目中,你可以根据需要组合多个渐变,并通过状态栈隔离不同的渐变区域。需要注意的是,渐变对象的创建可能会消耗一定资源,如果控件频繁重绘,建议缓存CGGradient对象以避免重复创建。
除了线性与径向渐变,Core Graphics还提供了CGGradientDrawingOptions来控制渐变是否在起止位置之外延伸,例如.drawsBeforeStartLocation和.drawsAfterEndLocation。这些选项能够避免渐变边缘出现空白或颜色突变的问题。
阴影绘制与性能优化
阴影能够增加UI的立体感和层次感,但如果不加控制,也可能成为性能瓶颈。Core Graphics中设置阴影有两种方式:一种是使用CGContextSetShadow,它会为后续绘制的所有图形添加阴影;另一种是使用CGContextSetShadowWithColor,可以指定阴影颜色。阴影参数包括偏移量(offset)和模糊值(blur)。偏移量决定了阴影在水平和垂直方向上的位移,模糊值则决定了阴影边缘的柔化程度。
以下代码演示了在绘制一个圆形时添加阴影。首先设置阴影参数,然后使用CGContextFillEllipseInRect填充圆形,阴影会自动应用到该图形上。需要注意的是,阴影是在路径填充或描边时计算的,因此必须在绘制路径之前设置阴影属性。
func drawCircleWithShadow(in context: CGContext) {
// 设置阴影:偏移(3, 5),模糊值10,颜色为半透明黑色
context.setShadow(offset: CGSize(width: 3, height: 5),
blur: 10,
color: UIColor.black.withAlphaComponent(0.5).cgColor)
// 设置填充颜色为白色
context.setFillColor(UIColor.white.cgColor)
// 绘制圆形
let circleRect = CGRect(x: 50, y: 50, width: 100, height: 100)
context.fillEllipse(in: circleRect)
// 清除阴影设置,避免影响后续绘制
context.setShadow(offset: .zero, blur: 0, color: nil)
}阴影绘制会消耗额外的计算资源,尤其是当阴影模糊值较大或者路径非常复杂时,性能开销会显著增加。为了提高性能,可以采用以下优化策略:第一,对于不透明的简单形状,使用shadowPath来明确指定阴影路径,避免Core Graphics自动计算;第二,在视图的draw(_:)方法中尽量少地改变阴影参数,或者使用CALayer的阴影属性(如layer.shadowRadius、layer.shadowPath)来替代Core Graphics中的阴影,因为CALayer的阴影可以利用GPU加速;第三,如果阴影不需要动态变化,可以将其缓存为图片,避免每次重绘都重新计算。
在实际开发中,将阴影与渐变结合使用时,需要注意绘制的顺序。通常先绘制带有阴影的形状,再在其上叠加渐变填充,或者反过来。通过使用多个状态栈保存不同的组合,可以灵活控制阴影与渐变的叠加效果。例如,可以先设置阴影并填充一个基础形状,然后恢复到无阴影状态,再使用渐变填充同一形状,从而产生带有阴影的渐变效果。
除了阴影性能和状态管理,坐标系的处理也是绘制自定义控件时容易出错的地方。Core Graphics默认的坐标系原点在左下角,而UIKit的坐标系原点在左上角。在UIView的draw(_:)方法中,UIKit已经对上下文进行了翻转,使得坐标系与UIKit一致,因此你可以直接使用UIKit的坐标系统进行绘制。但是当直接创建位图上下文时,需要注意坐标原点的差异。合理地利用CGContextSaveGState和CGContextRestoreGState来管理坐标变换也是状态栈的常见应用场景。
综上所述,掌握Core Graphics的状态栈、渐变与阴影绘制,能够显著提升自定义控件开发的效率与质量。通过合理地保存和恢复状态,你可以在不干扰其他绘制逻辑的情况下灵活地添加视觉效果。渐变填充和阴影绘制虽然强大,但需要结合实际场景进行性能调优,尤其是在列表等频繁重绘的场景中。希望本文提供的原理讲解和代码示例能够帮助你在iOS开发中更加自如地驾驭Core Graphics。
Core GraphicsiOS自定义UI控件渐变填充修改时间:2026-08-20 07:51:14