UIBezierPath - 基础API介绍实践

UIKit中的UIBezierPath是Core Graphics框架关于path的一个封装。可以创建基于矢量的路径,例如椭圆或者矩形,或者有多个直线和曲线段组成的形状.

Core Graphics 的基本介绍与使用

属性

  • lineWidth:线宽属性定义了 UIBezierPath 对象中绘制的曲线规格. 默认为: 1.0
  • lineCapStyle:应用于曲线的终点和起点. 该属性在一个闭合子路经中是无效果的. 默认为: kCGLineCapButt

枚举值:

1
2
3
4
5
  typedef CF_ENUM(int32_t, CGLineCap) {
kCGLineCapButt,
kCGLineCapRound,
kCGLineCapSquare
};

对应样式:

  • lineJoinStyle:曲线连接点的样式.

枚举值:

1
2
3
4
5
  typedef CF_ENUM(int32_t, CGLineJoin) {
kCGLineJoinMiter,
kCGLineJoinRound,
kCGLineJoinBevel
};

对应样式:

  • miterLimit:两条线交汇处内角和外角之间的最大距离, 仅当连接点样式为 kCGLineJoinMiter时生效.

图解如下

  • flatness:渲染精度(表示真实曲线的点和渲染曲线的点的最大允许距离)。值越小,精度越高。default:0.6。
  • usesEvenOddFillRule:是否使用基偶填充规则。两种规则的详细介绍
1
2
设置为 YES, 则路径将会使用 基偶规则 (even-odd) 进行填充.
设置为 NO, 则路径将会使用 非零规则 (non-zero) 规则进行填充.
  • CGPath:一个不可变的 CGPathRef 对象
1
他可以传入 CoreGraphics 提供的函数中你可以是用 CoreGraphics 框架提供的方法创建一个路径, 并给这个属性赋值, 当时设置了一个新的路径后, 这个将会对你给出的路径对象进行 Copy 操作
  • currentPoint:下一条绘制的直线或曲线的起始点。如果当前路径为空, 那么该属性的值将会是 CGPointZero
  • bounds:路径覆盖的矩形区域。该属性描述的是一个能够完全包含路径中所有点的一个最小的矩形区域. 该区域包含二次贝塞尔曲线和三次贝塞尔曲线的控制点.
  • empty:路径是否为空。 <注>: 就算你仅仅调用了 moveToPoint 方法那么当前路径也被看做不为空.

创建实例对象

  • 创建
1
+ (instancetype) bezierPath;
  • by 矩形
1
2
3
4
5
/**
* 该方法将会创建一个闭合路径, 起始点是 rect 参数的的 origin, 并且按照顺时针方向添加直线, 最终形成矩形
* @param rect: 矩形路径的 Frame
*/
+ (instancetype)bezierPathWithRect:(CGRect)rect;
  • by 椭圆
1
2
3
4
5
/**
* 该方法将会创建一个闭合路径, 该方法会通过顺时针的绘制贝塞尔曲线, 绘制出一个近似椭圆的形状. 如果 rect 参数指定了一个矩形, 那么该 UIBezierPath 对象将会描述一个圆形.
* @param rect: 矩形路径的 Frame
*/
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
  • by 圆角矩形:
1
2
3
4
5
6
7
/**
* 该方法将会创建一个闭合路径, 该方法会顺时针方向连续绘制直线和曲线. 当 rect 为正方形时且 cornerRadius 等于边长一半时, 则该方法会描述一个圆形路径.
* @param rect: 矩形路径的 Frame
* @param cornerRadius: 矩形的圆角半径
*/
+ (instancetype) bezierPathWithRoundedRect:(CGRect)rect
cornerRadius:(CGFloat)cornerRadius;

可以指定矩形的哪个角为圆角:

