Quartz 2D绘图引擎使用指南

简介

Quartz 2D也就是Core Graphics,是为Apple为不同设备提供的统一性、轻量级的二维绘图引擎,包括iOS、iPadOS、macOS和tvOS。Quartz 2D虽然是用C语言写的,但用起来并不麻烦,而且功能强大,包含的功能有:

  • 绘图上下文
  • 颜色及颜色空间管理
  • 绘制路径,比如贝塞尔曲线
  • 图形变换
  • 阴影和渐变
  • 离屏渲染
  • PDF相关

除此之外,Quartz 2D还可以与其他图像处理框架一起工作,比如Core Image、Core Video、OpenGL(目前为Metal)等。

但实际上,Quartz 2D != Core Graphics,它们之间的关系是:Quartz 2D是Core Graphics框架用于绘图的API,Core Graphics框架能够提供更多的能力,比如动画、图片处理。

Quartz 2D is an API of the Core Graphics framework that implements drawing.
Quartz Core is a framework that includes APIs for animation and image processing.

绘画上下文

绘图上下文drawing context是所有绘图操作发生的地方,比如位图、PDF、视图、窗口、图层等。对于每一种绘图上下文,里面都会封装进去所有相关信息,比如颜色、分辨率、字体、颜色空间,以及设备相关信息等。

在iOS中,如果要将某个绘图对象通过屏幕显示出来,一般需要定义一个UIView对象,并实现它的 drawRect: 方法,以进行具体的绘制操作。在程序运行起来之后,当这个视图对象变得可见时或者其上的内容需要更新时,drawRect: 会被自动调用。而在此期间,视图对象会自动创建并配置相关的环境上下文信息,以让绘图过程立即执行。在 drawRect:方法中,可以通过UIGraphicsGetCurrentContext得到当前绘图上下文对象。

创建不同的上下文对象可以参考:不同绘图对象的上下文对象的创建方式。

图形状态

针对不同的绘图对象和操作,有与之对应的参数决定不同的绘图行为,这些参数也就是图形的状态,比如当画一条线时,需要设置颜色color、线宽width、连接类型join style、端点样式cap style、虚线类型dash等参数。

对于上下文信息来说,一般会持有多个图形状态信息,它们会以栈的形式被保存,通过CGContextSaveGState可以将当前状态信息入栈,而通过CGContextRestoreGState会弹出栈顶的状态作为当前的绘图状态。

路径

路径Path是由几何方式定义的不同形状或子路径构成的组合,比如直线、曲线、以及圆、矩形等,即可开放,也可闭合。也就是说,Path由以下这些部分组合而成:

  • 直线Line
    • 实线
    • 虚线
  • 曲线Curve
    • 弧线Arc
    • 贝塞尔曲线
  • 子路径
    • 圆Circle
    • 矩形Rectangle
    • 椭圆Ellipse
    • ...

当然对于所有Path,创建之后还要对其进行装饰,也就是真正的绘制过程,比如设置线宽、颜色,以及描边或填充。

创建

创建Path有两种方式,其一是直接将路径的细节填入上下文对象CGContextRef中,每操作一次都需要调用一次 GContextMove* 或 CGContextAdd* 函数;其二是将所有细节放入一个CGPathRef对象中,等构建完成之后再加入上下文中。

相比较而言,前一种方法适用于一次性操作,简单快速,后一种则更适用于需要重复使用的场景,复用性强。同时,它们的API是对应的。

说明 CGContext CGPath
初始化 CGContextBeginPath CGPathCreateMutable
设置开始点 CGContextMoveToPoint CGPathMoveToPoint
添加直线 CGContextAddLineToPoint CGPathAddLineToPoint
添加曲线 CGContextAddCurveToPoint CGPathAddCurveToPoint
添加椭圆 CGContextAddEllipseToPoint CGPathAddEllipseToPoint
添加弧线 CGContextAddArc CGPathAddARc
添加矩形 CGContextAddRect CGPathAddRect
闭合路径 CGContextClosePath CGPathCloseSubpath

对于可复用的Path,在绘制完成之后可通过CGContextAddPath加入到当前的上下文中。

绘制

当创建好Path之后,就进行真正的绘制过程,这一步可分为描边Stroke或填充Fill两种类型。

