如何扩展UIBezierPath弧的外边缘

时间:2018-12-26 21:49:49

标签: ios swift xcode swift4

我正在制作带有4个UIBezierArcs的Simon Says风格的车轮。 我不能只制作具有不同颜色和白色部分的单个弧,因为我需要能够识别出按下了哪个弧。

但是,当我将圆弧放置在一个圆中时,内部边缘之间的白色空间小于外部边缘之间的白色空间,并使这些空间看起来像楔子而不是均匀的矩形。

如何调整外圆弧边缘,使其开始/结束角度长于内圆弧边缘?

private struct Constants {
    static let width: CGFloat = 115;
    static let height: CGFloat = 230;
}

override func draw(_ rect: CGRect) {
    let center = CGPoint(x: bounds.width / 2, y: bounds.height / 2)

    let radius: CGFloat = bounds.height

    let startAngle: CGFloat = 0 + .pi / 44
    let endAngle: CGFloat = .pi / 2 - .pi / 44

    shapePath = UIBezierPath(arcCenter: center,
                            radius: radius/2 - CGFloat(Constants.width/2),
                            startAngle: startAngle,
                            endAngle: endAngle,
                            clockwise: true)

    shapePath.lineWidth = Constants.width / 2
    color.setStroke()
    shapePath.stroke()
    shapePath.close()
}

这是当前的样子:

what it looks like currently

3 个答案:

答案 0 :(得分:4)

所以你想要这个:

wedges with gaps

让我们在UIBezierPath上写一个扩展名,以创建概述单个楔形的路径。

要热身,首先我们将编写一个函数来创建楔形路径,而在楔形之间不留间隙:

import UIKit
import PlaygroundSupport

// This is useful to remind us that we measure angles in radians, not degrees.
typealias Radians = CGFloat

extension UIBezierPath {

    static func simonWedge(innerRadius: CGFloat, outerRadius: CGFloat, centerAngle: Radians) -> UIBezierPath {
        let innerAngle: Radians = CGFloat.pi / 4
        let outerAngle: Radians = CGFloat.pi / 4
        let path = UIBezierPath()
        path.addArc(withCenter: .zero, radius: innerRadius, startAngle: centerAngle - innerAngle, endAngle: centerAngle + innerAngle, clockwise: true)
        path.addArc(withCenter: .zero, radius: outerRadius, startAngle: centerAngle + outerAngle, endAngle: centerAngle - outerAngle, clockwise: false)
        path.close()
        return path
    }

}

有了这个扩展,我们可以像这样创建楔形:

wedge examples

我们可以在UIView子类中使用此扩展来绘制楔形:

class SimonWedgeView: UIView {
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }

    required init?(coder decoder: NSCoder) {
        super.init(coder: decoder)
        commonInit()
    }

    var centerAngle: Radians = 0 { didSet { setNeedsDisplay() } }
    var color: UIColor = #colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1) { didSet { setNeedsDisplay() } }

    override func draw(_ rect: CGRect) {
        let path = wedgePath()
        color.setFill()
        path.fill()
    }

    private func commonInit() {
        contentMode = .redraw
        backgroundColor = .clear
        isOpaque = false
    }

    private func wedgePath() -> UIBezierPath {
        let bounds = self.bounds
        let outerRadius = min(bounds.size.width, bounds.size.height) / 2
        let innerRadius = outerRadius / 2
        let path = UIBezierPath.simonWedge(innerRadius: innerRadius, outerRadius: outerRadius, centerAngle: centerAngle)
        path.apply(CGAffineTransform(translationX: bounds.midX, y: bounds.midY))
        return path
    }
}

let rootView = UIView(frame: CGRect(x: 0, y: 0, width: 100, height: 100))
rootView.backgroundColor = .white

func addWedgeView(color: UIColor, angle: Radians) {
    let wedgeView = SimonWedgeView(frame: rootView.bounds)
    wedgeView.color = color
    wedgeView.centerAngle = angle
    rootView.addSubview(wedgeView)
}

