iOS-Swift知识之 UIBezierPath贝塞尔曲线常用方法详解

6,069 阅读6分钟

背景

iOS开发中,经常会遇到要绘制一些图形,比如矩形,圆形,椭圆,弧或者不规则的多边形,这时我们可以通过重写UIView的draw(_ rect: CGRect)方法,然后通过UIBezierPath来绘制。

UIBezierPath这个类在UIKit中, 是Core Graphics框架关于path的一个封装。

关于iOS绘图原理,可以查看博主的这篇文章 iOS底层原理之 UIView绘制显示原理流程解析以及性能优化

基本属性和方法说明

1 UIColor.red.set()

设置线条颜色,也就是画笔颜色。

2 lineWidth

线宽

3 lineCapStyle

线帽样式,是CGLineCap类型的枚举,定义如下

public enum CGLineCap : Int32, @unchecked Sendable {
    
    /// 指定不绘制端点,即线条结尾处直接结束。
    case butt    = 0

    /// 绘制圆形端点,即线条结尾处绘制一个直径为线条宽度的半圆
    case round   = 1

    /// 绘制方形端点,即线条结尾处绘制半个边长为线条宽度的正方形。
    /// 需要说明的是,这种形状的端点与“butt”形状的端点十分相似,只是采用这种形式的端点的线条略长一点而已
    case square  = 2
}

效果如下,顺序和枚举顺序一一对应

4 lineJoinStyle

线连接样式,即拐角样式,是CGLineJoin类型的枚举,定义如下

public enum CGLineJoin : Int32, @unchecked Sendable {
    
    /// 尖角
    case miter = 0

    /// 圆角
    case round = 1

    /// 缺角
    case bevel = 2
}

效果如下,顺序和枚举顺序一一对应

5 stroke()

stroke 得到的是不被填充的view,即只会绘制曲线路线。

6 fill()

fill 得到的内部被填充的 view,即会将曲线形成的几何图形,填充满画笔颜色。

绘制矩形

通过init(rect: CGRect)方法绘制,代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    
    let path1 = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 200, height: 100))
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.stroke()
        
    let path2 = UIBezierPath(rect: CGRect(x: 100, y: 230, width: 200, height: 200))
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.fill()
}

绘制带圆角的矩形

有两种方式,如果四个角都需要圆角,可以通过init(roundedRect rect: CGRect, cornerRadius: CGFloat)方法绘制。

如果只需要某些圆角,则可以通过init(roundedRect rect: CGRect, byRoundingCorners corners: UIRectCorner, cornerRadii: CGSize)方法绘制。

代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    let path1 = UIBezierPath(roundedRect: CGRect(x: 100, y: 100, width: 200, height: 100), cornerRadius: 10)
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.stroke()
        
    let path2 = UIBezierPath(roundedRect: CGRect(x: 100, y: 230, width: 200, height: 200), byRoundingCorners: [.topLeft, .topRight], cornerRadii: CGSize(width: 10, height: 10))
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.fill()
}

绘制多边形

通过move(to point: CGPoint)和addLine(to point: CGPoint)方法绘制,moveToPoint:这个方法是设置起始点,意味着从这个点开始。addLineToPoint:设置想要创建的多边形经过的点,也就是两线相交的那个点,可以连续创建line,每一个line的起点都是先前的终点,终点就是指定的点,将线段连接起来就是我们想要创建的多边形了。最后第五条线是用path.close()得到的,closePath方法不仅结束一个shape的subpath表述,它也在最后一个点和第一个点之间画一条线段,这是一个便利的方法,我们不需要去画最后一条线了。代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    let path = UIBezierPath()
    path.lineWidth = 5.0
    path.lineCapStyle = .round
    path.lineJoinStyle = .round
    path.move(to: CGPoint(x: 100, y: 300))
    path.addLine(to: CGPoint(x: 200, y: 200))
    path.addLine(to: CGPoint(x: 300, y: 300))
    path.addLine(to: CGPoint(x: 260, y: 400))
    path.addLine(to: CGPoint(x: 140, y: 400))
    path.close()
    path.fill()
}

绘制椭圆

通过init(ovalIn rect: CGRect)方法绘制,如果传入的rect是一个矩形,则得到矩形的内切椭圆,如果传入的rect是一个正方形,则得到正方形的内切圆。代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    
    let path1 = UIBezierPath(ovalIn: CGRect(x: 100, y: 100, width: 200, height: 100))
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.stroke()
        
    let path2 = UIBezierPath(ovalIn: CGRect(x: 100, y: 230, width: 200, height: 200))
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.fill()
}