描边需要考虑的参数包括:

  • 线宽,CGContextSetLineWidth
  • 颜色,CGContextSetStrokeColorWithColor
  • 模式,CGContextSetStrokePattern
  • 连接类型,CGContextSetLineJoin
  • 端头形式,CGContextSetLineCap
  • 虚线样式,CGContextSetLineDash
  • ...

如果填充,自然只需要设置一个参数——颜色,通过CGContextSetFillColorWithColor即可完成。

其他

当绘制视图存在背景色,或者已经有其他对象时,就涉及到另一个概念:混合,表示如何处理上层对象对于下层对象的覆盖行为。默认会根据下面这个公式计算上层对象的可见度:

result = (alpha * foreground) + (1 - alpha) * background

当然还有其他混合样式,具体可以通过CGContextSetBlendMode按需进行设置,可选值包括如下,效果见:混合模式效果。

typedef CF_ENUM (int32_t, CGBlendMode) {
    /* Available in Mac OS X 10.4 & later. */
    kCGBlendModeNormal,
    kCGBlendModeMultiply,
    kCGBlendModeScreen,
    kCGBlendModeOverlay,
    kCGBlendModeDarken,
    kCGBlendModeLighten,
    kCGBlendModeColorDodge,
    kCGBlendModeColorBurn,
    kCGBlendModeSoftLight,
    kCGBlendModeHardLight,
    kCGBlendModeDifference,
    kCGBlendModeExclusion,
    kCGBlendModeHue,
    kCGBlendModeSaturation,
    kCGBlendModeColor,
    kCGBlendModeLuminosity,

    /* Available in Mac OS X 10.5 & later. R, S, and D are, respectively,
       premultiplied result, source, and destination colors with alpha; Ra,
       Sa, and Da are the alpha components of these colors.

       The Porter-Duff "source over" mode is called `kCGBlendModeNormal':
         R = S + D*(1 - Sa)

       Note that the Porter-Duff "XOR" mode is only titularly related to the
       classical bitmap XOR operation (which is unsupported by
       CoreGraphics). */

    kCGBlendModeClear,                  /* R = 0 */
    kCGBlendModeCopy,                   /* R = S */
    kCGBlendModeSourceIn,               /* R = S*Da */
    kCGBlendModeSourceOut,              /* R = S*(1 - Da) */
    kCGBlendModeSourceAtop,             /* R = S*Da + D*(1 - Sa) */
    kCGBlendModeDestinationOver,        /* R = S*(1 - Da) + D */
    kCGBlendModeDestinationIn,          /* R = D*Sa */
    kCGBlendModeDestinationOut,         /* R = D*(1 - Sa) */
    kCGBlendModeDestinationAtop,        /* R = S*(1 - Da) + D*Sa */
    kCGBlendModeXOR,                    /* R = S*(1 - Da) + D*(1 - Sa) */
    kCGBlendModePlusDarker,             /* R = MAX(0, (1 - D) + (1 - S)) */
    kCGBlendModePlusLighter             /* R = MIN(1, S + D) */
};

另外,还可以根据Path的形状对当前图形实施剪裁,扮演类似蒙版的效果。

CGContextBeginPath (context);
CGContextAddArc (context, w/2, h/2, ((w>h) ? h : w)/2, 0, 2*PI, 0);
CGContextClosePath (context);
CGContextClip (context);

通过路径画正弦曲线的代码如下。

- (void)drawRect:(CGRect)rect{
    
    CGContextRef ctx = UIGraphicsGetCurrentContext();

    [self drawPathWithContext:ctx baseline:self.center.y-100 height:160];
}

