如何在Swift 4中使用CAShapeLayer和BezierPath绘制曲线?

时间:2018-05-25 11:09:12

标签: ios core-graphics swift4 uibezierpath cashapelayer

我想知道如何在Swift 4中使用CAShapeLayer和BezierPath给出两个点(A点和B点),如下图所示渲染曲线?

func drawCurvedLine(start: CGPoint, end: CGPoint) {
   //insert code here
}

enter image description here

1 个答案:

答案 0 :(得分:2)

你需要应用一些数学。我看到它的方式由2个不同半径的弧组成。从2点计算这些可能是非常具有挑战性的,但幸运的是我们已经为我们提供了弧形工具。 addQuadCurve上的方法UIBezierPath似乎是完美的。

我们需要输入2个点,这是一个关于弧的束缚程度和一些线厚度的因素。我们使用弯曲因子来确定控制点从您的情况中的两点的中心向下移动了多少。向下可以相对于2点,所以让我们只使用normal。我得到的是以下内容:

func generateSpecialCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat) -> UIBezierPath {

    let center = CGPoint(x: (from.x+to.x)*0.5, y: (from.y+to.y)*0.5)
    let normal = CGPoint(x: -(from.y-to.y), y: (from.x-to.x))
    let normalNormalized: CGPoint = {
        let normalSize = sqrt(normal.x*normal.x + normal.y*normal.y)
        guard normalSize > 0.0 else { return .zero }
        return CGPoint(x: normal.x/normalSize, y: normal.y/normalSize)
    }()

    let path = UIBezierPath()

    path.move(to: from)

    let midControlPoint: CGPoint = CGPoint(x: center.x + normal.x*bendFactor, y: center.y + normal.y*bendFactor)
    let closeControlPoint: CGPoint = CGPoint(x: midControlPoint.x + normalNormalized.x*thickness*0.5, y: midControlPoint.y + normalNormalized.y*thickness*0.5)
    let farControlPoint: CGPoint = CGPoint(x: midControlPoint.x - normalNormalized.x*thickness*0.5, y: midControlPoint.y - normalNormalized.y*thickness*0.5)


    path.addQuadCurve(to: to, controlPoint: closeControlPoint)
    path.addQuadCurve(to: from, controlPoint: farControlPoint)
    path.close()
    return path
}

我用来获得与您的请求类似的形状的数据是(覆盖drawRect是最快的):

override func draw(_ rect: CGRect) {
    super.draw(rect)

    let from: CGPoint = CGPoint(x: 100.0, y: 300.0)
    let to: CGPoint = CGPoint(x: 200.0, y: 300.0)

    UIColor.blue.setFill()
    generateSpecialCurve(from: from, to: to, bendFactor: -0.25, thickness: 10.0).fill()
}

现在负面因素意味着向下弯曲,其余部分应该非常直观。

编辑通过组合4条曲线中的形状,可以实现更多控制:

使用4条曲线可以创建更好的形状,但代码可能会很复杂。这就是我试图使形状更接近你想要的形状:

func generateSpecialCurve(from: CGPoint, to: CGPoint, bendFactor: CGFloat, thickness: CGFloat, showDebug: Bool = false) -> UIBezierPath {

    var specialCurveScale: CGFloat = 0.2 // A factor to control sides
    var midControlPointsScale: CGFloat = 0.3 // A factor to cotnrol mid

    let center = from.adding(to).scaled(by: 0.5)
    let direction = from.direction(toward: to)
    let directionNormalized = direction.normalized
    let normal = direction.normal
    let normalNormalized = normal.normalized

    let middlePoints: (near: CGPoint, far: CGPoint) = {
        let middlePoint = center.adding(normal.scaled(by: bendFactor))
        return (middlePoint.subtracting(normalNormalized.scaled(by: thickness*0.5)), middlePoint.adding(normalNormalized.scaled(by: thickness*0.5)))
    }()

    let borderControlPoints: (start: CGPoint, end: CGPoint) = {
        let borderTangentScale: CGFloat = 1.0
        let normalDirectionFactor: CGFloat = bendFactor < 0.0 ? -1.0 : 1.0

        let startTangent = normal.scaled(by: normalDirectionFactor).adding(direction.scaled(by: specialCurveScale)).scaled(by: normalDirectionFactor)
        let endTangent = normal.scaled(by: normalDirectionFactor).subtracting(direction.scaled(by: specialCurveScale)).scaled(by: normalDirectionFactor)

        return (from.adding(startTangent.scaled(by: bendFactor)), to.adding(endTangent.scaled(by: bendFactor)))
    }()


    let farMidControlPoints: (start: CGPoint, end: CGPoint) = {
        let normalDirectionFactor: CGFloat = bendFactor < 0.0 ? -1.0 : 1.0
        return (start: middlePoints.far.adding(direction.scaled(by: bendFactor*normalDirectionFactor*midControlPointsScale)),
                end: middlePoints.far.adding(direction.scaled(by: -bendFactor*normalDirectionFactor*midControlPointsScale)))
    }()
    let nearMidControlPoints: (start: CGPoint, end: CGPoint) = {
        let normalDirectionFactor: CGFloat = bendFactor < 0.0 ? -1.0 : 1.0
        return (start: middlePoints.near.adding(direction.scaled(by: -bendFactor*normalDirectionFactor*midControlPointsScale)),
                end: middlePoints.near.adding(direction.scaled(by: bendFactor*normalDirectionFactor*midControlPointsScale)))
    }()

    if showDebug {
        func line(_ a: CGPoint, _ b: CGPoint) -> UIBezierPath {
            let path = UIBezierPath()
            path.move(to: a)
            path.addLine(to: b)
            path.lineWidth = 1
            return path
        }

        let debugAlpha: CGFloat = 0.3

        UIColor.green.withAlphaComponent(debugAlpha).setFill()
        UIColor.green.withAlphaComponent(debugAlpha).setStroke()
        line(from, borderControlPoints.start).stroke()
        line(to, borderControlPoints.end).stroke()
        UIBezierPath(arcCenter: borderControlPoints.start, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()
        UIBezierPath(arcCenter: borderControlPoints.end, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()



        UIColor.red.withAlphaComponent(debugAlpha).setFill()
        UIColor.red.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.near, nearMidControlPoints.start).stroke()
        UIBezierPath(arcCenter: nearMidControlPoints.start, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()

        UIColor.cyan.withAlphaComponent(debugAlpha).setFill()
        UIColor.cyan.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.far, farMidControlPoints.start).stroke()
        UIBezierPath(arcCenter: farMidControlPoints.start, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()

        UIColor.yellow.withAlphaComponent(debugAlpha).setFill()
        UIColor.yellow.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.near, nearMidControlPoints.end).stroke()
        UIBezierPath(arcCenter: nearMidControlPoints.end, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()

        UIColor.purple.withAlphaComponent(debugAlpha).setFill()
        UIColor.purple.withAlphaComponent(debugAlpha).setStroke()
        line(middlePoints.far, farMidControlPoints.end).stroke()
        UIBezierPath(arcCenter: farMidControlPoints.end, radius: 3.0, startAngle: 0.0, endAngle: .pi*2.0, clockwise: true).fill()
    }


    let path = UIBezierPath()

    path.move(to: from)
    path.addCurve(to: middlePoints.near,
                  controlPoint1: borderControlPoints.start,
                  controlPoint2: nearMidControlPoints.start)
    path.addCurve(to: to,
                  controlPoint1: nearMidControlPoints.end,
                  controlPoint2: borderControlPoints.end)

    path.addCurve(to: middlePoints.far,
                  controlPoint1: borderControlPoints.end,
                  controlPoint2: farMidControlPoints.start)
    path.addCurve(to: from,
                  controlPoint1: farMidControlPoints.end,
                  controlPoint2: borderControlPoints.start)

    path.close()
    return path
}

为了便于阅读,我稍微扩展了CGPoint。其中一些方法一般没有意义,所以我不会将它暴露出来fileprivate

fileprivate extension CGPoint {

    var length: CGFloat { return sqrt(x*x + y*y) }
    var normal: CGPoint { return CGPoint(x: y, y: -x) }
    func scaled(by factor: CGFloat) -> CGPoint { return CGPoint(x: x*factor, y: y*factor) }
    func adding(_ point: CGPoint) -> CGPoint { return CGPoint(x: x+point.x, y: y+point.y) }
    func subtracting(_ point: CGPoint) -> CGPoint { return CGPoint(x: x-point.x, y: y-point.y) }
    func direction(toward point: CGPoint) -> CGPoint { return point.subtracting(self) }
    var normalized: CGPoint {
        let distance = length
        return distance > 0.0 ? scaled(by: 1.0/distance) : .zero
    }

}

在方法开始时有2个因素可以很好地控制形状,你可以使用它们(我添加了两个值为[0.0, 2.0]的滑块来设置它的样式)。我还在其中留下了调试部分,这在定位控制点时非常有用。

同样有圆角会很好,但从目前的代码来看,我不确定我是否能够实现这一目标。