iOS 3.2后, 你就可以使用UIBezierPath
类创建向量路径. 类UIBezierPath
是使用OC对Core Graphic中的绘图特性的一个封装. 你可以使用这个类去定义简单的形状, 比如想椭圆和矩形, 和一些比较包含直线和曲线的复杂形状. 然后你可以使用这些路径对象在界面上绘制图形. 你可以对路径描边, 填充颜色, 或者两者都有. 你也可以使用路径来裁剪绘画上下文中的区域, 裁剪的区域之后用来修改接下来的绘图操作.
Bezier Path基础
UIBezierPath
对象是对CGPathRef
类型数据的包装. 路径(path)是使用线段和曲线构成的基于向量的形状. 你可以使用线段构建矩形, 多边形, 使用曲线构建圆弧, 圆, 和其他复杂的曲线图形. 而线是由坐标系中的点构成, 而这些点是由绘画命令来控制的.
路径中的部分线段和曲线构成了子路径. 子路径中的结束点是下个子路径的起始点. 单个UIBezierPath
对象可能包含多个子路径, 这些子路径通过命令moveToPoint:
来区分. 该命令可以是画笔移动到一个新位置.
路径的创建和使用是分开的, 构建路径的是绘图过程中的第一部分, 下面是构建路径步骤:
- 创建路径对象
- 设置路径对象(UIBezierPath)一些绘图属性, 比如
lineWidth
(线宽),lineJoinStyle
(连接风格)等路径绘制的设置, 或者使用属性usesEvenOddFillRule
来填充路径. 这些属性设置会应用到整个路径. - 使用
moveToPoint:
命令来开始一个子路径 - 通过直线和曲线来构建一个子路径
- 调用方法
closePath
来关闭路径, 会将路径中最后一部分的结束点和第一部分的起始点连接起来, 该过程是可选的. - 重复步骤3,4,5来添加更多子路径, 该过程是可选的.
当你构建路径时, 要相对于原点(0,0)合理安排路径中的点, 这样在后续移动路径的时候比较方便. 在绘制路径时, 点的位置和当前坐标系中的一样. 如果你的路径时相对于原点进行定向的, 当你想改变路径的位置时, 只需要对当前绘图上下文做一次仿射变换. 为啥修改绘图上下文而不是直接修改路径本身呢? 修改绘图上下文的好处就是通过绘图状态的存储和恢复操作,可以取消上次的修改.
你可以使用stroke
和fill
方法来绘制路径. 渲染过程涉及使用路径对象的属性对线条和曲线进行光栅化. 光栅化过程不修改路径对象本身. 因此, 可以在当前上下文或其他上下文中多次渲染同一路径对象.
往路径中添加线条和多边形(polygon)
线段和多边形这些简单的图形是通过moveToPoint:
和addLineToPoint:
方法来逐点构建的. moveToPoint:
方法设置了图形的起点, 从起点出发调用addLineToPoint:
方法往图形中添加一条线. 通过这方式, 你可以连续移动点来添加一系列线段.
代码3-1展示了使用代码创建一个五边形. 代码中, 先创建设置一个初始点, 然后连续添加四条线, 再调用closePath
方法, 自动生成第五条线(连接第四条线的end-point到初始点). 该路径绘制的图形如图3-1所示.
代码清单3-1 创建一个五边形
UIBezierPath *aPath = [UIBezierPath bezierPath];
// Set the starting point of the shape.
[aPath moveToPoint:CGPointMake(100.0, 0.0)];
// Draw the lines.
[aPath addLineToPoint:CGPointMake(200.0, 40.0)];
[aPath addLineToPoint:CGPointMake(160, 140)];
[aPath addLineToPoint:CGPointMake(40.0, 140)];
[aPath addLineToPoint:CGPointMake(0.0, 40.0)];
[aPath closePath];
使用closePath
方法来关闭路径有一个好处是, 在绘制多变型是不需要绘制最后一条边, 因为该方法会自动绘制一条起始点到最后一点的线.
往路径中添加圆弧(Arcs)
使用UIBezierPath类的bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise:
方法可以绘制一段弧. 从该方法的参数(原点, 半径, 起始角度和结束角度, 时针方向),我们可以确定该如何画好圆弧. 图3-2, 展示了这些参数如何确定一段圆弧, 该弧是顺时针方法. 代码3-2,展示了创建图3-2中圆弧的代码.
代码清单3-2 创建一段圆弧路径
// pi is approximately equal to 3.14159265359.
#define DEGREES_TO_RADIANS(degrees) ((pi * degrees)/ 180)
- (UIBezierPath *)createArcPath {
UIBezierPath *aPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(150, 150)
radius:75
startAngle:0
endAngle:DEGREES_TO_RADIANS(135)
clockwise:YES];
return aPath;
}
如果你想将一段圆弧加入另一路径中, 那么请直接修改路径的CGPathRef
类型的对象. 关于如何使用Core Graphic函数修改路径的方法下面内容会提到.
使用Core Graphic函数来修改路径
类UIBezierPath
支持往路径中添加三次和二次贝塞尔曲线. 曲线由起点和终点确定曲线的首尾. 曲线弯曲在起点和终点的切线之间, 弯曲程度由控制点决定, 你也可以增加一个或多个控制点. 图3-3展示了两种类型的曲线间的控制点和曲线弯曲的关系, 底层的数学逻辑请看Wikipedia
往路径中添加贝塞尔曲线可以使用以下方法:
- 二次曲线:
addCurveToPoint:controlPoint1:controlPoint2:
- 三次曲线:
addQuadCurveToPoint:controlPoint:
因为曲线在当前点的基础上添加的, 并且以该点作为曲线的起始点. 所以在调用上面两个方法之前, 需要设置当前点.
创建椭圆和矩形路径
类UIBezierPath
提供了bezierPathWithRect:
和bezierPathWithOvalInRect:
方法来创建矩形和椭圆形. 这个两个方法都是创建一个新的path对象. 你可以直接使用返回的对象或者往该path中加入更多图形.
如果你想往现存的path中添加一个矩形, 那么可以使用moveToPoint
, addLineToPoint:
, closePath
方法来创建矩形, 就像创建多边形一样. 如果你想往现存的path中添加一个椭圆, 最简单的方法是使用Core Graphic. 尽管你可以使用addQuadCurveToPoint:controlPoint:
创建一个类椭圆, 但是CGPathAddEllipseInRect
函数更加简单实用.
使用CoreGraphic来修改路径
UIBezierPath
其实是CGPathRef
数据类型, 以及和该路径相关的绘画属性的包装类. 虽然通常使用UIBezierPath
的方法添加线段和曲线段, 但是该类还开了一个CGPath
属性, 你可以使用它直接修改底层路径数据类型. 当你希望使用Core Graphic框架的函数来修改路径时, 可以使用此属性.
有两种方法可修改UIBezierPath
对象相关的路径. 你可以完全使用Core Graphic函数修改路径, 也可以将Core Graphic函数和UIBezierPath
方法结合使用. 在某些方面, 完全使用Core Graphic函数修改路径更容易. 你可以创建一个可变的CGPathRef
数据类型, 并调用你需要的任何函数来修改路径信息. 完成后, 将路径对象分配给相应的UIBezierPath对象, 如代码3-3所示.
代码清单3-3 将一个新CGPathRef设置到UIBezierPath对象
// Create the path data.
CGMutablePathRef cgPath = CGPathCreateMutable();
CGPathAddEllipseInRect(cgPath, NULL, CGRectMake(0, 0, 300, 300));
CGPathAddEllipseInRect(cgPath, NULL, CGRectMake(50, 50, 200, 200));
// Now create the UIBezierPath object.
UIBezierPath *aPath = [UIBezierPath bezierPath];
aPath.CGPath = cgPath;
aPath.usesEvenOddFillRule = YES;
// 使用完后记得release掉
CGPathRelease(cgPath);
如果你选择混合使用Core Graphic函数和UIBezierPath方法, 那么必须小心路径对象在Core Graphic和UIBezierPath间的来回移动. 因为UIBezierPath对象拥有其底层CGPathRef数据类型,所以不能简单地获取该类型并直接对其进行修改。相反,您必须创建一个可变副本,修改副本,然后将副本分配回CGPath属性,如清单3-4所示
代码清单3-4 混合使用Core Graphic和UIBezierPath
UIBezierPath *aPath = [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 0, 300, 300)];
// Get the CGPathRef and create a mutable version.
CGPathRef cgPath = aPath.CGPath;
CGMutablePathRef mutablePath = CGPathCreateMutableCopy(cgPath);
// Modify the path and assign it back to the UIBezierPath object.
CGPathAddEllipseInRect(mutablePath, NULL, CGRectMake(50, 50, 200, 200));
aPath.CGPath = mutablePath;
// Release both the mutable copy of the path.
CGPathRelease(mutablePath);
渲染贝塞尔路径中的内容
当创建完一个UIBezierPath对象后, 你可以使用stroke
和fill
方法在当前绘图上下文中渲染该路径. 在调试前面方法之前, 这里还需要做一些其他操作来确保路径准确绘制到上下文中:
使用
UIColor
中的方法来设置想要的strokeColor和fillColor将该图形放到目标视图中合适的位置.
如果创建了相对于点(0,0)的路径,则可以对当前绘图上下文应用适当的仿射转换。例如,要从点(10,10)开始绘制形状,需要调用CGContextTranslateCTM函数,并为水平和垂直位移值指定10。调整图形上下文(与调整路径对象中的点相反)是首选的,因为通过保存和恢复以前的图形状态,可以更容易地撤消更改。更新路径的绘图属性. 设置
UIBezierPath
对象的绘图属性, 会将上下文中的绘图属性覆盖掉.
代码3-5展示在视图中绘制椭圆的drawRect:
方法实现. 因为填充操作直接绘制到路径边界,所以该方法在stroke路径之前填充路径。这样可以防止填充颜色遮蔽半行的线。
代码清单3-5 在view中绘制路径
- (void)drawRect:(CGRect)rect {
// Create an oval shape to draw.
UIBezierPath *aPath = [UIBezierPath bezierPathWithOvalInRect:
CGRectMake(0, 0, 200, 100)];
// Set the render colors.
[[UIColor blackColor] setStroke];
[[UIColor redColor] setFill];
CGContextRef aRef = UIGraphicsGetCurrentContext();
// If you have content to draw after the shape,
// save the current state before changing the transform.
//CGContextSaveGState(aRef);
// Adjust the view's origin temporarily. The oval is
// now drawn relative to the new origin point.
CGContextTranslateCTM(aRef, 50, 50);
// Adjust the drawing options as needed.
aPath.lineWidth = 5;
// Fill the path before stroking it so that the fill
// color does not obscure the stroked line.
[aPath fill];
[aPath stroke];
// Restore the graphics state before drawing any other content.
//CGContextRestoreGState(aRef);
}
在路径上执行点击检测
若要确定在路径的填充部分是否发生触摸事件,可以UIBezierPath的contiansPoint:
方法。此方法针对路径对象中的所有封闭子路径测试指定的点,如果位于或位于这些子路径中的任何一个子路径,则返回“YES”。
重要: 方法
containsPoint:
和Core Graphic的hit-testing函数必须依赖关闭的路径. 如果使用打开的路径那么这些方法会返回NO. 如果你想对开发的路径进行hit-test测试那么你必须先创建路径的副本, 然后关闭副本路径, 在使用副本路径进行测试.
如果你想对路径的stroke部分进行hit-test测试, 那么你必须使用CoreGraphic的函数. 函数CGContextPathContainsPoint
可以让你对路径的stroke部分进行hit-test也可对fill部分测试. 代码3-6中展示了测试一点是否和路径重合. 参数inFill控制方法是否对fill部分测试. 进行测试的路径必须包含关闭的子路径.
代码3-6 测试路径中的点
- (BOOL)containsPoint:(CGPoint)point onPath:(UIBezierPath *)path inFillArea:(BOOL)inFill {
CGContextRef context = UIGraphicsGetCurrentContext();
CGPathRef cgPath = path.CGPath;
BOOL isHit = NO;
// Determine the drawing mode to use. Default to
// detecting hits on the stroked portion of the path.
CGPathDrawingMode mode = kCGPathStroke;
if (inFill) {
// Look for hits in the fill area of the path instead.
if (path.usesEvenOddFillRule)
mode = kCGPathEOFill;
else
mode = kCGPathFill;
}
// Save the graphics state so that the path can be
// removed later.
CGContextSaveGState(context);
CGContextAddPath(context, cgPath);
// Do the hit detection.
isHit = CGContextPathContainsPoint(context, point, mode);
CGContextRestoreGState(context);
return isHit;
}