- (void)drawPathWithContext:(CGContextRef)ctx baseline:(CGFloat)baseline height:(CGFloat)height {

    // 外部矩形
    CGContextSaveGState(ctx);
    
    CGContextMoveToPoint(ctx, 0, baseline);
    CGContextSetRGBStrokeColor(ctx, 1, 1, 1, .5);
    CGContextSetLineWidth(ctx, 2);
    CGContextStrokeRect(ctx, CGRectMake(0, baseline, CGRectGetWidth(self.bounds), height));
    
    CGContextRestoreGState(ctx);

    /// 正弦曲线
    CGFloat radius = 50;
    CGFloat tickThickNess = 2;
    CGPoint center = CGPointMake(CGRectGetWidth(self.bounds)/2, baseline+height/2);
    CGMutablePathRef cgpath = CGPathCreateMutable();
    
    // 坐标轴
    CGPathMoveToPoint(cgpath, NULL, 10, center.y);
    CGPathAddLineToPoint(cgpath, NULL, CGRectGetWidth(self.bounds)-20, center.y);
    CGPathMoveToPoint(cgpath, NULL, center.x, baseline+5);
    CGPathAddLineToPoint(cgpath, NULL, center.x, baseline+height-10);
    CGContextAddPath(ctx, cgpath);
    
    CGContextSaveGState(ctx); {
        CGContextSetLineWidth(ctx,tickThickNess);
        CGContextSetLineJoin(ctx, kCGLineJoinRound);
        CGContextSetFlatness(ctx, 1);
        CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1);
        CGContextStrokePath(ctx);
    }
    CGContextRestoreGState(ctx);
    
    // 刻度
    CGPathMoveToPoint(cgpath, NULL, 10, center.y-tickThickNess);
    CGPathAddLineToPoint(cgpath, NULL, CGRectGetWidth(self.bounds)-20, center.y-tickThickNess);
    CGPathMoveToPoint(cgpath, NULL, center.x+tickThickNess, baseline+5);
    CGPathAddLineToPoint(cgpath, NULL, center.x+tickThickNess, baseline+height-10);
    CGContextAddPath(ctx, cgpath);
    
    CGContextSaveGState(ctx); {
        const CGFloat lengths[] = {2,5};
        CGContextSetLineWidth(ctx, tickThickNess);
        CGContextSetLineDash(ctx, 0, lengths, 2);
        CGContextSetLineCap(ctx, kCGLineCapSquare);
        CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1);
        CGContextStrokePath(ctx);
    }
    CGContextRestoreGState(ctx);
    
    // 右半部分圆弧
    CGPathMoveToPoint(cgpath, NULL, center.x+radius*2, center.y);
    CGPathAddArc(cgpath, NULL, center.x+radius, center.y, radius, 0, M_PI, YES);
    
    // 左半部分
    CGPathMoveToPoint(cgpath, NULL, center.x, center.y);
    CGPathAddArc(cgpath, NULL, center.x-radius, center.y, radius, 0, M_PI, NO);
    
    CGContextAddPath(ctx, cgpath);
    
    // 状态
    CGContextSaveGState(ctx); {
        CGContextSetLineWidth(ctx, 1);
        CGContextSetLineJoin(ctx, kCGLineJoinRound);
        CGContextSetFlatness(ctx, 1);
        CGContextSetRGBStrokeColor(ctx, 1, 1, 1, 1);
        CGContextStrokePath(ctx);
    }
    
    CGContextRestoreGState(ctx);
    CGPathRelease(cgpath);
}

变换

Quartz 2D定义了两个完全独立的坐标空间:用户空间和设备空间,用户空间表示绘图内容,设备空间表示设备的原生分辨率,它们之间没有什么关联。当需要将绘图内容显示在屏幕上,或者打印输出时,Quartz会自动完成从用户坐标空间向设备坐标空间的转换。

如果想对绘图内容做一些变换,我们只需要操作当前变化矩阵CTM(current transformation matrix),也可以通过放射变换实现。其中变换类型一共分为三种,但它们之间可以组合:

  • 平移
  • 缩放
  • 旋转

使用

操作CTM

默认情况下,CTM是伴随着Context而创建的一个单位矩阵,通过直接操作CTM,可快速实现对当前Context的变换操作,方法有:

  • 平移:CGContextTranslateCTM
  • 旋转:CGContextRotateCTM
  • 缩放:CGContextScaleCTM

放射变换

放射变换和CTM类似,只不过前者先操作一个3x3矩阵,然后将此矩阵作用于上下文中,实现真正的变换。方法有:

  • CGAffineTransformMakeTranslation
  • CGAffineTransformTranslate
  • CGAffineTransformTranslate
  • CGAffineTransformRotate
  • CGAffineTransformMakeScale
  • CGAffineTransformScale
