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 中翻转,直接使用左上角坐标系即可。

posted @ 2015-08-04 21:23  Mr.陳  阅读(502)  评论(0)    收藏  举报