iOS开发-Quartz 2D基础篇

Quartz 2D是一个二维图形绘制引擎,支持iOS环境和Mac OS X环境。我们可以使用Quartz 2D API来实现许多功能,如基本路径的绘制、透明度、描影、绘制阴影、透明层、颜色管理、反锯齿、PDF文档生成和PDF元数据访问。

绘制原理

Quartz 2D在图像绘制中使用了绘画者模型。在绘画者模型中,每个连续的绘制操作都是将一个绘制层放置于一个画布(canvas),我们可以理解为图像的绘制即为在画布上绘画,而绘画操作的顺序同时影响着我们绘制的结果,如图:

图形上下文(Graphics Context)

一个Graphics Context表示一个绘制目标。它包含完成绘制任务所需的一些绘制参数和设备相关信息。Graphics Context定义了基本的绘制属性,如颜色、裁减区域、线条宽度和字体信息、混合模式等。

Quartz 2D 坐标系

Quartz 2D中默认的坐标系统为:左下角为坐标系统原点(0,0),屏幕水平方向为x轴,沿着x轴从左到右为正方向;屏幕垂直方向为y轴,沿着y轴从下到上为正方向。在实际应用中,不同的Graphics Context可能使用了不同的坐标系统,它们在Quartz 2D默认的坐标系统基础之上做了调整,来适应不同的场景。

iOS中的视图绘制

在iOS应用程序中,如果需要在屏幕上进行视图绘制,需要创建一个UIView对象,UIView是定义为在屏幕上的一块矩形区域,用于管理这块区域所呈现的内容,而在这个矩形区域内,我们可以通过重写drawRect:方法来自定义一些需要显示的内容。drawRect:方法在视图显示在屏幕上及它的内容需要更新时被系统自动调用,我们手动调用是无效的,系统提供了两个方法让我们进行间接调用drawRect:来达到重绘视图的目的:

// 方法一:重新绘制这个view
- (void)setNeedsDisplay;
// 方法二:重新绘制view的某个区域
- (void)setNeedsDisplayInRect:(CGRect)rect;

获取Graphics Context

在调用自定义的drawRect:后,视图对象自动配置绘图环境以便代码能立即执行绘图操作。作为配置的一部分,视图对象将为当前的绘图环境创建一个Graphics Context。我们可以在drawRect:中使用代码获取这个context:

CGContextRef context = UIGraphicsGetCurrentContext();

这里需要注意的是,这个context的坐标系是默认原点位于左上角,y轴正方向为向下。这是因为UIKit使用的默认的坐标系统与Quartz 2D默认的坐标系统不同,在UIKit中,默认原点位于左上角,y轴正方向为向下。所以UIView通过修改Quartz的Graphics Context的CTM(ps:一种仿射矩阵,通过平移(translation)、旋转(rotation)、缩放(scale)操作可以将点从一个坐标空间映射到另外一个坐标空间)以使其与UIKit的坐标系匹配。

创建与绘制路径(Path)

路径我们可以理解为我们手中的画笔绘制出来的一个或者多个形状(子路径),每一个形状可以是线、圆、矩形、星形等简单的形状,也可以是一些更复杂的自定义形状。如下图显示了一些路径。左上角的直线可以是虚线;直线也可以是实线。上边中间的路径是由多条曲线组成的开放路径;右上角的同心圆填充了颜色,但没有描边;左下角的加利福尼亚州是闭合路径,由许多曲线和直线构成,且对路径进行填充和描边。


  • 创建路径:
    路径的创建和绘制是两个独立的工作,我们可以手动创建一个路径,也可以使用一些便利的函数帮我们隐式的创建一些路径,我们获取到的Quartz Context其实默认会在内部创建一个path用来保存绘图信息。我们以构建一条直线线段路径为例:
    - (void)drawRect:(CGRect)rect {
    
    // 1.获取Quartz Context
    CGContextRef context = UIGraphicsGetCurrentContext();
    // 2.开始构建路径,设置一个起点的坐标,绘制系统会追踪至该点进行绘制
    // 参数一:关联的绘制目标
    // 参数二:起点的x坐标
    // 参数三:起点的y坐标
    CGContextMoveToPoint(context, 100, 100);
    // 3.将当前点与一个点(这里我们举例为(200,100))之间构建一条直线
    // 参数一:关联的绘制目标
    // 参数二:该端点的x坐标
    // 参数三:该端点的y坐标
    CGContextAddLineToPoint(context, 200, 100);
    
    }