绘制一段弧线

通过init(arcCenter center: CGPoint, radius: CGFloat, startAngle: CGFloat, endAngle: CGFloat, clockwise: Bool)方法绘制,其中arcCenter指圆心坐标,radius指圆的半径,startAngle指圆弧的起始角度,endAngle指圆弧的结束角度,clockwise指是否顺时针方向绘制。其中圆弧的角度参考系如下

代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    
    let path1 = UIBezierPath(arcCenter: CGPoint(x: 200, y: 250), radius: 100, startAngle: 180 / 180 * .pi, endAngle: 45 / 180 * .pi, clockwise: true)
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.stroke()
        
    let path2 = UIBezierPath(arcCenter: CGPoint(x: 200, y: 450), radius: 100, startAngle: 0, endAngle: 2 * .pi, clockwise: true)
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.fill()
}

绘制二次贝塞尔曲线

通过addQuadCurve(to endPoint: CGPoint, controlPoint: CGPoint)方法绘制,曲线段在当前点开始,在指定的点结束,一个控制点的切线定义,图示如下

代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    
    let path1 = UIBezierPath()
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.move(to: CGPoint(x: 100, y: 200))
    path1.addQuadCurve(to: CGPoint(x: 300, y: 200), controlPoint: CGPoint(x: 100, y: 100))
    path1.stroke()
        
    let path2 = UIBezierPath()
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.move(to: CGPoint(x: 100, y: 250))
    path2.addQuadCurve(to: CGPoint(x: 300, y: 250), controlPoint: CGPoint(x: 250, y: 400))
    path2.fill()
}

绘制三次贝塞尔曲线

通过addCurve(to endPoint: CGPoint, controlPoint1: CGPoint, controlPoint2: CGPoint)方法绘制,曲线段在当前点开始,在指定的点结束,两个控制点的切线定义,图示如下

代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    
    let path1 = UIBezierPath()
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.move(to: CGPoint(x: 100, y: 200))
    path1.addCurve(to: CGPoint(x: 300, y: 200), controlPoint1: CGPoint(x: 150, y: 50), controlPoint2: CGPoint(x: 250, y: 350))
    path1.stroke()
        
    let path2 = UIBezierPath()
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.move(to: CGPoint(x: 100, y: 350))
    path2.addCurve(to: CGPoint(x: 300, y: 350), controlPoint1: CGPoint(x: 150, y: 200), controlPoint2: CGPoint(x: 250, y: 500))
    path2.fill()
}

绘制虚线

通过setLineDash(_ pattern: UnsafePointer?, count: Int, phase: CGFloat)方法绘制。

其中pattern是C样式的浮点值数组,包含线段和模式中的间隙的长度(以点为单位)。 数组中的值交替,从第一个线段长度开始,后跟第一个间隙长度,后跟第二个线段长度,依此类推。

count是模式中的值的数量,即虚线数组元素个数。

phase是虚线开始的位置,即开始绘制图案的偏移量,沿着虚线图案的点测量。 例如,图案5-2-3-2的相位值phase为6将导致绘图在第一个间隙的中间开始。

代码如下

override func draw(_ rect: CGRect) {
    UIColor.red.set()
    
    let path1 = UIBezierPath()
    path1.lineWidth = 5.0
    path1.lineCapStyle = .round
    path1.lineJoinStyle = .round
    path1.move(to: CGPoint(x: 50, y: 200))
    path1.addLine(to: CGPoint(x: 400, y: 200))
    var dashConfig1:[CGFloat] = [10.0, 15.0]
    path1.setLineDash(&dashConfig1, count: dashConfig1.count, phase: 0)
    path1.stroke()
        
    let path2 = UIBezierPath()
    path2.lineWidth = 5.0
    path2.lineCapStyle = .round
    path2.lineJoinStyle = .round
    path2.move(to: CGPoint(x: 50, y: 250))
    path2.addLine(to: CGPoint(x: 400, y: 250))
    var dashConfig2:[CGFloat] = [10.0, 15.0, 15.0, 25.0]
    path2.setLineDash(&dashConfig2, count: dashConfig2.count, phase: 0)
    path2.stroke()
}