iOS的UIBezierPath类和贝塞尔曲线

Yeolar   2015-03-25 17:58  

在iOS中绘制矢量图或者路径的时候通常会用到 UIBezierPath ,它在 UIKit 中,是CoreGraphics对path的封装。使用 UIBezierPath ,可以绘制直线、椭圆、多边形和贝塞尔曲线等。

使用UIBezierPath的方法

UIBezierPath 是对 CGPathRef 的封装。创建矢量图形时,拆解成一或多条线段,拼接起来,每条线段的终点都是下一条线段的起点。

具体地:

  1. 创建一个 UIBezierPath 对象
  2. moveToPoint: 设置初始线段的起点
  3. 添加线段,定义一或多个子路径
  4. 修改 UIBezierPath 的绘图相关的属性,比如stroke path的属性 lineWidthlineJoinStyle , filled path的属性 usesEvenOddFillRule

如果是矩形或者椭圆之类的特殊图形,可以不用第2步。

绘制各类矢量图形

UIBezierPath 包含了几个特殊形状的类方法,很容易使用。

// 创建矩形
+ (UIBezierPath *)bezierPathWithRect:(CGRect)rect

// 创建圆角矩形
+ (UIBezierPath *)bezierPathWithRoundedRect:(CGRect)rect cornerRadius:(CGFloat)cornerRadius

// 创建矩形内切圆
+ (UIBezierPath *)bezierPathWithOvalInRect:(CGRect)rect

// 创建弧形
+ (UIBezierPath *)bezierPathWithArcCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise

除了这些闭合的特殊路径,也有一些方法用来添加子路径。

// 添加直线
- (void)addLineToPoint:(CGPoint)point

// 添加弧形线段
- (void)addArcWithCenter:(CGPoint)center radius:(CGFloat)radius startAngle:(CGFloat)startAngle endAngle:(CGFloat)endAngle clockwise:(BOOL)clockwise

// 添加二阶贝塞尔曲线
- (void)addQuadCurveToPoint:(CGPoint)endPoint controlPoint:(CGPoint)controlPoint

// 添加三阶贝塞尔曲线
- (void)addCurveToPoint:(CGPoint)endPoint controlPoint1:(CGPoint)controlPoint1 controlPoint2:(CGPoint)controlPoint2

UIBezierPath 封装了 CGPathRef ,它提供了 CGPath 属性使我们可以获取底层的path。

如果我们还希望修改封装的path,有两种方法。一种是完全用CoreGraphics的函数修改,另一种是混用CoreGraphics和 UIBezierPath 的方法。

用CoreGraphics函数修改:

// 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;

// After assigning it to the UIBezierPath object, you can release
// your CGPathRef data type safely.
CGPathRelease(cgPath);

上面完全用CoreGraphics函数创建了path,然后赋给 UIBezierPath 对象的 CGPath 属性。如果我们混用的话,需要注意不能直接对 UIBezierPath 对象的 CGPath 属性进行修改,需要生成副本。

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,就是填充。

贝塞尔曲线

上面提到贝塞尔曲线,贝塞尔曲线不止在绘制路径时有用,动画效果的时间函数也是通过贝塞尔曲线进行设置的。

贝塞尔曲线的特点是切线性质,因此是平滑的。关于它的算法可以参考 Finding a Point on a Bézier Curve: De Casteljau's Algorithm

这里给出整理过的三阶贝塞尔曲线求任意点的具体算法:

// Bezier curve are: '[(0,0), c1, c2, (1,1)]'
+ (CGPoint)pointOnCubicBezierWithControlPoints:(float)c1x :(float)c1y :(float)c2x :(float)c2y :(float)t {
    // x = (1-t)^3 *x0 + 3*t*(1-t)^2 *x1 + 3*t^2*(1-t) *x2 + t^3 *x3
    // y = (1-t)^3 *y0 + 3*t*(1-t)^2 *y1 + 3*t^2*(1-t) *y2 + t^3 *y3

    float c0x = 0, c0y = 0;
    float c3x = 1, c3y = 1;
    float ax, bx, cx;
    float ay, by, cy;
    float tSquared, tCubed;
    float x, y;

    cx = 3.0 * (c1x - c0x);
    bx = 3.0 * (c2x - c1x) - cx;
    ax = c3x - c0x - cx - bx;

    cy = 3.0 * (c1y - c0y);
    by = 3.0 * (c2y - c1y) - cy;
    ay = c3y - c0y - cy - by;

    tSquared = t * t;
    tCubed = tSquared * t;

    x = (ax * tCubed) + (bx * tSquared) + (cx * t) + c0x;
    y = (ay * tCubed) + (by * tSquared) + (cy * t) + c0y;

    return CGPointMake(x, y);
}

有的朋友也许会用到。

有一个在线生成三阶贝塞尔曲线的 网站 很不错,可以做可视化调整。

http://www.yeolar.com/note/2015/03/25/ios-uibezierpath/

http://www.yeolar.com/note/2015/03/25/ios-uibezierpath/