1
2
3
4
5
6
7
8
9
/**
* 该方法将会创建一个闭合路径, 该方法会顺时针方向连续绘制直线和曲线.
* @param rect: 矩形路径的 Frame
* @param corners: UIRectCorner 枚举类型, 指定矩形的哪个角变为圆角
* @param cornerRadii: 矩形的圆角半径
*/
+ (instancetype) bezierPathWithRoundedRect:(CGRect)rect
byRoundingCorners:(UIRectCorner)corners
cornerRadii:(CGSize)cornerRadii;
  • by 圆弧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 该方法会创建出一个开放路径, 创建出来的圆弧是圆的一部分. 在默认的坐标系统中, 开始角度 和 结束角度 都是基于单位圆的(看下面这张图). 调用这个方法之后, currentPoint 将会设置为圆弧的结束点.
* 举例来说: 指定其实角度为0, 指定结束角度为π, 设置 clockwise 属性为 YES, 将会绘制出圆的下半部分.
* 然而当我们不修改起始角度 和 结束角度, 我们仅仅将 clockwise 角度设置为 NO, 则会绘制出来一个圆的上半部分.
* @param center: 圆心
* @param radius: 半径
* @param startAngle: 起始角度
* @param endAngle: 结束角度
* @param clockwise: 是否顺时针绘制
*/
+ (instancetype) bezierPathWithArcCenter:(CGPoint)center
radius:(CGFloat)radius
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
clockwise:(BOOL)clockwise;

)

  • by CGPath
1
+ (instancetype) bezierPathWithCGPath:(CGPathRef)CGPath;
  • by 反方向Path(这里的方向是指绘制方向)。
1
2
3
4
5
/**
* @return: 返回一个新的 UIBezierPath 对象, 形状和原来路径的形状一样,
* 但是绘制的方向相反.
*/
- (UIBezierPath *) bezierPathByReversingPath;

构造或添加路径

  • 将对象的currentPoint移到某个点。(对于大多数构造路径相关的方法而言, 在你绘制直线或曲线之前, 需要先调用这个方法.)
1
2
3
4
5
6
7
8
/**
* 如果当前有正在绘制的子路径, 该方法则会隐式的结束当前路径,
* 并将 currentPoint 设置为指定点. 当上一条子路径被终止, 该方法
* 实际上并不会去闭合上一条子路径. 所以上一条自路径的起始点 和
* 结束点并没有被链接.
* @param point: 当前坐标系统中的某一点
*/
- (void)moveToPoint:(CGPoint)point;
  • 追加 一条直线
1
2
3
4
5
6
7
8
9
/**
* 该方法将会从 currentPoint 到 指定点 链接一条直线.
* Note: 在追加完这条直线后, 该方法将会更新 currentPoint 为 指定点
* 调用该方法之前, 你必须先设置 currentPoint. 如果当前绘制路径
* 为空, 并且未设置 currentPoint, 那么调用该方法将不会产生任何
* 效果.
* @param point: 绘制直线的终点坐标, 当前坐标系统中的某一点
*/
- (void)addLineToPoint:(CGPoint)point;
  • 追加 一条圆弧
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 该方法将会从 currentPoint 添加一条指定的圆弧.
* 该方法的介绍和构造方法中的一样. 请前往上文查看
* @param center: 圆心
* @param radius: 半径
* @param startAngle: 起始角度
* @param endAngle: 结束角度
* @param clockwise: 是否顺时针绘制
*/
- (void)addArcWithCenter:(CGPoint)center
radius:(CGFloat)radius
startAngle:(CGFloat)startAngle
endAngle:(CGFloat)endAngle
clockwise:(BOOL)clockwise NS_AVAILABLE_IOS(4_0);
  • 追加 一条三次贝塞尔曲线
