iOS绘图和打印编程指导(三)-使用UIBezierPath来绘制图形

iOS 3.2后, 你就可以使用UIBezierPath类创建向量路径. 类UIBezierPath是使用OC对Core Graphic中的绘图特性的一个封装. 你可以使用这个类去定义简单的形状, 比如想椭圆和矩形, 和一些比较包含直线和曲线的复杂形状. 然后你可以使用这些路径对象在界面上绘制图形. 你可以对路径描边, 填充颜色, 或者两者都有. 你也可以使用路径来裁剪绘画上下文中的区域, 裁剪的区域之后用来修改接下来的绘图操作.

Bezier Path基础


UIBezierPath对象是对CGPathRef类型数据的包装. 路径(path)是使用线段和曲线构成的基于向量的形状. 你可以使用线段构建矩形, 多边形, 使用曲线构建圆弧, 圆, 和其他复杂的曲线图形. 而线是由坐标系中的点构成, 而这些点是由绘画命令来控制的.

路径中的部分线段和曲线构成了子路径. 子路径中的结束点是下个子路径的起始点. 单个UIBezierPath对象可能包含多个子路径, 这些子路径通过命令moveToPoint:来区分. 该命令可以是画笔移动到一个新位置.

路径的创建和使用是分开的, 构建路径的是绘图过程中的第一部分, 下面是构建路径步骤:

  1. 创建路径对象
  2. 设置路径对象(UIBezierPath)一些绘图属性, 比如lineWidth(线宽), lineJoinStyle(连接风格)等路径绘制的设置, 或者使用属性usesEvenOddFillRule来填充路径. 这些属性设置会应用到整个路径.
  3. 使用moveToPoint:命令来开始一个子路径
  4. 通过直线和曲线来构建一个子路径
  5. 调用方法closePath来关闭路径, 会将路径中最后一部分的结束点和第一部分的起始点连接起来, 该过程是可选的.
  6. 重复步骤3,4,5来添加更多子路径, 该过程是可选的.

当你构建路径时, 要相对于原点(0,0)合理安排路径中的点, 这样在后续移动路径的时候比较方便. 在绘制路径时, 点的位置和当前坐标系中的一样. 如果你的路径时相对于原点进行定向的, 当你想改变路径的位置时, 只需要对当前绘图上下文做一次仿射变换. 为啥修改绘图上下文而不是直接修改路径本身呢? 修改绘图上下文的好处就是通过绘图状态的存储和恢复操作,可以取消上次的修改.

你可以使用strokefill方法来绘制路径. 渲染过程涉及使用路径对象的属性对线条和曲线进行光栅化. 光栅化过程不修改路径对象本身. 因此, 可以在当前上下文或其他上下文中多次渲染同一路径对象.

往路径中添加线条和多边形(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];
iOS绘图和打印编程指导(三)-使用UIBezierPath来绘制图形_第1张图片
使用UIBezierPath来绘制图形

使用closePath方法来关闭路径有一个好处是, 在绘制多变型是不需要绘制最后一条边, 因为该方法会自动绘制一条起始点到最后一点的线.

往路径中添加圆弧(Arcs)


使用UIBezierPath类的bezierPathWithArcCenter:radius:startAngle:endAngle:clockwise:方法可以绘制一段弧. 从该方法的参数(原点, 半径, 起始角度和结束角度, 时针方向),我们可以确定该如何画好圆弧. 图3-2, 展示了这些参数如何确定一段圆弧, 该弧是顺时针方法. 代码3-2,展示了创建图3-2中圆弧的代码.

iOS绘图和打印编程指导(三)-使用UIBezierPath来绘制图形_第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

iOS绘图和打印编程指导(三)-使用UIBezierPath来绘制图形_第3张图片
图3-3 贝塞尔曲线

往路径中添加贝塞尔曲线可以使用以下方法:

  • 二次曲线: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对象后, 你可以使用strokefill方法在当前绘图上下文中渲染该路径. 在调试前面方法之前, 这里还需要做一些其他操作来确保路径准确绘制到上下文中:

  • 使用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;
}

你可能感兴趣的:(iOS绘图和打印编程指导(三)-使用UIBezierPath来绘制图形)