- (void)drawCicleToPath:(CGMutablePathRef)path angle:(CGFloat)angle {
    const CGPoint center = self.center;
    const CGFloat radius = 100.0;
          CGFloat ratio  = 4.0;
    
    CGFloat width, height, scale;
    
    width = CGRectGetWidth(self.bounds)*.5;
    height = width/ratio;
    scale = [self cicleScaleAtAngle:angle];
    
    // 注意,旋转中心是anchorPoint,而不是center
    CGAffineTransform transform = CGAffineTransformIdentity;
    
    transform = CGAffineTransformMakeRotation(angle/100);
    transform = CGAffineTransformTranslate(transform, -cos(angle), -sin(angle));
    transform = CGAffineTransformScale(transform, 1-scale*.5, .5*(1+scale));
    
    CGPathAddArc(path,
                 &transform,
                 center.x,
                 center.y,
                 radius,
                 0,
                 2*M_PI-1e-5, NO);
}

背后的数学原理见:放射变换的数学原理。

渐变

在Quartz 2D中有两个渐变实现CGShapingRef和CGGradientRef,都可以用来实现线性渐变和径向渐变。

CGShadingRef和CGGradientRef之间的区别在于,前者的自由度更大,可以实现自定义渐变样式,使用起来比较麻烦;而后者是前者的一个子集,使用更加方便,只需要提供一个渐变颜色序列和位置,就能够实现。

CGGradient

@implementation QuartzGradientViewExample

CGGradientRef CreateGradient() {
    CGGradientRef gradient;
    CGColorSpaceRef colorSpace;
    size_t num_locations    = 2;
    CGFloat locations[2]    = { 0.0, 1.0};
    CGFloat components[8]   = { 0.95, 0.3, 0.4, 1.0,
                                0.10, 0.20, 0.30, 1.0 };
    
    colorSpace = CGColorSpaceCreateWithName(kCGColorSpaceGenericGray);
    gradient = CGGradientCreateWithColorComponents(colorSpace,
                                                   components,
                                                   locations,
                                                   num_locations);
    
    return gradient;
}

- (void)drawRect:(CGRect)rect {
    
    CGContextDrawLinearGradient(UIGraphicsGetCurrentContext(),
                                CreateGradient(),
                                CGPointMake(0, 0),
                                CGPointMake(CGRectGetWidth(self.bounds), CGRectGetHeight(self.bounds)),
                                kCGGradientDrawsAfterEndLocation);
    
    CGContextDrawRadialGradient(UIGraphicsGetCurrentContext(),
                                CreateGradient(),
                                self.center, 10,
                                self.center, 200,
                                kCGGradientDrawsAfterEndLocation);
    
}


@end

CGShading

@implementation QuartzShadingViewExample

static void myCalculateShadingValues (void *info,
                            const CGFloat *in,
                            CGFloat *out) {
    CGFloat v;
    size_t k, components;
    static const CGFloat c[] = {1, 0, .5, 0 };
 
    components = (size_t)info;
 
    v = *in;
    for (k = 0; k < components -1; k++)
        *out++ = c[k] * v;
     *out++ = 1;
}


static CGFunctionRef myGetFunction (CGColorSpaceRef colorspace) {
    size_t numComponents;
    static const CGFloat input_value_range [2] = { 0, 1 };
    static const CGFloat output_value_ranges [8] = { 0, 1, 0, 1, 0, 1, 0, 1 };
    static const CGFunctionCallbacks callbacks = { 0,
                                &myCalculateShadingValues,
                                NULL };
 
    numComponents = 1 + CGColorSpaceGetNumberOfComponents (colorspace);
    return CGFunctionCreate ((void *) numComponents,
                                1,
                                input_value_range,
                                numComponents,
                                output_value_ranges,
                                &callbacks);
}

void myPaintRadialShading (CGContextRef myContext, CGRect bounds) {
    CGPoint startPoint, endPoint;
    CGFloat startRadius, endRadius;
    CGAffineTransform myTransform;
    CGColorSpaceRef colorspace;
    CGShadingRef shading;
    CGFunctionRef shadingFuction;
    
    CGFloat width = bounds.size.width;
    CGFloat height = bounds.size.height;
 
    startPoint = CGPointMake(0.25,0.3);
    startRadius = .1;
    endPoint = CGPointMake(.7,0.7);
    endRadius = .25;
 
    colorspace = CGColorSpaceCreateDeviceRGB();
    shadingFuction = myGetFunction (colorspace);
 
    shading = CGShadingCreateRadial (colorspace,
                            startPoint, startRadius,
                            endPoint, endRadius,
                            shadingFuction,
                            false, false);
 
    myTransform = CGAffineTransformMakeScale (width, height);
    CGContextConcatCTM (myContext, myTransform);
    CGContextSaveGState (myContext);
 
    CGContextClipToRect (myContext, CGRectMake(0, 0, 1, 1));
    CGContextSetRGBFillColor (myContext, 1, 1, 1, 1);
    CGContextFillRect (myContext, CGRectMake(0, 0, 1, 1));
 
    CGContextDrawShading (myContext, shading);
    CGColorSpaceRelease (colorspace);
    CGShadingRelease (shading);
    CGFunctionRelease (shadingFuction);
 
    CGContextRestoreGState (myContext);
}