1
2
3
4
5
6
7
8
9
10
11
12
13
14
/**
* 该方法将会从 currentPoint 到 指定的 endPoint 追加一条三次贝塞尔曲线.
* 三次贝塞尔曲线的弯曲由两个控制点来控制. 如下图所示
* Note: 调用该方法前, 你必须先设置 currentPoint, 如果路径为空,
* 并且尚未设置 currentPoint, 调用该方法则不会产生任何效果.
* 当添加完贝塞尔曲线后, 该方法将会自动更新 currentPoint 为
* 指定的结束点
* @param endPoint: 终点
* @param controlPoint1: 控制点1
* @param controlPoint2: 控制点2
*/
- (void)addCurveToPoint:(CGPoint)endPoint
controlPoint1:(CGPoint)controlPoint1
controlPoint2:(CGPoint)controlPoint2;

图解如下:

)

  • 追加一条二次贝塞尔曲线
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* 该方法将会从 currentPoint 到 指定的 endPoint 追加一条二次贝塞尔曲线.
* currentPoint、endPoint、controlPoint 三者的关系最终定义了二次贝塞尔曲线的形状.
* 二次贝塞尔曲线的弯曲由一个控制点来控制. 如下图所示
* Note: 调用该方法前, 你必须先设置 currentPoint, 如果路径为空,
* 并且尚未设置 currentPoint, 调用该方法则不会产生任何效果.
* 当添加完贝塞尔曲线后, 该方法将会自动更新 currentPoint 为
* 指定的结束点
* @param endPoint: 终点
* @param controlPoint: 控制点
*/
- (void)addQuadCurveToPoint:(CGPoint)endPoint
controlPoint:(CGPoint)controlPoint;

图解如下:

  • 追加 UIBezierPath 实例对象
1
2
3
4
5
/**
* 该方法将会在当前 UIBezierPath 对象的路径中追加
* 指定的 UIBezierPath 对象中的内容.
*/
- (void)appendPath:(UIBezierPath *)bezierPath;

虚线路径

  • 构建一条虚线路径:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**
* @param pattern: 该属性是一个 C 语言的数组, 其中每一个元素都是 CGFloat
* 数组中的元素代表着线段每一部分的长度, 第一个元素代表线段的第一条线,
* 第二个元素代表线段中的第一个间隙. 这个数组中的值是轮流的. 来解释一下
* 什么叫轮流的.
* 举个例子: 声明一个数组 CGFloat dash[] = @{3.0, 1.0};
* 这意味着绘制的虚线的第一部分长度为3.0, 第一个间隙长度为1.0, 虚线的
* 第二部分长度为3.0, 第二个间隙长度为1.0. 以此类推.

* @param count: 这个参数是 pattern 数组的个数
* @param phase: 这个参数代表着, 虚线从哪里开始绘制.
* 举个例子: 这是 phase 为 6. pattern[] = @{5, 2, 3, 2}; 那么虚线将会
* 第一个间隙的中间部分开始绘制, 如果不是很明白就请继续往下看,
* 下文实战部分会对虚线进行讲解.
*/
- (void)setLineDash:(const CGFloat *)pattern
count:(NSInteger)count
phase:(CGFloat)phase;
  • 获取虚线的模式:
1
2
3
4
5
6
7
8
9
10
/**
* 该方法可以重新获取之前设置过的虚线样式.
* Note: pattern 这个参数的容量必须大于该方法返回数组的容量.
* 如果无法确定数组的容量, 那么可以调用两次该方法, 第一次
* 调用该方法的时候, 传入 count 参数, 然后在用 count 参数
* 来申请 pattern 数组的内存空间. 然后再第二次正常的调用该方法
*/
- (void)getLineDash:(CGFloat *)pattern
count:(NSInteger *)count
phase:(CGFloat *)phase;

尝试Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
- (void) typeDashLine {

// 1. 先创建三条路径, 有对比更有助于理解
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint: CGPointMake(80, 40)];
[path addLineToPoint: CGPointMake(self.frame.size.width - 40, 40)];
path.lineWidth = 2;


UIBezierPath *path1 = [UIBezierPath bezierPath];
[path1 moveToPoint: CGPointMake(80, 80)];
[path1 addLineToPoint: CGPointMake(self.frame.size.width - 40, 80)];
path1.lineWidth = 2;


