前言:
最近的迭代需求需要实现个仪表盘,正好时间比较多,索性就从头看一遍Quartz 2D,就有了今天这篇笔记,闲话不多说,直接开始,由于内容过多,此篇主要对Quatrz 2D做个大致的介绍和怎么用Quartz 2D绘制简单图形、曲线、文字和图片,后续大概会有3到5篇对Core Graphics更详细的介绍的文章。
一、概览
Quartz 2D是一个先进的二维图像绘制引擎,可用于iOS、tvOS和macOS应用程序开发,Quartz 2D也是Core Graphics框架的一部分,Quartz 2D在UIKit中也有很好的封装和集成,我们日常开发时所用到的UIKit中的组件都是由Core Graphics进行绘制的。
二、Quartz 2D概览
Quartz 2D使用画家模型进行成像,在画家的模型中,每个连续的绘图操作都将‘绘图层’输出到‘画布’上,已经绘制到‘画布’上的图不能被修改,除非覆盖更多的图。
图形上下文-绘画的纸(CGContextRef):
图形上下文就是上文中我们说的‘画布’,也就是画的载体,只有获取到这个画布才可以开始绘图。
画布中包含着所有设备特定的特征,换句话说你可以简单的通过为相同的代码提供不同的图形上下文来将相同的图像绘制到不同设备上,除此之外,你不需要执行任何设备特定的计算。
另外,在iOS开发中,继承自UIView的类会在你调用drawRect:函数时自动生成一个默认的图形上下文,通过UIGraphicsGetCurrentContext函数获取,和UIKit自动生成的图形上下文不同,位图图形上下文和PDF图形上下文需要我们手动去创建,而且可以在任何方法中调用,不局限于drawRect函数。
在Quartz 2D中共有5种图形上下文,Mac OS支持全部的5种图形上下文,而iOS支持前三种。
- 位图图形上下文(Bitmap):允许你把绘制到此上下文的内容转换成位图。
- PDF图形上下文:允许你把绘制到此上下文的内容转换成一个PDF文件,并对PDF做一些操作。
- 层图形上下文( Layer):允许你在Layer层上绘图。一般而言,对于离屏渲染而言,它比位图图形上下文更优秀。
- 窗口图形上下文 (Window):只能Mac使用,不做了解。
- 打印图形上下文:只能Mac使用,不做了解。
Quartz相关的数据类型:
Data Types | Summary |
---|---|
CGPathRef | 构成矢量图形的路径 |
CGImageRef | 位图图像和位图图像蒙板 |
CGLayerRef | 可用于重复绘图(例如用于背景或图案)和用于屏外绘图的绘图层 |
CGPatternRef | 用于重复绘制 |
CGShadingRef、CGGradientRef | 用于绘制渐变效果 |
CGFunctionRef | 采用任意数量的浮点参数的回调函数,在为阴影创建渐变时使用此数据类型 |
CGColorRef | Quartz 2D的颜色 |
CGImageSourceRef | 用于导入或导出图像到Quartz中 |
CGFontRef | 与绘制文字相关 |
CGPDFDictionaryRef、CGPDFObjectRef、CGPDFPageRef、CGPDFStream、CGPDFStringRef、CGPDFArrayRef、CGPDFScannerRef、CGPDFContentStreamRef | 用于PDF解析 |
图形状态-画笔与颜料(抽象存在,没有具体的数据类型):
Quartz是根据上下文的图形状态来确定如何绘制图形的。例如,当你调用CGContextSetStrokeColorWithColor
函数来设置填充颜色时,当前图形状态的值就被你修改了。
一个图形上下文会包含一个或多个图形状态,当你或Quartz创建一个上下文时,图形状态的堆栈是空的,你可以通过CGContextSaveGState
将当前的图形状态压栈,通过CGContextRestoreGState
将当前图形状态替换为栈顶的图形状态。
图形状态包括以下参数:
Object | Handle |
---|---|
Current transformation matrix (CTM) | 变换矩阵 |
Clipping area | 裁剪区域 |
Line: width, join, cap, dash, miter limit | 线条的宽度、连接与顶点样式、短划线 |
Accuracy of curve estimation (flatness) | 曲线估算精度 |
Anti-aliasing setting | 抗锯齿设置 |
Color: fill and stroke settings | 填充与笔触的颜色设置 |
Alpha value (transparency) | 透明度 |
Rendering intent | 渲染意图 |
Color space: fill and stroke settings | 色彩空间中填充与笔触设置 |
Text: font, font size, character spacing, text drawing mode | 字体、字号、字间隙、模式 |
Blend mode | 混合模式 |
二维坐标系统:
在iOS中,主要有两种坐标系统:
- 左上角坐标系(ULO), 绘图操作的起点位于绘图区域的左上角,其正值向下和向右延伸。UIKit和Core Animation框架使用的默认坐标系统是基于ULO的。
-
左下原点坐标系(LLO),绘图操作的原点位于绘图区域的左下角,其正值向上和向右延伸。Core Graphics框架使用的默认坐标系是基于LLO的。
你可以通过CGContextScaleCTM
和CGContextTranslateCTM
实现从LLO到ULO坐标的转换。
在drawRect
函数中默认生成的上下文是ULO坐标系,是已经经过转换的坐标系。
内存管理:
虽然Quatrz使用了Foundation的内存管理模型,但是由于Core Graphics的下一层都是由C语言实现的,所以其并不支持ARC,所以还是需要你手动去管理内存,记住谁创建谁释放的原则。
三、图形绘制
再说如何绘制基本图形之前先来了解一下路径和视图绘制周期,下面所绘制的图形都是在填充与描边路径,而视图绘制周期告诉你iOS在什么时候会调用重绘函数drawRect
。
路径(Path):
路径由一个或多个图形和子路径构成,就像绘画一样,你在纸上的每落下一笔都可以称为一个子路径。但和绘画不同,在Quratz中,如果不使用CGPathMoveToPoint
移动画笔,每一个子路径都会默认以上一个子路径的终点作为起点,就好像画笔落下去后就不能随便再离开画布,除非这个路径被关闭。
当你用含有FillPath
、StrokePath
、DrawPath
、ClosePath
等的函数进行最后的绘制时,代表一个路径被关闭,接下来的绘制就属于一个新的路径。
视图绘制周期:
当View第一次显示或需要重绘View的一部分时,iOS会要求View调用其drawRect:
方法更新视图,有几个动作可以触发视图更新:
- 移动或移除部分遮挡View的其他View
- 设置之前隐藏的View中的可见属性
hidden
为NO重新显示出View- 屏幕外的View滚动到屏幕范围内
- 显式调用
setNeedsDisplay
或setNeedsDisplayInRect:
方法
绘制步骤:
1.获取绘图上下文
2.设置图形状态
3.创建并设置路径
4.将路径添加到上下文(根据调用函数不同可省略)
5.绘制与释放路径
1、绘制基本的线
#pragma mark - 绘制基本的线
- (void) drawLine{
//1.获取自动生成的上下文对象
CGContextRef context = UIGraphicsGetCurrentContext();
//2.设置图形状态
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);//笔触的颜色
CGContextSetFillColorWithColor(context, UIColorHex(0x43454E).CGColor);//填充颜色
CGContextSetLineWidth(context, 5.0);//设置线条宽度
CGContextSetLineCap(context, kCGLineCapRound);//设置顶点样式
CGContextSetLineJoin(context, kCGLineJoinBevel);//设置连接点样式
//保存图形状态
CGContextSaveGState(context);
//3.创建并设置路径
CGMutablePathRef path = CGPathCreateMutable();
/*
path:路径对象
m:变化矩阵(在后面模块会讲到)
x,y:坐标点
*/
CGPathMoveToPoint(path, nil, 100, 50);//移动画笔到某个点
CGPathAddLineToPoint(path, nil, 50, 100);//以上个点为起点,目标点为终点画直线,并默认把此终点作为下一个路径的起点
CGPathAddLineToPoint(path, nil, 150, 100);
CGPathAddLineToPoint(path, nil, 100, 50);
//4.添加路径到上下文
CGContextAddPath(context, path);
//5.绘制路径 在绘制的时候会自动关闭当前路径
/*
CGContextRef:上下文对象
CGPathDrawingMode:绘制模式
*/
CGContextDrawPath(context, kCGPathFillStroke);
//5.释放路径
CGPathRelease(path);
}
2、绘制虚线
#pragma mark - 绘制虚线
- (void) drawLashLine{
//1.获取自动生成的上下文对象
CGContextRef context = UIGraphicsGetCurrentContext();
/* 2.设置线段样式
phase:虚线开始的位置,和下面数组一起使用,比如设置4,从第三段开始绘制,以下面的数组为例,会先绘制一段10长度的空白
lengths:虚线长度间隔(比如下面的数组会先绘制20长度的实线,在绘制10长度空白)
count:虚线数组元素个数
*/
CGFloat lengths[2] = {20, 10};
CGContextSetLineDash(context, 3, lengths, 2);
[[UIColor blueColor] setFill];//在文中说过UIKit也做了封装,这里就是颜色的封装
[[UIColor yellowColor] setStroke];
/*设置阴影
context:图形上下文
offset:偏移量
blur:模糊度
color:阴影颜色
*/
CGColorRef color = [UIColor grayColor].CGColor;//颜色转化,Quartz 2d必须使用转换后的颜色
CGContextSetShadowWithColor(context, CGSizeMake(2, 2), 0.8, color);
//3.创建路径(以下代码会自动创建)
CGContextBeginPath(context);
CGContextMoveToPoint(context, 250, 50);
CGContextAddLineToPoint(context, 200, 100);
CGContextAddLineToPoint(context, 300, 100);
CGContextClosePath(context);//关闭路径,会自动连接终点和最开始的起点
//5.绘制路径 自动创建的路径会自动释放
CGContextStrokePath(context);
}
3、绘制矩形
#pragma mark - 绘制矩形
- (void) drawRectangle{
//1.获取自动生成的上下文对象,你也可以传参用
CGContextRef context = UIGraphicsGetCurrentContext();
//2.设置图形状态,还记得我们在drawLine里调用CGContextSaveGState保存了图形状态吗,这里我们恢复我们保存的图形状态
CGContextRestoreGState(context);
//3.创建路径(自动添加到当前上下文了)
CGContextAddRect(context, CGRectMake(50, 150, 100, 50));
//5.绘制路径
CGContextDrawPath(context, kCGPathFillStroke);//填充并描边
}
效果图如下:
可以看到通过
CGContextRestoreGState
函数恢复的图形状态和画第一个三角形通过CGContextSaveGState
函数保存的图形状态是一致的。
4、绘制椭圆
#pragma mark - 绘制椭圆
- (void) drawEllipse{
//1.获取自动生成的上下文对象,你也可以传参用
CGContextRef context = UIGraphicsGetCurrentContext();
//3.创建路径(自动添加到当前上下文了)
CGContextAddEllipseInRect(context, CGRectMake(200, 150, 100, 50));
//5.绘制路径
CGContextDrawPath(context, kCGPathFillStroke);//填充并描边
}
效果图如下:
5、绘制圆形
再说绘制圆形之前我们先了解下两个规则,这两个规则对于我们绘制填充复杂图形很有帮助,在iOS中使用以下两个规则去填充或裁剪路径。
- 非零环绕数规则(Nonzero Winding Number Rule):从任意位置p作一条射线(只有一个端点的无线长的直线)。当从p点沿射线方向移动时,对每个方向穿过射线的边计数,每当多边形的边从右到左穿过射线时,环绕数(只是一个名词)加1,从左到右穿过射线时,环绕数减1。一直计数到没有边穿过射线为止,若环绕数为非0,则填充或裁剪p点,若环绕数为0,则不填充或裁剪p点,此规则同样可用作判断p点是否在不规则多边形内外。
- 奇-偶规则(Odd-even Rule):从任意位置p作一条射线,若与该射线相交的多边形边的数目为奇数,则填充或裁剪p点,则不填充或裁剪p点,此规则同样可用作判断p点是否在不规则多边形内外。
当然你同时只能使用一种规则,因为奇-偶规则和非零环绕数规则有时候是冲突的,况且Quartz 2D也不允许你这么做,下面通过代码解释一下:
#pragma mark - 绘制圆
- (void) drawCircle{
//1.获取自动生成的上下文对象,你也可以传参用
CGContextRef context = UIGraphicsGetCurrentContext();
//2.设置上下文环境
[[UIColor yellowColor] setFill];
[[UIColor blackColor] setStroke];
//3.创建路径
for (NSInteger i = 0; i < 2; i ++) {//绘制内外圆
CGContextAddArc(context, 100, 300, 50 - i * 20, 0, M_PI * 2, NO);
}
//5.绘制路径
CGContextDrawPath(context, kCGPathFillStroke);//填充并描边,默认使用的非零环绕数规则
//3.创建路径
for (NSInteger i = 0; i < 2; i ++) {//绘制内外圆
CGContextAddArc(context, 250, 300, 50 - i * 20, i ? M_PI * 2 : 0, i ? 0 : M_PI * 2, i ? YES : NO);
}
//5.绘制路径
CGContextDrawPath(context, kCGPathFillStroke);//填充并描边,默认使用的非零环绕数规则
}
效果图如下:
从代码中可以看出,左边的外圆和内圆都是逆时针方向的,如果从内圆内部发出任意一条射线,内圆和外圆穿过射线的方向始终是是一致的,环绕数最后不为0,根据规则,内圆内部的任意点都是必须填充的。
而右边的圆则不同,外圆是逆时针的,内圆则是顺时针的,内圆的边与外圆的边穿过从内圆发出的任意射线都是相反的,环绕数始终为0,根据规则,内圆内部的任意点都是不会被填充的。
而如果把CGContextDrawPath(context, kCGPathFillStroke)
换成CGContextDrawPath(context, kCGPathEOFillStroke)
,使用奇偶规则去填充的话,发现两者的内部圆都是不填充的,效果图如下:
这里顺便说一下关于路径的裁剪。
- 裁剪的作用就是当前的画布上的路径裁剪下来并抹清上面画的内容,所以裁剪最好在绘制前使用。
- 裁剪同样也适用奇偶规则与非零环绕数规则
下面我们用代码检测一下结论:
#pragma mark - 裁剪
- (void) tailor{
//1.获取自动生成的上下文对象,你也可以传参用
CGContextRef context = UIGraphicsGetCurrentContext();
//2.设置上下文环境
[[UIColor blueColor] setFill];
[[UIColor redColor] setStroke];
CGContextSetLineWidth(context, 20);
//3.创建路径
for (NSInteger i = 0; i < 2; i ++) {//绘制内外圆
CGContextAddArc(context, 100, 450, 50 - i * 20, 0, M_PI * 2, NO);
}
for (NSInteger i = 0; i < 2; i ++) {//绘制内外圆
CGContextAddArc(context, 250, 450, 50 - i * 20, i ? M_PI * 2 : 0, i ? 0 : M_PI * 2, i ? YES : NO);
}
//默认使用非零环绕裁剪
CGContextClip(context);
//获取裁剪出来的区域
CGRect rect = CGContextGetClipBoundingBox(context);
CGContextAddRect(context, rect);
CGContextDrawPath(context, kCGPathFillStroke);
}
我们创建的路径和上面两张图的路径是一样的,然后用非零环绕规则进行裁剪,最后在裁剪后的画布上画了一个矩形,效果图如下:
可以看到裁剪的规则和填充的规则是一致的。
如果在上文的代码最后加上这样一段代码块:
//重置裁剪区域
CGContextResetClip(context);
CGContextAddRect(context, rect);
CGContextDrawPath(context, kCGPathFillStroke);
效果图如下:通过两张图的比较我们可以直观的看出裁剪的功能。
PS:弧形与弧线绘制不在赘述,其实就是画圆的时候控制起始角度和填充与描边。
6、绘制贝赛尔曲线
在iOS中,Quartz 2D除了可以画直线和圆弧、椭圆、矩形等简单图形之外,还可以绘制一种比较复杂的曲线就是贝塞尔曲线,贝塞尔曲线在计算机图形中应用十分广泛,在iOS中也有很多大牛能做出优美的动画和图形,iOS支持二次和三次贝塞尔曲线,二次贝塞尔曲线和三次贝塞尔曲线的不同之处在于前者有一个控制点,后者有两个控制点。
关于贝塞尔曲线的原理请看这篇贝塞尔曲线扫盲。
二次贝塞尔曲线:
#pragma mark - 二次贝塞尔曲线
- (void) drawQuadCurve{
CGContextRef context = UIGraphicsGetCurrentContext();
[[UIColor redColor] setFill];
[[UIColor blueColor] setStroke];
CGContextSaveGState(context);
CGContextAddArc(context, 20, 200, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextAddArc(context, 100, 50, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextAddArc(context, self.width - 20, 300, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextMoveToPoint(context, 20, 200);
CGContextAddLineToPoint(context, 100, 50);
CGContextAddLineToPoint(context, self.width - 20, 300);
CGContextStrokePath(context);
[[UIColor blackColor] setStroke];
[[UIColor yellowColor] setFill];
CGContextSetLineWidth(context, 2);
//起始点
CGContextMoveToPoint(context, 20, 200);
/*
cpx, cpy:控制点
x, y:结束点
*/
CGContextAddQuadCurveToPoint(context, 100, 50, self.width - 20, 300);
CGContextDrawPath(context, kCGPathFillStroke);
}
三次贝塞尔曲线
#pragma mark - 三次贝塞尔曲线
- (void) drawBezierCurve{
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextRestoreGState(context);
CGPoint pointA = CGPointMake(20, 500);
CGPoint pointB = CGPointMake(self.width - 20, 400);
CGPoint controlA = CGPointMake(100, 350);
CGPoint controlB = CGPointMake(250, 600);
CGContextAddArc(context, pointA.x, pointA.y, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextAddArc(context, controlA.x, controlA.y, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextAddArc(context, controlB.x, controlB.y, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextAddArc(context, pointB.x, pointB.y, 5, 0, M_PI * 2, NO);
CGContextFillPath(context);
CGContextMoveToPoint(context, pointA.x, pointA.y);
CGContextAddLineToPoint(context, controlA.x, controlA.y);
CGContextAddLineToPoint(context, controlB.x, controlB.y);
CGContextAddLineToPoint(context, pointB.x, pointB.y);
CGContextStrokePath(context);
[[UIColor blackColor] setStroke];
[[UIColor yellowColor] setFill];
CGContextMoveToPoint(context, pointA.x, pointA.y);
CGContextAddCurveToPoint(context, controlA.x, controlA.y, controlB.x, controlB.y, pointB.x, pointB.y);
CGContextDrawPath(context, kCGPathFillStroke);
}
效果图如下:
关于贝塞尔曲线的运用会在4月份整理Core Graphics与Core Animation后会单独写一篇文章来详细了解一下。
Quartz除了可以绘制图形之外,还可以绘制图像与文本,下面我们简单了解一下图像与文本的绘制。
四、简单文本与图像绘制
- (void)drawRect:(CGRect)rect {
//绘制文字
CGContextRef context = UIGraphicsGetCurrentContext();
NSString * string = @"大概每个人都有一个Dream Lover梦中情人的时候,也有一个Dream game梦想中的游戏吧。这可能和个人的经历有关,虽然从小并没有接触过桌面rpg,但是对于rpg却一下子就沉迷其中。依靠一点点撇脚的英语,勇敢闯进像《龙腾世纪》《永恒之柱》《暴君》这样的游戏中,试着感受他们的魅力,故事确实动人,战斗也富策略。但是在今天,看到《神界:原罪2》这款游戏的时候,深切地感受到了42所说“之前夸的都白夸”了的感觉,因为《原罪2》方方面面都做的比前人好太多、深入太多,如果要打一个比方的话,《原罪2》在业界的地位很快就可以赶上几年前的《天际》和脍炙人口的《塞尔达:旷野之息》了。";
CGRect textRect = CGRectMake(10, 10, self.width - 20, 150);
[[UIColor blackColor] setFill];
CGContextAddRect(context, textRect);
CGContextFillPath(context);
UIFont * font = [UIFont systemFontOfSize:12];
UIColor * color = [UIColor greenColor];
NSMutableParagraphStyle * style = [[NSMutableParagraphStyle alloc]init];//段落样式
style.alignment = NSTextAlignmentLeft;//对齐方式
NSDictionary * paramDic = @{
NSFontAttributeName : font,
NSForegroundColorAttributeName : color,
NSParagraphStyleAttributeName : style
};
[string drawInRect:textRect withAttributes:paramDic];
//绘制图像
CGRect imageRect = CGRectMake(10, 170, kScreenWidth - 20, (kScreenWidth - 20) / 1080 * 1920);
UIImage * image = [UIImage imageNamed:@"Beauty2"];
//绘制到指定的矩形中,注意如果大小不合适会会进行拉伸
[image drawInRect:imageRect];
//从某一点开始绘制,注意会自动根据@2x @3x决定在一个长度上绘制几个像素点,也就说如果有个张宽为1080像素的图片,其后缀名没有加上@2x和@3x会绘制成占据1080个点宽度的图片。
// [image drawAtPoint:CGPointMake(10, 160)];
//平铺绘制
// [image drawAsPatternInRect:imageRect];
}
效果图如下:
哈哈,顺便安利个游戏:神界:原罪2
总结:
如果看到这里,相信大家对Core Graphics已经有了简单的认识,下一篇会对Core Graphics的变换矩阵、绘制模式、阴影和渐变等做详细的介绍,当然要等到周末喽。
附录-参考文献:
Drawing and Printing Guide for iOS
Quartz 2D Programming Guide
CGContextDef