iOS开发基础32-Quartz2D(一)图形上下文、路径绘制、矩阵变换与图片处理
Quartz 2D 深度解析:图形上下文、路径绘制、矩阵变换与图片处理
Quartz 2D 是 iOS/macOS 原生的二维绘图引擎,基于 Core Graphics 框架。本文系统梳理图形上下文、drawRect 绘制机制、路径拼接与绘制、上下文栈与矩阵变换、内存管理,以及图片水印、圆形裁剪、屏幕截图等实用功能,并补充 UIBezierPath 和 iOS 10+ UIGraphicsImageRenderer 现代 API。
一、Quartz 2D 简介
1. 什么是 Quartz 2D
Quartz 2D 是基于 Core Graphics(CG)框架的二维绘图引擎,同时支持 iOS 和 macOS。它提供 C 语言 API,用于绘制图形、文字、图像,生成 PDF 等。
2. 能完成的工作
- 绘制图形:线条、三角形、矩形、圆、弧、贝塞尔曲线
- 绘制文字
- 绘制/生成图片
- 读取/生成 PDF
- 截图、裁剪图片
- 自定义 UI 控件
3. 典型应用场景
- 图片裁剪(圆形头像)
- 涂鸦/画板
- 手势解锁
- 图表:折线图、饼状图、柱状图
- 自定义控件(自定义进度条、滑块等)
二、自定义 View 与 drawRect
1. 图形上下文(Graphics Context)
图形上下文是 CGContextRef 类型的数据,保存绘图状态(颜色、线宽、变换矩阵等)并决定输出目标。Quartz 2D 提供多种上下文:
| 上下文类型 | 输出目标 |
|---|---|
| 位图上下文(Bitmap) | UIImage(内存位图) |
| PDF 上下文 | PDF 文件 |
| 窗口上下文(Window) | 窗口(macOS) |
| 图层上下文(Layer) | CALayer |
| 打印机上下文(Printer) | 打印输出 |
2. drawRect: 方法
在自定义 View 中绘图,需重写 drawRect: 方法:
@interface CustomView : UIView
@end
@implementation CustomView
- (void)drawRect:(CGRect)rect {
// 1. 获取当前 View 的图形上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 2. 拼接路径
CGContextMoveToPoint(ctx, 10, 10);
CGContextAddLineToPoint(ctx, 100, 100);
// 3. 设置绘图状态
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 2);
// 4. 绘制路径(渲染到上下文)
CGContextStrokePath(ctx);
}
@end
3. drawRect: 调用时机
- View 第一次显示到屏幕时(被加入 UIWindow 并可见)
- 调用
setNeedsDisplay或setNeedsDisplayInRect:时(标记为需要重绘,下一个绘制周期自动调用 drawRect:)
drawRect:不能手动直接调用,必须通过setNeedsDisplay触发。手动调用不会获得正确的图形上下文。
4. 触发重绘
// 标记整个 View 需要重绘
[self.customView setNeedsDisplay];
// 标记指定区域需要重绘(性能更优)
[self.customView setNeedsDisplayInRect:CGRectMake(0, 0, 100, 100)];
三、绘图实现
1. 绘图三步
获取上下文 → 拼接路径 → 绘制路径(渲染)
// 1. 获取上下文
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 2. 拼接路径
CGContextMoveToPoint(ctx, 10, 10); // 起点
CGContextAddLineToPoint(ctx, 100, 100); // 线段
// 3. 绘制路径
CGContextStrokePath(ctx); // 空心(描边)
// CGContextFillPath(ctx); // 实心(填充)
2. 常用拼接路径函数
| 函数 | 说明 |
|---|---|
CGContextMoveToPoint |
设置起点 |
CGContextAddLineToPoint |
添加线段 |
CGContextAddLines |
批量添加线段(点数组) |
CGContextAddRect |
添加矩形 |
CGContextAddRects |
批量添加矩形 |
CGContextAddEllipseInRect |
添加椭圆(矩形内接椭圆,正方形时为圆) |
CGContextAddArc |
添加圆弧 |
CGContextAddArcToPoint |
添加圆弧(通过切点) |
CGContextAddCurveToPoint |
添加三次贝塞尔曲线(两个控制点) |
CGContextAddQuadCurveToPoint |
添加二次贝塞尔曲线(一个控制点) |
// 矩形
CGContextAddRect(ctx, CGRectMake(10, 10, 100, 50));
// 圆(正方形内接椭圆)
CGContextAddEllipseInRect(ctx, CGRectMake(10, 10, 100, 100));
// 圆弧:圆心(50,50),半径30,从0度到180度,逆时针(0)/顺时针(1)
CGContextAddArc(ctx, 50, 50, 30, 0, M_PI, 0);
// 三次贝塞尔曲线:终点(100,100),控制点1(50,0),控制点2(50,100)
CGContextMoveToPoint(ctx, 0, 50);
CGContextAddCurveToPoint(ctx, 50, 0, 50, 100, 100, 50);
Quartz 2D 坐标系原点在左下角,y 轴向上;UIKit 坐标系原点在左上角,y 轴向下。在
drawRect:中获取的上下文已由 UIKit 翻转过,直接使用左上角坐标系即可。
3. 常用绘制路径函数
| 函数 | 说明 |
|---|---|
CGContextStrokePath |
描边(空心) |
CGContextFillPath |
填充(实心,使用非零环绕规则) |
CGContextEOFillPath |
填充(使用奇偶规则) |
CGContextDrawPath |
按指定模式绘制 |
CGPathDrawingMode 枚举:
typedef CF_ENUM(int32_t, CGPathDrawingMode) {
kCGPathFill, // 填充
kCGPathEOFill, // 奇偶填充
kCGPathStroke, // 描边
kCGPathFillStroke, // 填充+描边
kCGPathEOFillStroke // 奇偶填充+描边
};
// 同时填充和描边
CGContextDrawPath(ctx, kCGPathFillStroke);
4. 绘图状态设置
在绘制路径前,可设置上下文的绘图状态:
// 描边颜色
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
// 或用 RGB
CGContextSetRGBStrokeColor(ctx, 1.0, 0, 0, 1.0);
// 填充颜色
CGContextSetFillColorWithColor(ctx, [UIColor blueColor].CGColor);
CGContextSetRGBFillColor(ctx, 0, 0, 1.0, 1.0);
// 线宽
CGContextSetLineWidth(ctx, 2);
// 线帽样式(端点)
CGContextSetLineCap(ctx, kCGLineCapRound); // 圆角端点
// 线条连接样式
CGContextSetLineJoin(ctx, kCGLineJoinRound); // 圆角连接
// 虚线样式
CGFloat lengths[] = {5, 5}; // 5pt 实线,5pt 空白
CGContextSetLineDash(ctx, 0, lengths, 2);
// 阴影
CGContextSetShadowWithColor(ctx, CGSizeMake(2, 2), 3, [UIColor grayColor].CGColor);
5. 图形上下文栈
上下文栈用于保存和恢复绘图状态,避免状态污染后续绘制:
// 保存当前状态(颜色、线宽、变换矩阵等)
CGContextSaveGState(ctx);
// 修改状态并绘制
CGContextSetStrokeColorWithColor(ctx, [UIColor redColor].CGColor);
CGContextSetLineWidth(ctx, 5);
CGContextStrokePath(ctx);
// 恢复到保存时的状态(红色和5线宽被撤销)
CGContextRestoreGState(ctx);
CGContextSaveGState/CGContextRestoreGState必须成对出现,保存几次就要恢复几次。
6. 矩阵操作(CTM)
CTM(Current Transformation Matrix,当前变换矩阵)作用于上下文中的所有路径,支持缩放、旋转、平移:
// 平移:x 方向移 50,y 方向移 50
CGContextTranslateCTM(ctx, 50, 50);
// 缩放:x 方向缩 0.5,y 方向缩 0.5
CGContextScaleCTM(ctx, 0.5, 0.5);
// 旋转:逆时针旋转 45 度(Quartz 逆时针为正)
CGContextRotateCTM(ctx, M_PI_4);
// 矩阵操作顺序很重要:先平移再旋转 ≠ 先旋转再平移
矩阵操作是累积的,且作用于后续所有绘制。如需临时变换,配合
CGContextSaveGState/CGContextRestoreGState使用。
7. 内存管理
使用含 Create 或 Copy 的函数创建的 Core Foundation 对象需手动释放:
// 创建路径
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, NULL, 0, 0);
CGPathAddLineToPoint(path, NULL, 100, 100);
// ... 使用
CGPathRelease(path); // 释放
// 创建颜色空间
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
// ... 使用
CGColorSpaceRelease(colorSpace);
// 创建渐变
CGGradientRef gradient = CGGradientCreateWithColorComponents(colorSpace, components, locations, 2);
// ... 使用
CGGradientRelease(gradient);
桥接到 Objective-C 对象(如
CGColorRef→UIColor)时,ARC 不管理 Core Foundation 对象,仍需手动CFRelease,除非使用__bridge_transfer转移所有权。
四、UIBezierPath(Objective-C 封装)
UIBezierPath 是 UIKit 对 CGPath 的 Objective-C 封装,API 更简洁,推荐优先使用:
- (void)drawRect:(CGRect)rect {
// 创建路径
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint:CGPointMake(10, 10)];
[path addLineToPoint:CGPointMake(100, 100)];
[path addArcWithCenter:CGPointMake(100, 100) radius:30 startAngle:0 endAngle:M_PI clockwise:YES];
// 设置样式
path.lineWidth = 2;
path.lineCapStyle = kCGLineCapRound;
path.lineJoinStyle = kCGLineJoinRound;
// 设置颜色并绘制
[[UIColor redColor] setStroke]; // 设置描边颜色
[[UIColor blueColor] setFill]; // 设置填充颜色
[path stroke]; // 描边
// [path fill]; // 填充
}
五、实用功能
1. 图片水印
利用位图图形上下文,将水印(文字/Logo)绘制到图片上:
- (UIImage *)watermarkImage:(UIImage *)originalImage text:(NSString *)text {
// 1. 开启位图上下文(size: 图片尺寸,opaque: 是否不透明,scale: 缩放因子,0 为屏幕 scale)
UIGraphicsBeginImageContextWithOptions(originalImage.size, NO, 0);
// 2. 绘制原图
[originalImage drawInRect:CGRectMake(0, 0, originalImage.size.width, originalImage.size.height)];
// 3. 绘制水印文字
NSDictionary *attrs = @{
NSFontAttributeName: [UIFont systemFontOfSize:20],
NSForegroundColorAttributeName: [UIColor whiteColor]
};
[text drawAtPoint:CGPointMake(20, originalImage.size.height - 40) withAttributes:attrs];
// 4. 从上下文中获取图片
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
// 5. 结束上下文
UIGraphicsEndImageContext();
return result;
}
UIGraphicsBeginImageContext(无 Options 版本)默认 scale=1,在 Retina 屏上会模糊;推荐使用UIGraphicsBeginImageContextWithOptions并将 scale 设为 0(自动取屏幕 scale)。
2. 圆形裁剪(头像)
利用裁剪区域将图片裁剪为圆形:
- (UIImage *)circleImage:(UIImage *)originalImage {
CGFloat side = MIN(originalImage.size.width, originalImage.size.height);
CGSize size = CGSizeMake(side, side);
UIGraphicsBeginImageContextWithOptions(size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 1. 添加圆形路径作为裁剪区域
CGRect rect = CGRectMake(0, 0, side, side);
CGContextAddEllipseInRect(ctx, rect);
// 2. 裁剪:后续绘制只在圆形区域内可见
CGContextClip(ctx);
// 3. 绘制原图(被裁剪为圆形)
[originalImage drawInRect:rect];
UIImage *result = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return result;
}
CGContextClip会将当前路径作为裁剪区域,后续所有绘制都被限制在该区域内。裁剪是上下文状态,可用CGContextSaveGState/RestoreGState临时使用。
3. 屏幕截图
方式一:renderInContext:(兼容旧系统)
- (UIImage *)snapshotView:(UIView *)view {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
CGContextRef ctx = UIGraphicsGetCurrentContext();
// 将 view 的 layer 渲染到上下文
[view.layer renderInContext:ctx];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
方式二:drawViewHierarchyInRect:afterScreenUpdates:(iOS 7+,更高效)
- (UIImage *)snapshotView:(UIView *)view {
UIGraphicsBeginImageContextWithOptions(view.bounds.size, NO, 0);
// afterScreenUpdates: YES 表示等待当前动画/更新完成后再截图
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
UIImage *image = UIGraphicsGetImageFromCurrentImageContext();
UIGraphicsEndImageContext();
return image;
}
drawViewHierarchyInRect:afterScreenUpdates:比renderInContext:性能更好,且能正确捕获 UIKit 特效(如 UIVisualEffectView 模糊效果)。iOS 7+ 推荐使用。
方式三:UIGraphicsImageRenderer(iOS 10+,现代 API)
- (UIImage *)snapshotView:(UIView *)view API_AVAILABLE(ios(10.0)) {
UIGraphicsImageRenderer *renderer = [[UIGraphicsImageRenderer alloc] initWithSize:view.bounds.size];
return [renderer imageWithActions:^(UIGraphicsImageRendererContext *rendererContext) {
[view drawViewHierarchyInRect:view.bounds afterScreenUpdates:YES];
}];
}
UIGraphicsImageRenderer自动管理上下文的创建和结束,无需手动调用UIGraphicsBeginImageContext/EndImageContext,API 更安全简洁,iOS 10+ 推荐使用。
六、Swift 版本对照
drawRect 绘图
class CustomView: UIView {
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.move(to: CGPoint(x: 10, y: 10))
ctx.addLine(to: CGPoint(x: 100, y: 100))
ctx.addArc(center: CGPoint(x: 100, y: 100), radius: 30, startAngle: 0, endAngle: .pi, clockwise: false)
ctx.setStrokeColor(UIColor.red.cgColor)
ctx.setFillColor(UIColor.blue.cgColor)
ctx.setLineWidth(2)
ctx.setLineCap(.round)
ctx.drawPath(using: .fillStroke)
}
}
UIBezierPath
override func draw(_ rect: CGRect) {
let path = UIBezierPath()
path.move(to: CGPoint(x: 10, y: 10))
path.addLine(to: CGPoint(x: 100, y: 100))
path.lineWidth = 2
UIColor.red.setStroke()
path.stroke()
}
图片水印
func watermark(image: UIImage, text: String) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: image.size)
return renderer.image { _ in
image.draw(in: CGRect(origin: .zero, size: image.size))
let attrs: [NSAttributedString.Key: Any] = [
.font: UIFont.systemFont(ofSize: 20),
.foregroundColor: UIColor.white
]
text.draw(at: CGPoint(x: 20, y: image.size.height - 40), withAttributes: attrs)
}
}
圆形裁剪
func circleImage(_ image: UIImage) -> UIImage {
let side = min(image.size.width, image.size.height)
let size = CGSize(width: side, height: side)
let renderer = UIGraphicsImageRenderer(size: size)
return renderer.image { ctx in
let rect = CGRect(origin: .zero, size: size)
ctx.cgContext.addEllipse(in: rect)
ctx.cgContext.clip()
image.draw(in: rect)
}
}
屏幕截图
func snapshot(view: UIView) -> UIImage {
let renderer = UIGraphicsImageRenderer(size: view.bounds.size)
return renderer.image { _ in
view.drawHierarchy(in: view.bounds, afterScreenUpdates: true)
}
}
矩阵变换与上下文栈
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
ctx.translateBy(x: 50, y: 50)
ctx.rotate(by: .pi / 4)
ctx.scaleBy(x: 0.5, y: 0.5)
// 绘制变换后的图形
ctx.setFillColor(UIColor.red.cgColor)
ctx.fill(CGRect(x: 0, y: 0, width: 100, height: 100))
ctx.restoreGState()
// 后续绘制恢复到变换前的状态
}
七、总结
- Quartz 2D:基于 Core Graphics 的二维绘图引擎,通过 CGContextRef 图形上下文绘制。
- drawRect::自定义 View 绘图的核心方法,只能通过 setNeedsDisplay 触发,不能手动调用;在此方法中通过 UIGraphicsGetCurrentContext 获取上下文。
- 绘图三步:获取上下文 → 拼接路径(Move/AddLine/AddRect/AddArc/贝塞尔曲线)→ 绘制路径(Stroke/Fill/DrawPath)。
- 绘图状态:描边/填充颜色、线宽、线帽、连接样式、虚线、阴影,通过 CGContextSet 系列函数设置。
- 上下文栈:CGContextSaveGState/RestoreGState 成对使用,保存/恢复绘图状态。
- 矩阵变换:CTM 支持 Translate/Scale/Rotate,作用于后续所有绘制,累积生效。
- 内存管理:含 Create/Copy 的 Core Foundation 对象需手动 Release(CGPathRelease/CGColorSpaceRelease 等)。
- UIBezierPath:UIKit 对 CGPath 的 Objective-C 封装,API 更简洁,推荐优先使用。
- 图片处理:位图上下文(UIGraphicsBeginImageContextWithOptions)实现水印、圆形裁剪(CGContextClip)、截图(renderInContext/drawViewHierarchyInRect)。
- 现代 API:iOS 7+ 推荐 drawViewHierarchyInRect 截图;iOS 10+ 推荐 UIGraphicsImageRenderer 管理位图上下文,自动管理生命周期。
- 坐标系:Quartz 原点在左下角,UIKit 已在 drawRect 中翻转,直接使用左上角坐标系即可。

浙公网安备 33010602011771号