UIBezierPath *path2 = [UIBezierPath bezierPath];
[path2 moveToPoint: CGPointMake(80, 120)];
[path2 addLineToPoint: CGPointMake(self.frame.size.width - 40, 120)];
path2.lineWidth = 2;

// 2. 这部分是配置三条路径虚线的规格, 重点主要是这部分.
CGFloat dashLineConfig[] = {8.0, 4.0};
[path setLineDash: dashLineConfig
count: 2
phase: 0];


CGFloat dashLineConfig1[] = {8.0, 4.0, 16.0, 8.0};
[path1 setLineDash: dashLineConfig1
count: 4
phase: 0];


CGFloat dashLineConfig2[] = {8.0, 4.0, 16.0, 8.0};
[path2 setLineDash: dashLineConfig2
count: 4
phase: 12];

// 3. 绘制
[[UIColor orangeColor] set];
[path stroke];
[path1 stroke];
[path2 stroke];
}

显示效果:

)

更改路径

  • 关闭当前子路径
1
2
3
4
5
6
/**
* 该方法将会从 currentPoint 到子路经的起点 绘制一条直线,
* 以此来关闭当前的自路径. 紧接着该方法将会更新 currentPoint
* 为 刚添加的这条直线的终点, 也就是当前子路经的起点.
*/
- (void)closePath;
  • 删除 UIBezierPath 对象中的所有点, 效果也就等同于删除了所有子路经:
1
- (void)removeAllPoints;
  • 剪切路径
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 该方法将会修改当前绘图上下文的可视区域.
* 当调用这个方法之后, 会导致接下来所有的渲染
* 操作, 只会在剪切下来的区域内进行, 区域外的
* 内容将不会被渲染.
* 如果你希望执行接下来的绘图时, 删除剪切区域,
* 那么你必须在调用该方法前, 先使用 CGContextSaveGState 方法
* 保存当前的绘图状态, 当你不再需要这个剪切区域
* 的时候, 你只需要使用 CGContextRestoreGState 方法
* 来恢复之前保存的绘图状态就可以了.
* @param blendMode: 混合模式决定了如何和
* 已经存在的被渲染过的内容进行合成
* @param alpha: 填充路径时的透明度
*/
- (void)addClip;
  • 放射变换操作
1
2
3
4
5
/**
* 该方法将会直接对路径中的所有点进行指定的放射
* 变换操作.
*/
- (void)applyTransform:(CGAffineTransform)transform;

绘制相关

  • 填充路径:
1
2
3
4
5
6
7
/**
* 该方法当前的填充颜色 和 绘图属性对路径的封闭区域进行填充.
* 如果当前路径是一条开放路径, 该方法将会隐式的将路径进行关闭后进行填充
* 该方法在进行填充操作之前, 会自动保存当前绘图的状态, 所以我们不需要
* 自己手动的去保存绘图状态了.
*/
- (void)fill;
  • 使用混合模式填充路径
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 该方法当前的填充颜色 和 绘图属性 (外加指定的混合模式 和 透明度)
* 对路径的封闭区域进行填充. 如果当前路径是一条开放路径, 该方法将
* 会隐式的将路径进行关闭后进行填充
* 该方法在进行填充操作之前, 会自动保存当前绘图的状态, 所以我们不需要
* 自己手动的去保存绘图状态了.
*
* @param blendMode: 混合模式决定了如何和已经存在的被渲染过的内容进行合成
* @param alpha: 填充路径时的透明度
*/
- (void)fillWithBlendMode:(CGBlendMode)blendMode
alpha:(CGFloat)alpha;
  • 绘制路径:(一般用于将路径相关设置完成后的最后一步操作).
1
- (void)stroke;

