Swift4中如何使用CAShapeLayer和UIBezierPath绘制曲线?
How to draw a curved line using CAShapeLayer and BezierPath in Swift 4?
我想知道如何在 Swift 4 中使用 CAShapeLayer 和 BezierPath 在给定两个点(点 A 和点 B)的情况下渲染如下图所示的曲线?
func drawCurvedLine(start: CGPoint, end: CGPoint) {
//insert code here
}
您需要应用一些数学知识。在我看来,它由 2 个不同半径的弧组成。从 2 个点计算这些可能非常具有挑战性,但幸运的是我们有 arc 工具已经为我们做了这件事。 UIBezierPath
上的方法 addQuadCurve
似乎很适合这个。
我们需要输入 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()
}
现在负的vend factor意味着向下弯曲,其余的应该很直观。
编辑 通过将 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 个因素可以很好地控制形状,您可以使用它们(我添加了 2 个值为 [0.0, 2.0]
的滑块来设置样式)。另外我在其中留下了调试部分,这在定位控制点时非常有用。
最好也有圆角,但从目前的代码来看,我不确定我是否能够实现。
我想知道如何在 Swift 4 中使用 CAShapeLayer 和 BezierPath 在给定两个点(点 A 和点 B)的情况下渲染如下图所示的曲线?
func drawCurvedLine(start: CGPoint, end: CGPoint) {
//insert code here
}
您需要应用一些数学知识。在我看来,它由 2 个不同半径的弧组成。从 2 个点计算这些可能非常具有挑战性,但幸运的是我们有 arc 工具已经为我们做了这件事。 UIBezierPath
上的方法 addQuadCurve
似乎很适合这个。
我们需要输入 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()
}
现在负的vend factor意味着向下弯曲,其余的应该很直观。
编辑 通过将 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 个因素可以很好地控制形状,您可以使用它们(我添加了 2 个值为 [0.0, 2.0]
的滑块来设置样式)。另外我在其中留下了调试部分,这在定位控制点时非常有用。
最好也有圆角,但从目前的代码来看,我不确定我是否能够实现。