很多童鞋知道CoreGraphics,但不见得听过Quartz 2D,二者关系见上一篇(等等,其实就是土豆和马铃薯的区别啦)在“度娘”中搜索CoreGraphics多数文章都是CGPathRef的操作,没有关于API详细的说明和解释,对于一些不太容易理解的地方自然也没有解释。
本篇立足于绘制 Path 基本概念和API,并结合实战完成图形绘制。
此文是对于官文的总结和实战。
Paths(路径)原理
路径定义了一个或多个形状或是子路径。一个子路径可由直线,曲线,或者同时由两者构成。它既可以是开放的,也可以是闭合的。
创建路径和绘制路径是分开的任务,1.首先创建一个路径,2.当我们需要渲染一个路径时,我们使用Quartz来绘制它。
创建、绘制路径
子路径是由点、线、圆弧和曲线构成的,Quartz同时也提供了函数用于绘制矩形和椭圆形,其中点是定义了绘制的起点和终点,当然有些图片的绘制仅仅选定了原点即可绘制,比如说圆弧、椭圆形等。
注:复杂图形多是由n个子路径构成。
路径的绘制,Quartz提供了“两”组API(允许我这么理解),分别是是CGPath 和 CGContext。
CGContext
是直接操作上下文,在上下文中绘制子路径,由CGContextClosePath(context)
关闭当前的子路径,然后继续绘制下一个路径,那么此时如何需要再继续修改或者绘制上一个路径便不太可能实现了。于是。。CGPath
出场了,此时操作的是CGPathRef
对象,每此绘制完子路径使用CGContextAddPath(context, curvePath)
添加到当前上下文中,然后绘制下一个子路径,此时所有的子路径都可以保存起来,以被再使用。
所以笔者建议,用CGPath绘制复杂的组合图形,CGContext绘制简单图形。
0. 上下文
//获取上下文,UIKit坐标系。
CGContextRef context = UIGraphicsGetCurrentContext();
..
/* Begin a new path. The old path is discarded. */
/* 所以一定要注意,这里创建的不是子路径,如果采用CGContext绘制路径时,
* 开始绘制下一个子路径时再次调用CGContextBeginPath(context)会覆盖
* 上一个路径,所以这个函数只用调用一次即可*/
CGContextBeginPath(context);
1. 点 Point
CGContextMoveToPoint(context, p1.x, p1.y);
//或者
CGPathMoveToPoint(path, nil, fromPoint.x, fromPoint.y);
2. 线 Lines
CGContextMoveToPoint(context, p1.x, p1.y);
CGContextAddLineToPoint(context, x, y);
CGContextClosePath(context);
//或者
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, fromPoint.x, fromPoint.y);
CGPathAddLineToPoint(path, nil, toPoint.x, toPoint.y);
CGPathCloseSubpath(path);
CGContextAddPath(context, path);
CGPathRelease(path);
3. 圆弧 Arcs
CGContextAddArc(context, center.x, center.y, radius, radians(0.0f), radians(360.0f), 0);
CGContextClosePath(context);
//或者
CGMutablePathRef path = CGPathCreateMutable();
CGPathAddArc(path, nil, center.x, center.y, 50, radians(-90.0f), radians((- 180.0f)), YES);
CGPathCloseSubpath(path);
CGContextAddPath(context, path);
CGPathRelease(path);
//其他画法
CGContextAddArcToPoint;
CGContextAddQuadCurveToPoint;
4. 曲线 Curves
CGContextAddCurveToPoint(context, refPoint1.x, refPoint1.y, refPoint2.x, refPoint2.y, toPoint.x, toPoint.y);
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y);
CGContextAddQuadCurveToPoint(context, refPoint1.x, refPoint1.y, toPoint.x, toPoint.y);
CGContextClosePath(context);
//或者
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, fromPoint.x, fromPoint.y);
CGPathAddCurveToPoint(path, nil,refPoint1.x, refPoint1.y, refPoint2.x, refPoint2.y, toPoint.x, toPoint.y);
CGPathCloseSubpath(path);
CGContextAddPath(context, path);
CGPathRelease(path);
5. 矩形 Rectangle
CGContextAddRect(context, rect);
CGContextClosePath(context);
//或者
CGPathAddRect(path, nil, rect);
CGPathCloseSubpath(path);
CGContextAddPath(context, path);
CGPathRelease(path);
//一起添加多个矩形
CGContextAddRects(context, rects, 3);
6. 椭圆形 Ellipses
CGContextAddEllipseInRect(context, rect);
CGContextClosePath(context);
//或者
CGPathAddEllipseInRect(path, nil, rect);
CGPathCloseSubpath(path);
CGContextAddPath(context, path);
CGPathRelease(path);
绘制路径规则总结
- 在开始绘制路径前,调用函数CGContextBeginPath;
- 直线、弧、曲线开始于当前点。空路径没有当前点;我们必须用CGContext- MoveToPoint来设置第一个子路径的起始点,或者调用一个便利函数来隐式地完成该任务。
- 如果要闭合当前子路径,调用函数CGContextClosePath。随后路径将开始一个新的子路径,即使我们不显示设置一个新的起始点。
- 当绘制弧时,Quartz将在当前点与弧的起始点间绘制一条直线。
- 添加椭圆和矩形的Quartz程序将在路径中添加新的闭合子路径。
- 我们必须调用绘制函数来填充或者描边一条路径,因为创建路径时并不会绘制路径。
深度刻画(描边、填充)
影响描边的属性
下表中展示了上下文的一些属性,修改这些的属性会影响后续的进行描边操作。
Parameter | Function to set parameter value |
---|---|
Line width | CGContextSetLineWidth |
Line join | CGContextSetLineJoin |
Line cap | CGContextSetLineCap |
Miter limit | CGContextSetMiterLimit |
Line dash pattern | CGContextSetLineDash |
Stroke color space | CGContextSetStrokeColorSpace |
Stroke color | CGContextSetStrokeColorCG... |
Stroke pattern | CGContextSetStrokePattern |
描边函数
Quartz提供了下表的函数来描边当前路径。
Function | Description |
---|---|
CGContextStrokePath | 给当前的path描边 |
CGContextStrokeRect | 注:创建一个矩形子路径,并对其进行描边 |
CGContextStrokeRectWithWidth | 注:创建一个矩形子路径,设置描边宽度,并对其进行描边 |
CGContextStrokeEllipseInRect | 注:创建一个椭圆子路径,并对其进行描边 |
CGContextStrokeLineSegments | 给一组直线进行描边 |
CGContextDrawPath | 注:在上下文中绘制当前的子路径,需指定CGPathDrawingMode(见下表) |
CGPathDrawingMode有以下几种:
- kCGPathFill, 仅填充
- kCGPathEOFill, 奇偶填充
- kCGPathStroke, 仅描边
- kCGPathFillStroke, 既填充又描边
- kCGPathEOFillStroke, 奇偶描边
填充路径
Quartz将路径包含的每个子路径都看作是闭合的。然后,使用这些闭合路径并计算填充的像素。 椭圆和矩形这样的路径其区域都很明显。但是如果路径是由几个重叠的部分组成或者路径包含多个子路径,我们则有两种规则来定义填充区域。
非零缠绕数规则(nonzero windingnumber rule):
为了确定一个点是否需要绘制,我们从该点开始绘制一条直线穿过绘图的边界。从0开始计数,每次路径片断从左到右穿过直线是,计数加1;而从右到左穿过直线时,计数减1。如果结果为0,则不绘制该点,否则绘制。路径片断绘制的方向会影响到结果。偶数-奇数规则: 为了确定一个点是否被绘制,我们从该点开始绘制一条直线穿过绘图的边界。计算穿过该直线的路径片断的数目。如果是奇数,则绘制该点,如果是偶数,则不绘制该点。路径片断绘制的方向不影响结果。
综上:默认 采用的是非零缠绕数规则。非零缠绕数的填充规则与绘制的方向有关、偶数-奇数规则则与方向无关。
详见:官方文档。
Quartz提供了下表的函数来填充当前路径。
Function | Description |
---|---|
CGContextEOFillPath | 采用奇偶规则给当前的path填充 |
CGContextFillPath | 采用非零缠绕数规则给当前的path填充 |
CGContextFillRect | 注:创建一个矩形子路径,并对其进行填充。 |
CGContextFillRects | 注:创建一组矩形路径,并对其进行填充 |
CGContextFillEllipseInRect | 注:创建一个椭圆形路径,并对其进行填充 |
CGContextDrawPath | 注:在上下文中绘制当前的子路径,需指定CGPathDrawingMode |
设置混合模式
混合模式指定了Quartz如何将绘图绘制到背景上。Quartz默认使用普通混合模式(normal blend mode),该模式使用如下公式来计算前景绘图与背景绘图如何混合:
result = (alpha * foreground) + (1 - alpha) *background
调用 CGContextSetBlendMode(context, kCGBlendModeNormal)
设置回合模式。
你可以在《Quartz 2D Programming Guide》- Paths章节中查看混合模式的具体效果。
注意:CGContext保存和恢复函数
当构建复杂的路径,其由n个子路径构成,每个子路径样式不一致,分别需要对于当前Context上下文属性做了各种不同的修改,为了保证下一个子路径的context参数值保持原有值(即不继承上一个子路径的参数值),我们需要在每绘制一个子路径之前调用CGContextSaveGState(context)
保存当前的context状态,并在绘制完当前子路径后恢复设置,即调用CGContextRestoreGState(context)
。
Paths 实战,绘制图形。
实战中我们来绘制一图形,它是由几个形状各异的子路径构成。
实现 drawRect
为了使用Core Graphics来绘图,最简单的方法就是自定义一个类继承自UIView,并重写子类的drawRect方法。在这个方法中绘制图形。
Core Graphics必须一个画布,才能把东西画在这个画布上。在drawRect方法方法中,我们可以直接获取当前栈顶的上下文(Context)。
- (void)drawRect:(CGRect)rect {
CGContextRef context = UIGraphicsGetCurrentContext();
CGContextBeginPath(context);
//1.直线绘图
[self beginLinesDemo:context];
//2.圆弧绘图
[self beginArcsDemo:context];
//3.曲线绘图
[self beginCurvesDemo:context];
//4.椭圆绘图
[self beginEllipsesDemo:context];
//5.矩形绘图
[self beginRectangleDemo:context];
//6.影响描边的属性
// CGContextSet...
//7.路径描边的函数
// CGContextStroke...
//8.填充路径
// CGContextFill...
}
直线绘制五角星
本文重点不是如何计算五角星,而是学会使用CG,所以如果你对计算有兴趣的话,我想度娘会给你答案的,继续。
- (void)beginLinesDemo:(CGContextRef)context
{
//1.Line线
CGContextSaveGState(context);
cg_drawPentagramByLine(context, CGPointMake(150, 150), 100);
//设置描边属性
CGContextSetFillColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetStrokeColorWithColor(context, [UIColor redColor].CGColor);
CGContextSetLineWidth(context, 2);
CGContextDrawPath(context, kCGPathFillStroke);
CGContextRestoreGState(context);
}
void cg_drawPentagramByLine(CGContextRef context, CGPoint center,CGFloat radius)
{
CGPoint p1 = CGPointMake(center.x, center.y - radius);
CGContextMoveToPoint(context, p1.x, p1.y);
CGFloat angle=4 * M_PI / 5.0;
for (int i=1; i<=5; i++)
{
CGFloat x = center.x -sinf(i*angle)*radius;
CGFloat y = center.y -cosf(i*angle)*radius;
CGContextAddLineToPoint(context, x, y);
}
CGContextClosePath(context);
}
绘制图如下:
圆弧链接五角星顶点
- (void)beginArcsDemo:(CGContextRef)context
{
//2.Arcs弧线
CGContextSaveGState(context);
cd_drawCircleByArc(context, CGPointMake(150, 150), 102);
//设置描边属性
CGContextSetStrokeColorWithColor(context, [UIColor whiteColor].CGColor);
CGContextSetLineWidth(context, 2);
CGContextDrawPath(context, kCGPathStroke);
CGContextRestoreGState(context);
}
void cd_drawCircleByArc(CGContextRef context, CGPoint center, CGFloat radius)
{
CGContextAddArc(context, center.x, center.y, radius, radians(0.0f), radians(360.0f), 0);
//下面两种用法可以自行研究了
// CGContextAddArcToPoint;
// CGContextAddQuadCurveToPoint;
}
绘制图如下:
曲线绘制大风车
- (void)beginCurvesDemo:(CGContextRef)context
{
//3.Curve曲线
CGContextSaveGState(context);
cd_drawPinwheelByCurve(context, CGPointMake(300, 100), CGPointMake(0, 200), CGPointMake(0, 0), CGPointMake(300, 300));
CGContextSetRGBFillColor(context, 246 / 255.0f, 122 / 255.0f , 180 / 255.0f, 0.6);
CGContextSetStrokeColorWithColor(context, [UIColor orangeColor].CGColor);
CGContextDrawPath(context, kCGPathFillStroke);
CGMutablePathRef curvePath = cd_drawPinwheelCurveByPath(CGPointMake(300, 200), CGPointMake(0, 100), CGPointMake(300, 0), CGPointMake(0, 300));
CGContextAddPath(context, curvePath);
CGPathRelease(curvePath);
CGContextDrawPath(context, kCGPathFillStroke);
CGContextRestoreGState(context);
}
void cd_drawPinwheelByCurve(CGContextRef context,CGPoint refPoint1, CGPoint refPoint2, CGPoint fromPoint, CGPoint toPoint)
{
CGContextMoveToPoint(context, fromPoint.x, fromPoint.y);
CGContextAddCurveToPoint(context, refPoint1.x, refPoint1.y, refPoint2.x, refPoint2.y, toPoint.x, toPoint.y);
CGContextAddQuadCurveToPoint(context, refPoint1.x, refPoint1.y, toPoint.x, toPoint.y);
}
CGMutablePathRef cd_drawPinwheelCurveByPath(CGPoint refPoint1, CGPoint refPoint2, CGPoint fromPoint, CGPoint toPoint)
{
CGMutablePathRef path = CGPathCreateMutable();
CGPathMoveToPoint(path, nil, fromPoint.x, fromPoint.y);
CGPathAddCurveToPoint(path, nil,refPoint1.x, refPoint1.y, refPoint2.x, refPoint2.y, toPoint.x, toPoint.y);
CGPathCloseSubpath(path);
//其他addCurve同context创建curve方法。
return path;
}
绘制图如下:
至此,关于Quartz中Paths主干相关的东西就写这么多了,还有边角知识点和API这里就不做详细介绍了。
详细查看官文文档 。
这是CoreGraphic第二篇,实战详细篇,接下来会进入CG美化篇,如果你感兴趣,来个赞和关注。我们下期见。