判断方法

  • 是否包含某个点
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 该方法返回一个布尔值, 当曲线的覆盖区域包含
* 指定的点(内部点), 则返回 YES, 否则返回 NO.
* Note: 如果当前的路径是一个开放的路径, 那么
* 就算指定点在路径覆盖范围内, 该方法仍然会
* 返回 NO, 所以如果你想判断一个点是否在一个
* 开放路径的范围内时, 你需要先Copy一份路径,
* 并调用 -(void)closePath; 将路径封闭, 然后
* 再调用此方法来判断指定点是否是内部点.
* @param point: 指定点.
*/
- (BOOL) containsPoint:(CGPoint)point;

Demo实例

  • 属性方法简单应用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
    
//========================================================
//>>>Part one : 创建UIBezierPath对象。
//========================================================

//1、通过矩形:
UIBezierPath * rectPath = [UIBezierPath bezierPathWithRect:CGRectMake(0, 0, 30, 50)];
/**
判断相关:
*/
//ONE:是否包含。
BOOL isContain = [rectPath containsPoint:CGPointMake(25, 24)];
if (isContain) {
NSLog(@"contain");
}
//TWO:路径是否为空。
BOOL isEmpty = [rectPath isEmpty];
if (!isEmpty) {
NSLog(@"not empty");
}

/**
相关属性:
*/
rectPath.lineWidth = 3;
rectPath.lineCapStyle = kCGLineCapSquare; //曲线终点样式,不适用于闭合路径。
rectPath.lineJoinStyle = kCGLineJoinMiter; //曲线连接样式。
rectPath.miterLimit = 2; //内外角最大距离。
rectPath.flatness = 0.6; //默认0.6,越低精度越高。
rectPath.usesEvenOddFillRule = YES; //是否适用奇偶填充规则。默认非零规则填充。
NSLog(@"覆盖的矩形区域:%@",[NSValue valueWithCGRect:rectPath.bounds]);//判断覆盖的矩形区域

[[UIColor orangeColor]set];
[rectPath stroke];

//2、通过椭圆(矩形的内切椭圆)
UIBezierPath * ovalPath= [UIBezierPath bezierPathWithOvalInRect:CGRectMake(0, 70, 30, 50)];
[[UIColor redColor]set];
[ovalPath stroke];

//3、通过圆角矩形:
UIBezierPath * cornerRectPath = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 130, 30, 50) cornerRadius:3];
[[UIColor blackColor] set];
[cornerRectPath stroke];

//4、通过圆弧:
UIBezierPath * arcPath = [UIBezierPath bezierPathWithArcCenter:CGPointMake(60, 200) radius:50 startAngle:M_PI_2 endAngle:M_PI clockwise:YES];
[[UIColor brownColor] set];
arcPath.lineWidth = 4;
[arcPath stroke];

//5、通过path。
CGPathRef pathRef = CGPathCreateWithRect(CGRectMake(0, 280, 30, 50), nil);
UIBezierPath * pathPath = [UIBezierPath bezierPathWithCGPath:pathRef];
[[UIColor redColor]set];
[pathPath stroke];


//========================================================
//>>>Part two:构造路径:
//========================================================
UIBezierPath * allocPath = [UIBezierPath bezierPath];
allocPath.lineWidth= 4;
[allocPath moveToPoint:CGPointMake(50, 350)];

//追加一条直线路径:
// [allocPath addLineToPoint:CGPointMake(50, 400)];

//追加一条圆弧:(当起始点到圆心的距离大于半径的时候,起始点到距离圆心半径的点上会作一条直线)
// [allocPath addArcWithCenter:CGPointMake(50, 400) radius:50 startAngle:M_PI_2+M_PI endAngle:M_PI_2 clockwise:NO];

//追加一条三次贝塞尔曲线:
// [allocPath addCurveToPoint:CGPointMake(50, 400) controlPoint1:CGPointMake(60, 370) controlPoint2:CGPointMake(30, 385)];