addWedgeView(color: #colorLiteral(red: 0.8549019694, green: 0.250980407, blue: 0.4784313738, alpha: 1), angle: 0)
addWedgeView(color: #colorLiteral(red: 0.5843137503, green: 0.8235294223, blue: 0.4196078479, alpha: 1), angle: 0.5 * .pi)
addWedgeView(color: #colorLiteral(red: 0.2588235438, green: 0.7568627596, blue: 0.9686274529, alpha: 1), angle: .pi)
addWedgeView(color: #colorLiteral(red: 0.9686274529, green: 0.78039217, blue: 0.3450980484, alpha: 1), angle: 1.5 * .pi)

PlaygroundPage.current.liveView = rootView

结果:

wedge views

所以现在我们要在楔形之间添加间隙。

考虑此图:

arc diagram

在该图中,有一个半径为r的圆(以原点为中心),并且该圆弧的一个对角为θ。当θr为弧度时,弧的长度为θ。 (这个公式θr是为什么我们使用弧度来测量角度!)

在上面的无间隙方法中,θ(作为变量innerAngleouterAngle)为.pi / 4。但是现在我们希望角度小于.pi / 4以形成间隙。我们希望沿着内半径的间隙长度等于沿着外半径的间隙长度。因此,我们有一个预定的间隙长度g,我们需要为此计算合适的θ

gapless arc length = r π / 4
gapful arc length = θ r = r π / 4 - g / 2

(我们使用g / 2是因为每个楔形体的一端有一半的间隙,另一端有一半的间隙。)

θ r = r π / 4 - g / 2
// Solve for θ by dividing both sides by r:
θ = π / 4 - g / (2 r)

现在,我们可以更新扩展名中的公式innerAngleouterAngle,以创建包含间隙的路径:

static func simonWedge(innerRadius: CGFloat, outerRadius: CGFloat, centerAngle: Radians, gap: CGFloat) -> UIBezierPath {
    let innerAngle: Radians = CGFloat.pi / 4 - gap / (2 * innerRadius)
    let outerAngle: Radians = CGFloat.pi / 4 - gap / (2 * outerRadius)
    let path = UIBezierPath()
    path.addArc(withCenter: .zero, radius: innerRadius, startAngle: centerAngle - innerAngle, endAngle: centerAngle + innerAngle, clockwise: true)
    path.addArc(withCenter: .zero, radius: outerRadius, startAngle: centerAngle + outerAngle, endAngle: centerAngle - outerAngle, clockwise: false)
    path.close()
    return path
}

然后,我们更新wedgePath的{​​{1}}方法以计算并将间隙长度传递给SimonWedgeView方法:

simonWidge

我们得到了预期的结果:

wedges with gaps

您可以找到完整的游乐场源代码(适用于带间隙的版本)in this gist

顺便说一句,在使draw方法起作用之后,您可能会想要检测出点击了哪个楔形。为此,您需要覆盖 private func wedgePath() -> UIBezierPath { let bounds = self.bounds let outerRadius = min(bounds.size.width, bounds.size.height) / 2 let innerRadius = outerRadius / 2 let gap = (outerRadius - innerRadius) / 4 let path = UIBezierPath.simonWedge(innerRadius: innerRadius, outerRadius: outerRadius, centerAngle: centerAngle, gap: gap) path.apply(CGAffineTransform(translationX: bounds.midX, y: bounds.midY)) return path } 中的point(inside:with:)方法。我说明了该怎么做in this answer

答案 1 :(得分:2)

您将无法使用4个粗圆弧段。这些将有倾斜的末端。

相反,您必须构建4个单独的填充多边形,每个多边形具有2条弧线和2条线段。

弄清楚它会涉及一些代数2和一些trig。

想象一下您的图形的边界正方形,该正方形具有从左上角到右下角以及从右上角到左下角的给定厚度的条。我们称它们为交叉开关。想象一下,您用方形标记画了这些横杆,因此,横杆始于正方形的每个角,而横杆的角则稍微超出边界正方形。绘制它。

现在在正方形内绘制内外径圆。

每个圆将与每个条的外线相交。您将需要解决钢筋边界线与内,外圆的交点。 如果横杆的粗细为t,则横杆角的坐标将与正方形的角偏移t•√2/4。您需要绘制横条并绘制横条外线的坐标。然后记下每个交叉开关的端点并计算外线方程。 (采用截距截取形式或标准形式。标准形式允许您处理垂直线,但您的案子将没有垂直线。

圆圈的公式为r² = (x-h)² + (y-k)²。您应该能够对每个圆和每个钢筋边界线使用联立方程,以找到每个钢筋外线与每个圆圈的交点。每对应该有2个解决方案。

一旦有了相交的坐标,就必须使用Trig来计算每个圆弧的起点和终点角度。然后,您只需使用UIBezierPath弧命令和直线命令来组装形状。

这是基本方法。确定细节既繁琐又耗时,但很简单。我会将其保留为“读者运动”。

答案 2 :(得分:1)

您可以定义一条路径,该路径外接图的四个弧形。您只需要通过间隙的反正弦除以半径来偏移每条曲线的开始/结束角度。例如

let angleAdjustment = asin(gap / 2 / radius) 

因此:

@IBDesignable
class SimonSaysView: UIView {

    @IBInspectable var innerPercentage: CGFloat = 0.5 { didSet { updatePaths() } }
    @IBInspectable var gap: CGFloat = 20              { didSet { updatePaths() } }

    let shapeLayers: [CAShapeLayer] = {
        let colors: [UIColor] = [.red, .blue, .black, .green]
        return colors.map { color -> CAShapeLayer in
            let shapeLayer = CAShapeLayer()
            shapeLayer.strokeColor = UIColor.clear.cgColor
            shapeLayer.fillColor = color.cgColor
            shapeLayer.lineWidth = 0
            return shapeLayer
        }
    }()

    override init(frame: CGRect = .zero) {
        super.init(frame: frame)
        configure()
    }

    required init?(coder aDecoder: NSCoder) {
        super.init(coder: aDecoder)
        configure()
    }

    override func layoutSubviews() {
        super.layoutSubviews()
        updatePaths()
    }

    func configure() {
        shapeLayers.forEach { layer.addSublayer($0) }
    }

    func updatePaths() {
        let arcCenter = CGPoint(x: bounds.midX, y: bounds.midY)
        let outerRadius = min(bounds.width, bounds.height) / 2
        let innerRadius = outerRadius * innerPercentage
        let outerAdjustment = asin(gap / 2 / outerRadius)
        let innerAdjustment = asin(gap / 2 / innerRadius)

        for (i, shapeLayer) in shapeLayers.enumerated() {
            let startAngle: CGFloat = -3 * .pi / 4 + CGFloat(i) * .pi / 2
            let endAngle = startAngle + .pi / 2
            let path = UIBezierPath(arcCenter: arcCenter, radius: outerRadius, startAngle: startAngle + outerAdjustment, endAngle: endAngle - outerAdjustment, clockwise: true)
            path.addArc(withCenter: arcCenter, radius: innerRadius, startAngle: endAngle - innerAdjustment, endAngle: startAngle + innerAdjustment, clockwise: false)
            path.close()
            shapeLayer.path = path.cgPath
        }
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }

        for (index, shapeLayer) in shapeLayers.enumerated() {
            if shapeLayer.path!.contains(touch.location(in: self)) {
                print(index)
                return
            }
        }
    }
}

收益:

enter image description here


如果您想知道如何计算给定间隙偏移 x 和给定半径r的调整角θ ,基本三角函数告诉我们 sin(θ)= x / r ,因此θ= asin(x / r)其中 x 是总差距除以2。

enter image description here