由直线和曲线线段组成的路径,可以在自定义视图中呈现这些线段。
路径可以定义简单的形状,例如矩形,椭圆形和弧形,或者它们可以定义包含直线和曲线线段混合的复杂多边形。定义形状后,可以使用此类的其他方法在当前图形上下文中呈现路径。
UIBezierPath对象将路径的几何与在渲染期间描述路径的属性组合在一起。你可以单独设置几何和属性,并可以相互独立地更改它们。在按照你希望的方式配置对象后,你可以告诉它在当前上下文中绘制自己。由于创建,配置和渲染过程都是不同的步骤,因此可以在代码中轻松地重用Bézier路径对象。你甚至可以使用同一个对象多次渲染相同的形状,也许可以在连续的绘制调用之间更改渲染选项。
你可以通过操纵路径的当前点来设置路径的几何。创建新的空路径对象时,当前点未定义,必须明确设置。要在不绘制线段的情况下移动当前点,请使用moveToPoint:方法。所有其他方法都会在路径中添加线段或曲线段。添加新段的方法始终假设你从当前点开始并以你指定的某个新点结束。添加段后,新段的结束点将自动成为当前点。
单个贝塞尔路径对象可以包含任意数量的打开或关闭子路径,其中每个子路径表示一系列连接的路径段。调用closePath方法通过添加从当前点到子路径中第一个点的直线段来关闭子路径。调用moveToPoint:方法结束当前子路径(不关闭它)并设置下一个子路径的起始点。 Bézier路径对象的子路径共享相同的绘图属性,必须作为一组进行操作。要绘制具有不同属性的子路径,必须将每个子路径放在其自己的UIBezierPath对象中。
配置贝塞尔路径的几何和属性后,使用笔触和填充方法在当前图形上下文中绘制路径。笔划方法使用当前笔划颜色和Bézier路径对象的属性来跟踪路径的轮廓。类似地,填充方法使用当前填充颜色填充路径包围的区域。 (使用UIColor类设置笔触和填充颜色。)
除了使用贝塞尔路径对象绘制形状之外,你还可以使用它来定义新的剪切区域。 addClip方法将路径对象表示的形状与图形上下文的当前剪切区域相交。在后续绘制期间,只有位于新交叉区域内的内容才会实际呈现给图形上下文。
文档接口:
- 创建并且返回一个新的
UIBezierPath
对象
+ (instancetype) bezierPath;
- 通过一个矩形, 创建并且返回一个新的
UIBezierPath
对象
/**
* 该方法将会创建一个闭合路径, 起始点是 rect 参数的的 origin, 并且按照顺时针方向添加直线, 最终形成矩形
* @param rect: 矩形路径的 Frame
*/
+ (instancetype)bezierPathWithRect:(CGRect)rect;
- 通过一个指定的矩形中的椭圆形, 创建并且返回一个新的
UIBezierPath
对象
/**
* 该方法将会创建一个闭合路径, 该方法会通过顺时针的绘制贝塞尔曲线, 绘制出一个近似椭圆的形状. 如果 rect 参数指定了一个矩形, 那么该 UIBezierPath 对象将会描述一个圆形.
* @param rect: 矩形路径的 Frame
*/
+ (instancetype)bezierPathWithOvalInRect:(CGRect)rect;
- 根据一个圆角矩形, 创建并且返回一个新的
UIBezierPath
对象
/**
* 该方法将会创建一个闭合路径, 该方法会顺时针方向连续绘制直线和曲线. 当 rect 为正方形时且 cornerRadius 等于边长一半时, 则该方法会描述一个圆形路径.
* @param rect: 矩形路径的 Frame
* @param cornerRadius: 矩形的圆角半径
*/
+ (instancetype) bezierPathWithRoundedRect:(CGRect)rect
cornerRadius:(CGFloat)cornerRadius;
- 根据一个圆角矩形, 创建并且返回一个新的
UIBezierPath
对象
/**
* 该方法将会创建一个闭合路径, 该方法会顺时针方向连续绘制直线和曲线.
* @param rect: 矩形路径的 Frame
* @param corners: UIRectCorner 枚举类型, 指定矩形的哪个角变为圆角
* @param cornerRadii: 矩形的圆角半径
*/
+ (instancetype) bezierPathWithRoundedRect:(CGRect)rect
byRoundingCorners:(UIRectCorner)corners
cornerRadii:(CGSize)cornerRadii;
- 通过一个圆弧, 创建并且返回一个新的
UIBezierPath
对象
/**
* 该方法会创建出一个开放路径, 创建出来的圆弧是圆的一部分. 在默认的坐标系统中, 开始角度 和 结束角度 都是基于单位圆的(看下面这张图). 调用这个方法之后, 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;
- 通过一个 CGPath, 创建并且返回一个新的
UIBezierPath
对象
+ (instancetype) bezierPathWithCGPath:(CGPathRef)CGPath;
- 创建并返回一个新的BezierPath, 这个 BezierPath 的方向是原 BezierPath 的反方向
/**
* 通过该方法反转一条路径, 并不会修改该路径的样子. 它仅仅是修改了绘制的方向
* @return: 返回一个新的 UIBezierPath 对象, 形状和原来路径的形状一样,
* 但是绘制的方向相反.
*/
- (UIBezierPath *) bezierPathByReversingPath;
构造路径
- 将
UIBezierPath
对象的currentPoint
移动到指定的点
/**
* 如果当前有正在绘制的子路径, 该方法则会隐式的结束当前路径,
* 并将 currentPoint 设置为指定点. 当上一条子路径被终止, 该方法
* 实际上并不会去闭合上一条子路径. 所以上一条自路径的起始点 和
* 结束点并没有被链接.
* 对于大多数构造路径相关的方法而言, 在你绘制直线或曲线之前, 需要先调用这个方法.
* @param point: 当前坐标系统中的某一点
*/
- (void)moveToPoint:(CGPoint)point;
- 在当前
子路径
中追加一条直线
/**
* 该方法将会从 currentPoint 到 指定点 链接一条直线.
* Note: 在追加完这条直线后, 该方法将会更新 currentPoint 为 指定点
* 调用该方法之前, 你必须先设置 currentPoint. 如果当前绘制路径
* 为空, 并且未设置 currentPoint, 那么调用该方法将不会产生任何
* 效果.
* @param point: 绘制直线的终点坐标, 当前坐标系统中的某一点
*/
- (void)addLineToPoint:(CGPoint)point;
- 在当前
子路径
中追加一条圆弧
/**
* 该方法将会从 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);
- 在当前
子路经
中追加一条三次贝塞尔曲线
/**
* 该方法将会从 currentPoint 到 指定的 endPoint 追加一条三次贝塞尔曲线.
* 三次贝塞尔曲线的弯曲由两个控制点来控制. 如下图所示
* Note: 调用该方法前, 你必须先设置 currentPoint, 如果路径为空,
* 并且尚未设置 currentPoint, 调用该方法则不会产生任何效果.
* 当添加完贝塞尔曲线后, 该方法将会自动更新 currentPoint 为
* 指定的结束点
* @param endPoint: 终点
* @param controlPoint1: 控制点1
* @param controlPoint2: 控制点2
*/
- (void)addCurveToPoint:(CGPoint)endPoint
controlPoint1:(CGPoint)controlPoint1
controlPoint2:(CGPoint)controlPoint2;
- 在当前
子路经
中追加一条二次贝塞尔曲线
/**
* 该方法将会从 currentPoint 到 指定的 endPoint 追加一条二次贝塞尔曲线.
* currentPoint、endPoint、controlPoint 三者的关系最终定义了二次贝塞尔曲线的形状.
* 二次贝塞尔曲线的弯曲由一个控制点来控制. 如下图所示
* Note: 调用该方法前, 你必须先设置 currentPoint, 如果路径为空,
* 并且尚未设置 currentPoint, 调用该方法则不会产生任何效果.
* 当添加完贝塞尔曲线后, 该方法将会自动更新 currentPoint 为
* 指定的结束点
* @param endPoint: 终点
* @param controlPoint: 控制点
*/
- (void)addQuadCurveToPoint:(CGPoint)endPoint
controlPoint:(CGPoint)controlPoint;
- 关闭当前
子路经
/**
* 该方法将会从 currentPoint 到子路经的起点 绘制一条直线,
* 以此来关闭当前的自路径. 紧接着该方法将会更新 currentPoint
* 为 刚添加的这条直线的终点, 也就是当前子路经的起点.
*/
- (void)closePath;
- 删除
UIBezierPath
对象中的所有点, 效果也就等同于删除了所有子路经
- (void)removeAllPoints;
- 将指定
UIBezierPath
中的内容添加到当前UIBezierPath
对象中
/**
* 该方法将会在当前 UIBezierPath 对象的路径中追加
* 指定的 UIBezierPath 对象中的内容.
*/
- (void)appendPath:(UIBezierPath *)bezierPath;
-
UIBezierPath
中的CGPath
对象
此属性包含任何给定时间点的路径快照。 获取此属性将返回一个不可变的路径对象CGPathRef,你可以将该对象传递给Core Graphics函数。 路径对象本身由UIBezierPath对象拥有,仅在你对路径进行进一步修改之前有效。
您可以将此属性的值设置为使用Core Graphics框架的功能构建的路径。 设置新路径时,此方法将会对你给出的路径对象进行 Copy 操作。
@property(nonatomic) CGPathRef CGPath;
- 绘图路径中的当前点
此属性中的值表示新线和曲线段的起点。 如果当前路径为空,则此属性的值将会是 CGPointZero
/**
* 该属性的值, 将会是下一条绘制的直线或曲线的起始点.
* 如果当前路径为空, 那么该属性的值将会是 CGPointZero
*/
@property(nonatomic, readonly) CGPoint currentPoint;
绘图属性
- 线宽
/**
* 线宽属性定义了 `UIBezierPath` 对象中绘制的曲线规格. 默认为: 1.0
*/
@property(nonatomic) CGFloat lineWidth;
- 曲线终点样式
/**
* 该属性应用于曲线的终点和起点. 该属性在一个闭合子路经中是无效果的. 默认为: kCGLineCapButt
*/
@property(nonatomic) CGLineCap lineCapStyle;
typedef CF_ENUM(int32_t, CGLineCap) {
kCGLineCapButt,//该属性值指定不绘制端点, 线条结尾处直接结束。这是默认值。
kCGLineCapRound,//该属性值指定绘制圆形端点, 线条结尾处绘制一个直径为线条宽度的半圆。
kCGLineCapSquare//该属性值指定绘制方形端点。 线条结尾处绘制半个边长为线条宽度的正方形。需要说明的是,这种形状的端点与“butt”形状的端点十分相似,只是采用这种形式的端点的线条略长一点而已
};
- 曲线连接点样式
@property(nonatomic) CGLineJoin lineJoinStyle;
typedef CF_ENUM(int32_t, CGLineJoin) {
kCGLineJoinMiter,//斜接
kCGLineJoinRound,//圆滑衔接
kCGLineJoinBevel//斜角连接
};
- 内角和外角距离
/**
* 两条线交汇处内角和外角之间的最大距离, 只有当连接点样式为 kCGLineJoinMiter
* 时才会生效,最大限制为10
* 我们都知道, 两条直线相交时, 夹角越小, 斜接长度就越大.
* 该属性就是用来控制最大斜接长度的.
* 当我们设置了该属性, 如果斜接长度超过我们设置的范围,
* 则连接处将会以 kCGLineJoinBevel 连接类型进行显示.
*/
@property(nonatomic) CGFloat miterLimit;
- 渲染精度
/**
* 该属性用来确定渲染曲线路径的精确度.
* 该属性的值用来测量真实曲线的点和渲染曲线的点的最大允许距离.
* 值越小, 渲染精度越高, 会产生相对更平滑的曲线, 但是需要花费更
* 多的计算时间. 值越大导致则会降低渲染精度, 这会使得渲染的更迅
* 速. flatness 的默认值为 0.6.
* Note: 大多数情况下, 我们都不需要修改这个属性的值. 然而当我们
* 希望以最小的消耗去绘制一个临时的曲线时, 我们也许会临时增
* 大这个值, 来获得更快的渲染速度.
*/
@property(nonatomic) CGFloat flatness;
- 是否使用基偶填充规则
/**
* 设置为 YES, 则路径将会使用 基偶规则 (even-odd) 进行填充.
* 设置为 NO, 则路径将会使用 非零规则 (non-zero) 规则进行填充.
*/
@property(nonatomic) BOOL usesEvenOddFillRule;
- 重新获取虚线的模式
/**
* 该方法可以重新获取之前设置过的虚线样式.
* Note: pattern 这个参数的容量必须大于该方法返回数组的容量.
* 如果无法确定数组的容量, 那么可以调用两次该方法, 第一次
* 调用该方法的时候, 传入 count 参数, 然后在用 count 参数
* 来申请 pattern 数组的内存空间. 然后再第二次正常的调用该方法
*/
- (void)getLineDash:(CGFloat *)pattern
count:(NSInteger *)count
phase:(CGFloat *)phase;
绘制路径
- 填充路径
用 stroke 得到的是不被填充的 view ,[path fill]; 用 fill 得到的内部被填充的 view
/**
* 该方法当前的填充颜色 和 绘图属性对路径的封闭区域进行填充.
* 如果当前路径是一条开放路径, 该方法将会隐式的将路径进行关闭后进行填充
* 该方法在进行填充操作之前, 会自动保存当前绘图的状态, 所以我们不需要
* 自己手动的去保存绘图状态了.
*/
- (void)fill;
- 使用混合模式进行填充
/**
* 该方法当前的填充颜色 和 绘图属性 (外加指定的混合模式 和 透明度)
* 对路径的封闭区域进行填充. 如果当前路径是一条开放路径, 该方法将
* 会隐式的将路径进行关闭后进行填充
* 该方法在进行填充操作之前, 会自动保存当前绘图的状态, 所以我们不需要
* 自己手动的去保存绘图状态了.
*
* @param blendMode: 混合模式决定了如何和已经存在的被渲染过的内容进行合成
* @param alpha: 填充路径时的透明度
*/
- (void)fillWithBlendMode:(CGBlendMode)blendMode
alpha:(CGFloat)alpha;
- 绘制路径
- (void)stroke;
- 使用混合模式进行填充
/**
* @param blendMode: 混合模式决定了如何和已经存在的被渲染过的内容进行合成
* @param alpha: 填充路径时的透明度
*/
- (void)strokeWithBlendMode:(CGBlendMode)blendMode
alpha:(CGFloat)alpha;
剪切路径
- 剪切路径
/**
* 该方法将会修改当前绘图上下文的可视区域.
* 当调用这个方法之后, 会导致接下来所有的渲染
* 操作, 只会在剪切下来的区域内进行, 区域外的
* 内容将不会被渲染.
* 如果你希望执行接下来的绘图时, 删除剪切区域,
* 那么你必须在调用该方法前, 先使用 CGContextSaveGState 方法
* 保存当前的绘图状态, 当你不再需要这个剪切区域
* 的时候, 你只需要使用 CGContextRestoreGState 方法
* 来恢复之前保存的绘图状态就可以了.
* @param blendMode: 混合模式决定了如何和
* 已经存在的被渲染过的内容进行合成
* @param alpha: 填充路径时的透明度
*/
- (void)addClip;
Hit Detection
- 是否包含某个点
/**
* 该方法返回一个布尔值, 当曲线的覆盖区域包含
* 指定的点(内部点), 则返回 YES, 否则返回 NO.
* Note: 如果当前的路径是一个开放的路径, 那么
* 就算指定点在路径覆盖范围内, 该方法仍然会
* 返回 NO, 所以如果你想判断一个点是否在一个
* 开放路径的范围内时, 你需要先Copy一份路径,
* 并调用 -(void)closePath; 将路径封闭, 然后
* 再调用此方法来判断指定点是否是内部点.
* @param point: 指定点.
*/
- (BOOL) containsPoint:(CGPoint)point;
- 路径是否为空
/**
* 检测当前路径是否绘制过直线或曲线.
* Note: 记住, 就算你仅仅调用了 moveToPoint 方法
* 那么当前路径也被看做不为空.
*/
@property (readonly, getter=isEmpty) BOOL empty;
- 路径覆盖的矩形区域
/**
* 该属性描述的是一个能够完全包含路径中所有点
* 的一个最小的矩形区域. 该区域包含二次贝塞尔
* 曲线和三次贝塞尔曲线的控制点.
*/
@property (nonatomic, readonly) CGRect bounds;
Apply Transform
- Apply Transform
/**
* 该方法将会直接对路径中的所有点进行指定的放射
* 变换操作.
*/
- (void)applyTransform:(CGAffineTransform)transform;
使用示例
简单的写几句代码, 看看效果
bezierPathByReversingPath
- (UIBezierPath *) bezierPathByReversingPath;
把这个方法拿出来和大家分享, 是因为我最初看到这个方法的介绍之后, 有了一个错误的理解. 开始我认为调用这个方法, 会产生一个镜像的路径出来(就像照镜子一样的那种反转). 其实并不是这样的, 这个方法并不会去修改一条路径的形状, 仅仅是改变了绘制路径的方向. 先来看代码:
- (void) drawRect:(CGRect)rect {
// 1. 随便画一个路径出来.
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint: CGPointMake(10, 10)];
[path addLineToPoint: CGPointMake(80, 40)];
[path addLineToPoint: CGPointMake( 40, 80)];
[path addLineToPoint: CGPointMake(40, 40)];
path.lineWidth = 3;
// 2. 为这条路径制作一个反转路径
UIBezierPath *reversingPath = [path bezierPathByReversingPath];
reversingPath.lineWidth = 3;
// 3. 为了避免两条路径混淆在一起, 我们为第一条路径做一个位移
CGAffineTransform transform = CGAffineTransformMakeTranslation(200, 0);
[path applyTransform: transform];
// 4. 设置颜色, 并绘制路径
[[UIColor redColor] set];
[path stroke];
[[UIColor greenColor] set];
[reversingPath stroke];
}
效果:
左侧绿色路径就是我们调用 - (UIBezierPath *) bezierPathByReversingPath;
方法反转出来的路径, 右侧则是原路径, 我们可以看到, 两条路径在形状上并没有任何的变化. 通过实践证明了我最初的理解是错误的. 那么我们现在来证明一下路径的绘制方向发生了改变. 前文中提到, 当绘制一条直线之后, currentPoint
将会自动更新为这条直线的endPoint
. 我们尝试一下分别使用两条路径的currentPoint
与 self.center
进行连接, 将刚才的代码修改为下面这个样子:
- (void) drawRect:(CGRect)rect {
// 1. 随便画一个路径出来.
UIBezierPath *path = [UIBezierPath bezierPath];
[path moveToPoint: CGPointMake(10, 10)];
[path addLineToPoint: CGPointMake(80, 40)];
[path addLineToPoint: CGPointMake( 40, 80)];
[path addLineToPoint: CGPointMake(40, 40)];
path.lineWidth = 3;
// 2. 为这条路径制作一个反转路径
UIBezierPath *reversingPath = [path bezierPathByReversingPath];
reversingPath.lineWidth = 3;
// 3. 为了避免两条路径混淆在一起, 我们为第一条路径做一个位移
CGAffineTransform transform = CGAffineTransformMakeTranslation(200, 0);
[path applyTransform: transform];
// 4. 两条路径分别添加一条直接到 self.center
[path addLineToPoint: CGPointMake(self.frame.size.width*0.5, self.frame.size.height*0.5)];
[reversingPath addLineToPoint: CGPointMake(self.frame.size.width*0.5, self.frame.size.height*0.5)];
// 5. 设置颜色, 并绘制路径
[[UIColor redColor] set];
[path stroke];
[[UIColor greenColor] set];
[reversingPath stroke];
}
效果:
从图中我们可以清晰的看到, 路径反转以后, 原路径的beginPoint
现在变成了endPoint
。
usesEvenOddFillRule
这个属性是一个布尔值, 意思是: 是否使用奇偶填充规则
, 该属性默认为 No, 默认的填充规则是 非零环绕数原则
.
虚线
设置路径的描边模式。
- (void)setLineDash:(nullable const CGFloat *)pattern count:(NSInteger)count phase:(CGFloat)phase;
pattern
C样式的浮点值数组,包含线段和模式中的间隙的长度(以点为单位)。 数组中的值交替,从第一个线段长度开始,后跟第一个间隙长度,后跟第二个线段长度,依此类推。
count
模式中的值的数量,即虚线数组元素个数。
phase
虚线开始的位置:开始绘制图案的偏移量,沿着虚线图案的点测量。
- (void) typeDashLine {
UIBezierPath *path = [UIBezierPath bezierPath];
if (step.paintLineType == PaintLineType1) {
CGFloat dashLineConfig[] = {4.0,2.0};
[path setLineDash:dashLineConfig count:2 phase:0];
}else if (step.paintLineType == PaintLineType2){
CGFloat dashLineConfig[] = {4.0, 2.0, 8.0, 2.0,16.0,2.0};
[path setLineDash:dashLineConfig count:6 phase:0];
}else if (step.paintLineType == PaintLineType3){
CGFloat dashLineConfig[] = {1.0,1.0};
[path setLineDash:dashLineConfig count:2 phase:0];
}
path.lineWidth = strokeWidth;
[path moveToPoint:startPoint];
[path addLineToPoint:endPoint];
[path stroke];
}
效果:
Quartz2D总结:
-
为什么要实现drawRect:方法?
因为只有在drawRect:方法中才能获取到上下文
首先获取图形上下文,然后描述路径,把路径添加到上下文,渲染到视图,图形上下文相当于一个内存缓存区,在内存里面操作是最快的,比直接在界面操作快多了。