//追加一条二次贝塞尔曲线;
[allocPath addQuadCurveToPoint:CGPointMake(50, 400) controlPoint:CGPointMake(30, 385)];
//关闭路径:
[allocPath closePath];
//删除所有点:
// [allocPath removeAllPoints];
//添加一个路径到当前路径中来。
// [allocPath appendPath:pathPath];
//填充;
// [allocPath fill];
//选择将以何种混合方式进行填充。//?
// [allocPath fillWithBlendMode:kCGBlendModeLighten alpha:0.8];



[[UIColor orangeColor] set];
[allocPath stroke];


[[UIColor redColor]setFill];

UIRectFill(CGRectMake(100, 50, 100, 50));

CAShapeLayer

属性介绍:

大多属性和UIBezierPath的属性类似。

  • path: CGPathRef 对象,图形边线路径。

  • fillColor:CGColorRef对象,图形填充色,默认为黑色。

  • fillRule:填充规则。类似于UIBezierPath的fillMode属性。

kCAFillRuleNonZero : 非零环绕数规则。

这个规则通过从canvas上的某个点往任一方向绘制射线到无穷远,然后检查图形的线段和射线相交的点,来确定“内部区域”。从0开始计数,每次路径线段是从左到右穿过射线就加一,从右到左的就减一。通过计算交叉点,如果结果是0,则这个点在路径外边,不然,就是在里边。

[图片上传失败…(image-4ee41e-1552033143799)]

kCAFillRuleEvenOdd : 奇偶原则。

通过从canvas上某个点往任一方向绘制射线到无穷远,然后计算给定图形上线段路径和该射线交叉点的数量。如果这个数是奇数,那么该点在图形内部;如果是偶数,该点在图形外部。

[图片上传失败…(image-53b934-1552033143799)]

  • strokeColor:边线颜色。

  • lineDashPhase:边线样式的起始位置,即,如果lineDashPattern设置为@[2,2,3,4], lineDashPhase即为第一个长度为2的线的起始位置。

  • strokeStart,strokeEnd: [0,1]表示画边线的起点和终点。
  • lineDashPattern: NSNumber数组,依次表示单个线的长度和空白的长度。

####DEMO

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//   创建一个路径对象
UIBezierPath *linePath = [UIBezierPath bezierPath];
// 起点
[linePath moveToPoint:(CGPoint){20,20}];
// 其他点
[linePath addLineToPoint:(CGPoint){180,160}];
[linePath addLineToPoint:(CGPoint){200,50}];
[linePath addLineToPoint:CGPointMake(250, 250)];

// 设置路径画布
CAShapeLayer *lineLayer = [CAShapeLayer layer];
lineLayer.bounds = (CGRect){0,0,200,200};
lineLayer.position = CGPointMake(100, 100);
lineLayer.lineWidth = 2.0;
lineLayer.strokeColor = [UIColor blueColor].CGColor; // 边线颜色

lineLayer.path = linePath.CGPath;
lineLayer.fillColor = nil; // 默认是black

// 添加到图层上
[self.layer addSublayer:lineLayer];

更多Demo可见关于贝塞尔曲线与CAShapeLayer的学习

####总结:

UIBezierPath 在当前的绘图上下文中绘制图形了. 因为创建、 配置、 渲染路径等操作, 都是完全不同的步骤, 所以你可以在你的代码中非常容易的对UIBezierPath 对象进行复用. 你甚至可以使用同一个 UIBezierPath 对象去渲染同一个图形很多次, 你也可以再多次渲染的间隔中, 修改属性来渲染出不同样式的路径.

当你为UIBezierPath 对象配置完几何路径绘图属性之后, 你就可以使用strokefill方法在当前的绘图上下文中进行绘制了. stroke方法将会使用当前的strokeColor绘图属性来描绘曲线的轮廓. 同样的, fill 方法将会使用fillColor 来填充路径所围成的图形(使用UIColor 类方法来设置strokeColorfillColor).