- (void)drawRect:(CGRect)rect{
    myPaintRadialShading(UIGraphicsGetCurrentContext(), self.bounds);
}

@end

Other

模式

Pattern,模式或图案,一小段可重复绘制的对象,可用来填充图形表明或描边。

#define H_PATTERN_SIZE 16
#define V_PATTERN_SIZE 18
 
void MySquaredPattern (void *info, CGContextRef myContext) {
    CGFloat subunit = 5; // the pattern cell itself is 16 by 18
 
    CGRect  rect1 = {{0,0}, {subunit, subunit}},
            rect2 = {{subunit, subunit}, {subunit, subunit}},
            rect3 = {{0,subunit}, {subunit, subunit}},
            rect4 = {{subunit,0}, {subunit, subunit}};
 
    CGContextSetRGBFillColor (myContext, 1, 1, 1, 0.5);
    CGContextFillRect (myContext, rect1);
    CGContextSetRGBFillColor (myContext, 1, 0, 0, 0.5);
    CGContextFillRect (myContext, rect2);
    CGContextSetRGBFillColor (myContext, 0, 1, 0, 0.5);
    CGContextFillRect (myContext, rect3);
    CGContextSetRGBFillColor (myContext, .5, 0, .5, 0.5);
    CGContextFillRect (myContext, rect4);
}

void MyPatternPaint(CGContextRef context, CGRect rect) {
    CGPatternRef    pattern;
    CGColorSpaceRef patternSpace;
    CGFloat         alpha = 1;
    static const    CGPatternCallbacks callbacks = {
                        0,
                        &MySquaredPattern,
                        NULL
                    };
    patternSpace =  CGColorSpaceCreatePattern(NULL);

    CGContextSaveGState(context);
    CGContextSetFillColorSpace(context, patternSpace);
    CGColorSpaceRelease(patternSpace);
    
    pattern = CGPatternCreate(NULL,
                              CGRectMake(0, 0, H_PATTERN_SIZE, V_PATTERN_SIZE),
                              CGAffineTransformMake(1, 0, 0, 1, 0, 0),
                              H_PATTERN_SIZE,
                              V_PATTERN_SIZE,
                              kCGPatternTilingConstantSpacing,
                              true,
                              &callbacks);
    
    CGContextSetFillPattern(context, pattern, &alpha);
    CGPatternRelease(pattern);
    CGContextFillRect(context, rect);
    CGContextRestoreGState(context);
    
}

- (void)drawRect:(CGRect)rect {
    CGRect square = CGRectMake(0, (CGRectGetHeight(self.bounds)-CGRectGetWidth(self.bounds))/2, CGRectGetWidth(self.bounds), CGRectGetWidth(self.bounds));
    
    MyPatternPaint(UIGraphicsGetCurrentContext(), square);
}

透明图层

图层和PS中的图层概念类似,可以给层对象分别设置不同的效果,然后合成一体。

通过执行CGContextBeginTransparencyLayer,即可开启一个全新的图层,CGContextBeginTransparencyLayer则表示关闭当前图层。

- (void)drawRect:(CGRect)rect {
    NSUInteger   count      = 100;
    CGContextRef context    = UIGraphicsGetCurrentContext();
    CGFloat      angleSeg   = M_PI*2/count;
    
    CGContextSaveGState(context);
    CGContextSetLineWidth(context, 1.f);
    
    for (int i=0; i

参考

  • Apple官方文档
  • Stack Overflow:Quartz 2D VS Core Graphics

你可能感兴趣的:(Quartz 2D绘图引擎使用指南)