让我们共同分析一下上面的代码:
第一步,我们在drawRect:方法中获取Quartz Context;
第二步,我们准备开始构建路径,在构建路径时我们使用了CGContextMoveToPoint()函数来确定绘制的一个起始点,context会将该点信息存储至默认生成的path中;
第三步,我们使用了CGContextAddLineToPoint()函数将两点之间建立直线关系,同样context会将该点信息也存储至默认生成的path中,注意此时,我们只是构建了一个直线路径,并没有绘制,所以运行后不会有效果。
但是上述代码的可读性并不友好,而且在绘制路径后,系统将清空Quartz Context,我们可能想保留路径,特别是在绘制一些比较复杂场景时,我们需要反复使用,所以我们在实际开发中,通常会手动创建一个path对象,而且这个path对象通常是可变的,使用的函数为CGPathCreateMutable(),数据类型为:CGMutablePathRef,然后我们可以向该对象添加直线、弧、曲线和矩形等。Quartz提供了一个类似于操作图形上下文的CGPath的函数集合。这些路径函数直接操作CGPath对象,而不是Quartz Context。这些函数包括:

CGPathCreateMutable,取代CGContextBeginPath
CGPathMoveToPoint,取代CGContextMoveToPoint
CGPathAddLineToPoint,取代CGContexAddLineToPoint
CGPathAddCurveToPoint,取代CGContexAddCurveToPoint
CGPathAddEllipseInRect,取代CGContexAddEllipseInRect
CGPathAddArc,取代CGContexAddArc
CGPathAddRect,取代CGContexAddRect
CGPathCloseSubpath,取代CGContexClosePath

如果想要添加一个路径或者多个路径到Quartz Context,可以调用CGContextAddPath。路径将保留在Quartz Context中,直到Quartz绘制它。

  • 绘制路径:
    构建路径后我们可以给它描边(Stroke)或者填充(Fill)。
    描边:绘制路径的边框;
    填充:填充是绘制路径包含的区域。
    Quartz提供了关于描边和填充的函数,我们可以设置描边线的属性,如宽度、颜色等,也可以设置填充的颜色以及填充的方式。

基本图形绘制

  • 线段绘制:
    一条最基本的直线线段需要两个端点,一个起始点一个结束点,下面的代码案例绘制了一条P1(100, 100) 至P2(200, 100)之间的直线线段;
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGMutablePathRef linePath = CGPathCreateMutable();
    // 设置一个起点的坐标,绘制系统会追踪至该点进行绘制
    CGPathMoveToPoint(linePath, NULL, 100, 100);
    // 将当前点与一个点(这里我们举例为(200,100))之间构建一条直线
    CGPathAddLineToPoint(linePath, NULL, 200, 100);
    CGContextAddPath(context, linePath);
    // 设置描边的颜色(两种方式)
    // 方法一:
    // CGContextSetRGBStrokeColor(context, 211.f / 255.f, 106.f / 255.f, 119.f / 255.f, 1.0);
    // 方法二:
    [[UIColor redColor] setStroke];
    // 设置描边的宽度
    CGContextSetLineWidth(context, 10);
    // 绘制路径-仅描边
    CGContextStrokePath(context);
    // 注意内存管理
    CGPathRelease(linePath);
  • 多条连续线段绘制:
    如果绘制多个且连续的线段,只需多次调用CGPathAddLineToPoint()函数连接更多的点即可,当然也有更便利的方式提供给我们:
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGMutablePathRef linesPath = CGPathCreateMutable();
    CGPoint pointArray[] = {CGPointMake(100, 100), CGPointMake(200, 100), CGPointMake(200, 200), CGPointMake(100, 200)};
    // 通过数组绘制一条多端点的线
    // 参数一:关联的绘制路径
    // 参数二:仿射变化
    // 参数三:数组
    // 参数四:数组元素个数
    CGPathAddLines(linesPath, NULL, pointArray, sizeof(pointArray) / sizeof(CGPoint));
    CGContextAddPath(context, linesPath);
    // 设置填充的颜色
    // 方法一:
    CGContextSetRGBFillColor(context, 211.f / 255.f, 106.f / 255.f, 119.f / 255.f, 1.0);
    // 方法二:
    [[UIColor redColor] setFill];
    // 闭合绘制路径,会从当前点绘制至起点
    CGContextClosePath(context);
    // 绘制路径-仅描边
    //    CGContextStrokePath(context);
    // 绘制路径-仅填充
    //    CGContextFillPath(context);
    // 绘制路径-既有描边也有填充
    CGContextDrawPath(context, kCGPathFillStroke);
    CGPathRelease(linesPath);
  • 弧线绘制:
    弧指的是圆弧段。我们指定一个圆心,半径和放射角(以弧度为单位)。放射角为2*PI时,创建的是一个圆。
    CGContextRef context = UIGraphicsGetCurrentContext();
    CGMutablePathRef arcPath = CGPathCreateMutable();
    // 绘制一条弧
    // 参数一:关联的绘制路径
    // 参数二:仿射变化
    // 参数三:原点的坐标x
    // 参数四:原点的坐标y
    // 参数五:半径
    // 参数六:起始弧度
    // 参数七:结束弧度
    // 参数八:绘制方向(0为顺时针,1为逆时针)
    CGPathAddArc(arcPath, NULL, 100, 100, 50, 0, 135 * (M_PI / 180), 1);
    CGContextAddPath(context, arcPath);
    CGContextStrokePath(context);
    CGPathRelease(arcPath);   
  • 贝赛尔(Bezier)曲线绘制:
    Bezier曲线是应用于二维图形的曲线。曲线由顶点和控制点组成,通过改变控制点坐标可以改变曲线的形状。
    一次Bezier曲线是由P0至P1的连续点,描述的一条线段:

二次Bezier曲线是 P0至P1 的连续点Q0和P1至P2 的连续点Q1 组成的线段上的连续点B(t),描述一条抛物线:

三次Bezier曲线:

绘制一条三次Bezier曲线:

CGContextRef context = UIGraphicsGetCurrentContext();
CGMutablePathRef curvePath = CGPathCreateMutable();
CGPathMoveToPoint(curvePath, NULL, 20, 100);
// 绘制贝赛尔曲线
// 参数一:关联的绘制路径
// 参数二:仿射变化
// 参数三:控制点1的x坐标
// 参数四:控制点1的y坐标
// 参数五:控制点2的x坐标
// 参数六:控制点2的y坐标
// 参数七:终点的x坐标
// 参数八:终点的y坐标
CGPathAddCurveToPoint(curvePath, NULL, 50, 50, 80, 100, 130, 100);
CGContextAddPath(context, curvePath);
CGContextStrokePath(context);
CGPathRelease(curvePath);   
  • 矩形绘制:
    我们可以调用CGPathAddRect或者CGContextAddRect来添加一个矩形到当前路径中,并提供一个CGRect结构体(包含矩形的原点及大小)作为参数。
    添加到路径的矩形开始于一个move-to-point操作,结束于一个close-subpath操作,所有的移动方向都是顺时针。
    我们也可能调用CGPathAddRects或者CGContextAddRects函数来添加一系列的矩形到当前路径,并传递一个CGRect结构体的数组。
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    CGMutablePathRef rectPath = CGPathCreateMutable();
    // 绘制矩形
    // 参数一:关联的绘制路径
    // 参数二:仿射变化
    // 参数二:绘制位置及大小
    CGPathAddRect(rectPath, NULL, CGRectMake(100, 100, 100, 100));
    
    CGContextAddPath(context, rectPath);
    CGContextStrokePath(context);
    CGPathRelease(rectPath);
  • 椭圆绘制:
    椭圆是一种特殊的圆。椭圆是通过定义两个焦点,在平面内所有与这两个焦点的距离之和相等的点所构成的图形。
    CGContextRef context = UIGraphicsGetCurrentContext();
    
    CGMutablePathRef ellipsePath = CGPathCreateMutable();
    // 绘制椭圆
    // 参数一:关联的绘制路径
    // 参数二:仿射变化
    // 参数三:绘制的位置及大小
    CGPathAddEllipseInRect(ellipsePath, NULL, CGRectMake(100, 100, 200, 200));
    
    CGContextAddPath(context, ellipsePath);
    [[UIColor redColor] setFill];
    CGContextDrawPath(context, kCGPathFillStroke);
    CGPathRelease(ellipsePath);

参考:《Quartz 2D Programming Guide》

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,924评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,902评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,716评论 0 239
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,783评论 0 203
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,166评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,510评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,784评论 2 311
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,476评论 0 196
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,196评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,459评论 2 243
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,978评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,321评论 2 252
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,964评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,046评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,803评论 0 193
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,530评论 2 271
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,420评论 2 265

推荐阅读更多精彩内容