首页 > 解决方案 > 在 315° 圆弧上绘制圆角

问题描述

我正在绘制一条 270° 的弧,两端都有圆角。这工作正常,但是现在我想将我的拱门更改为 315°(-45°),但是我的角计算将不起作用。

我试图以不同的方式计算这种不同的方法,但似乎无法找到用于在起点和终点不垂直或水平时向我的弧添加圆角的通用函数的公式。

这是我的游乐场代码:

import UIKit
import PlaygroundSupport

class ArcView: UIView {

  private var strokeWidth: CGFloat {
    return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
  }
  private let cornerRadius: CGFloat = 10

  override open func draw(_ rect: CGRect) {
    super.draw(rect)
    backgroundColor = UIColor.white

    drawNormalCircle()
  }

  func drawNormalCircle() {
    let strokeWidth = CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
    let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
    let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0

    var endAngle: CGFloat = 270.0
    let bezierPath = UIBezierPath(arcCenter: self.center, radius: outerRadius, startAngle: 0, endAngle: endAngle * .pi / 180, clockwise: true)

    var point = bezierPath.currentPoint
    point.y += cornerRadius
    let arc = UIBezierPath(arcCenter: point, radius: cornerRadius, startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)
    arc.apply(CGAffineTransform(rotationAngle: (360 - endAngle) * .pi / 180))

    var firstCenter = bezierPath.currentPoint
    firstCenter.y += cornerRadius

    bezierPath.addArc(withCenter: firstCenter, radius: cornerRadius , startAngle: 270 * .pi / 180 , endAngle: 0, clockwise: true)
    bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x, y: strokeWidth - cornerRadius))

    var secondCenter = bezierPath.currentPoint
    secondCenter.x -= cornerRadius
    bezierPath.addArc(withCenter: secondCenter, radius: cornerRadius , startAngle: 0, endAngle: 90 * .pi / 180, clockwise: true)
    bezierPath.addArc(withCenter: self.center, radius: innerRadius, startAngle: 270 * .pi / 180, endAngle: 0, clockwise: false)

    var thirdCenter = bezierPath.currentPoint
    thirdCenter.x += cornerRadius
    bezierPath.addArc(withCenter: thirdCenter, radius: cornerRadius , startAngle: 180 * .pi / 180, endAngle: 270 * .pi / 180, clockwise: true)

    bezierPath.addLine(to: CGPoint(x: bezierPath.currentPoint.x + strokeWidth - (cornerRadius * 2), y: bezierPath.currentPoint.y))

    var fourthCenter = bezierPath.currentPoint
    fourthCenter.y += cornerRadius
    bezierPath.addArc(withCenter: fourthCenter, radius: cornerRadius , startAngle: 270 * .pi / 180, endAngle: 0, clockwise: true)

    bezierPath.close()

    let backgroundLayer = CAShapeLayer()
    backgroundLayer.path = bezierPath.cgPath
    backgroundLayer.strokeColor = UIColor.red.cgColor
    backgroundLayer.lineWidth = 2
    backgroundLayer.fillColor = UIColor.lightGray.cgColor

    self.layer.addSublayer(backgroundLayer)
  }
}

let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 400, height: 400))
PlaygroundPage.current.liveView = arcView

对我来说,问题是当拐角不是给定的 X - CornerRadius 或 Y + 拐角半径时,如何计算拐角的圆弧中心,在完全水平或垂直的情况下。当圆弧为 315° 时,我怎么能有圆角。

标签: swiftcore-graphicsuibezierpath

解决方案


前言:通常当我对一个问题的回答是“做一些完全不同的事情”时,我的目标是按原样解决最初的问题,然后另外提出更好的方法。然而,这对这个来说是不可行的,因为这段代码的复杂性,如果要以相同的风格进行扩展,会变得如此巨大,以至于不值得。

这里的问题基本上是代码组织的问题。许多表达式通过复制粘贴重复。将它们提取到变量中不仅可以为编辑提供中心位置,还可以为表达式命名,从而极大地提高可读性。

这段代码会很长。但这没关系,因为它会很简单。拥有一堆简单的东西几乎总是胜过少量复杂的东西。您也许可以编写一些疯狂的三角代码来完美地展示您的贝塞尔曲线,但很可能,您不会第一次就正确。调试会很困难。这将是完全陌生的,对任何不是你的人来说都更加困难……包括未来的你。未来你会奋斗。

在我们开始之前,这里有一个粗略的图表,可以帮助您了解这篇文章的其余部分:

概览图

可视化开发环境

首先,我们需要建立一种可视化结果的好方法。

Playground 可以快速重新加载预览,这是一个优点。但是使用封闭路径进行调试很困难,因为很难将贝塞尔路径的各个部分区分开来,而且通常对于凸形,路径会以某种方式封闭自身,从而掩盖您正在处理的路径部分. 所以我要处理的代码的第一部分是一个抽象层UIBezierPath

为了解决这个问题,我将通过用不同的颜色抚摸每个部分来发展形状。不幸的是,你不能将 a 的一个小节UIBezierPath与其他部分分开,所以要实现这一点,我们的形状需要由多个UIBezierPaths 组成,随着我们的进行抚摸每一个。但这在性能敏感的环境中可能会很慢,所以理想情况下我们只想在开发过程中这样做。我希望能够在两种不同的方式中选择一种来做同样的事情。协议是完美的,所以让我们从那里开始。

我将从BezierPathBuilder. 它所做的只是允许我将BezierPathRenderable部分附加到它(我稍后会谈到),并构建一条最终路径,我可以将其交给我的CALayer或其他任何东西。

protocol BezierPathBuilder: AnyObject {
    func append(_: BezierPathRenderable)
    func build() -> UIBezierPath
}

这个协议的主要实现非常简单,它只是包装了一个UIBezierPath. 当renderable告诉它自己渲染时,它会简单地在我们给它的路径上操作,而不需要分配任何中间路径。

class BezierPathBuilderImpl: BezierPathBuilder {
    let path = UIBezierPath()

    func append(_ renderable: BezierPathRenderable) {
        renderable.render(into: self, self.path)
    }
    func build() -> UIBezierPath {
        return path
    }
}

调试实现更有趣一些。附加 arenderable时,我们不会让它直接将自己绘制到我们的主路径中。相反,我们创建了一个新的临时路径供它使用,它会在其中绘制自己。然后我们有机会描边那条路径(每次都用不同的颜色)。完成后,我们可以将该临时路径附加到主路径,然后继续。

class DebugBezierPathBuilder: BezierPathBuilder {

    var rainbowIterator = ([
        .red, .orange, .yellow, .green, .cyan, .blue, .magenta, .purple ] as Array<UIColor>).makeIterator()

    let path = UIBezierPath()

    func append(_ renderable: BezierPathRenderable) {
        let newPathSegment = UIBezierPath()
        renderable.render(into: self, newPathSegment)

        // This will crash if you use too many colours, but it suffices for now.
        rainbowIterator.next()!.setStroke()
        newPathSegment.lineWidth = 20
        newPathSegment.stroke()

        path.append(newPathSegment)
    }

    func build() -> UIBezierPath {
        return path
    }
}

使我们的几何对象化

在您的代码中,几何计算和绘图之间没有分离。因此,您不能轻松地定义一个组件引用另一个组件,因为您没有办法“捞出”您在 UIBezierPath 或其他任何东西中绘制的最后一条弧线。所以让我们纠正它。

首先,我将定义一个协议 ,BezierPathRenderable我们的程序将使用它来定义实体可渲染为BezierPath.

protocol BezierPathRenderable {
    func render(into builder: BezierPathBuilder, _ path: UIBezierPath)
}

这个设计不是我最喜欢的,但它是我能想到的最好的。这里的两个参数允许符合类型将自己直接绘制到,path或调用append. builder后者对于由更简单的成分组成的聚合形状很有用(听起来很熟悉?)

以愿望为导向的发展

我最喜欢的代码编写过程包括在早期搭建一大堆东西,编写代码中我感兴趣的任何部分,然后左右添加存根实现。每一步,我基本上都是在回答“我希望我现在拥有什么 API?”的问题,然后我将它存根,并假装它存在。

存根主要形状

我将从主要结构开始。我们想要一个对象来模拟一个圆角的环形扇区。这个对象必须做两件事:

  1. 存储参数化形状的所有值
  2. 定义一种渲染形状的方法,通过符合BezierPathRenderable

所以让我们从那个开始:

struct RoundedAnnulusSector: BezierPathRenderable {
    let center: CGPoint
    var innerRadius: CGFloat
    var outerRadius: CGFloat
    var startAngle: Angle
    var endAngle: Angle
    var cornerRadius: CGFloat

    func render(into builder: BezierPathBuilder, _ path: BezierPath) {
        /// ???
    }

准备一些渲染代码

让我们编写一些脚手架代码来使用我们的新渲染系统绘制它。现在我将使用我们的调试笔画进行渲染,所以我将注释掉这些CAShapeLayer东西:

import UIKit
import PlaygroundSupport

class ArcView: UIView {
    private var strokeWidth: CGFloat {
        return CGFloat(min(self.bounds.width, self.bounds.height) * 0.25)
    }

    override open func draw(_ rect: CGRect) {
        super.draw(rect)
        self.backgroundColor = UIColor.white

        let innerRadius = (min(self.bounds.width, self.bounds.height) - strokeWidth*2) / 2.0
        let outerRadius = (min(self.bounds.width, self.bounds.height)) / 2.0

        let shape = RoundedAnnulusSector(
            center: self.center,
            innerRadius: innerRadius - 50,
            outerRadius: outerRadius - 50,
            startAngle: (45 * .pi) / 180,
            endAngle: (315 * .pi) / 180,
            cornerRadius: 25
        )
        let builder = DebugBezierPathBuilder()
        builder.append(shape)
        let path = builder.build()

        let backgroundLayer = CAShapeLayer()
//      backgroundLayer.path = path.cgPath
//      backgroundLayer.strokeColor = UIColor.red.cgColor
//      backgroundLayer.lineWidth = 2
//      backgroundLayer.fillColor = UIColor.lightGray.cgColor

        self.layer.addSublayer(backgroundLayer)
    }
}

let arcView = ArcView(frame: CGRect(x: 0, y: 0, width: 800, height: 800))
PlaygroundPage.current.liveView = arcView

这显然没有任何作用,因为我们还没有实现RoundedAnnulusSector.render(into:_:).

挖角

我们可以注意到,这个形状的整个绘图都取决于它的 4 个角的细节。如果我们的形状有四个角,我们为什么不直接说呢?

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    private var corner1: RoundedAnnulusSectorCorner { ??? }
    private var corner2: RoundedAnnulusSectorCorner { ??? }
    private var corner3: RoundedAnnulusSectorCorner { ??? }
    private var corner4: RoundedAnnulusSectorCorner { ??? }
}

在写这篇文章时,我希望存在一个名为 的结构RoundedAnnulusSectorCorner,它可以做两件事:

  1. 存储参数化形状的所有值
  2. 定义一种渲染形状的方法,通过符合BezierPathRenderable

请注意,它们是相同的两个角色RoundedAnnulusSector。这些东西是故意简单的,并且是可组合的。

现在我们可以存根RoundedAnnulusSectorCorner

struct RoundedAnnulusSectorCorner {}

...并填写我们的计算属性以返回默认实例。接下来我们要定义形状的内弧和外弧。

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    private var corner1: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
    private var corner2: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
    private var corner3: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
    private var corner4: RoundedAnnulusSectorCorner { return RoundedAnnulusSectorCorner() }
    private var outerArc: Arc { ??? }
    private var innerArc: Arc { ??? }
}

实施Arc

同样,Arc它只是另一种形状,它将履行与其他人相同的两个角色。根据我们对弧形 API 的熟悉程度UIBezierPath,我们将知道弧形需要中心、半径、开始/结束角度以及顺时针或逆时针绘制它们的指示符。所以我们可以填写:

struct Arc: BezierPathRenderable {
    let center: CGPoint
    let radius: CGFloat
    let startAngle: CGFloat
    let endAngle: CGFloat
    let clockwise: Bool

    func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
        path.addArc(withCenter: center, radius:  radius,
                startAngle: startAngle, endAngle: endAngle, clockwise: clockwise)
    }
}

innerArc/的第一个近似值outerArc

现在我们需要确定初始化弧的参数。我们将从没有圆角开始,所以我们将直接使用我们的startAngle/endAngle和我们的innerRadius/ outerRadius

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    private var outerArc: Arc {
        return Arc(
            center: self.center,
            radius: self.outerRadius,
            startAngle: self.startAngle,
            endAngle: self.endAngle,
            clockwise: true
        )
    }

    private var innerArc: Arc {
        return Arc(
            center: self.center,
            radius: self.innerRadius,
            startAngle: self.endAngle,
            endAngle: self.startAngle,
            clockwise: false
        )
    }
}

初始渲染

完成这两部分后,我们可以开始绘制,看看它到目前为止的样子,通过做一个初始实现RoundedAnnulusSector.render(into:_:)

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    func render(into builder: BezierPathBuilder, _ path: BezierPath) {
        let components: [BezierPathRenderable] = [
            self.outerArc,
            self.innerArc,
        ]
        builder.append(contentsOf: components)
    }

}

extension BezierPathBuilder {
    func append<S: Sequence>(contentsOf renderables: S) where S.Element == BezierPathRenderable {
        for renderable in renderables {
            self.append(renderable)
        }
    }
}

随着我们的进步,我们可以将更多BezierPathRenderable组件添加到此列表中。我看到了这一点,所以我做了它BezierPathBuilder来处理序列,所以我们可以给它一个数组并让它自动附加其中的所有元素。

存根startAngleEdgeendAngleEdge

这个形状需要两条直线。第一个将角 4 与角 1 连接(这将是一条从中心沿 向外的径向线startAngle),第二个将角 2 与角 3 连接起来(这将是一条从中心向外沿 的径向线endAngle。让我们把那些在:

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    func render(into builder: BezierPathBuilder, _ path: BezierPath) {
        let components: [BezierPathRenderable] = [
            self.outerArc,
            self.endAngleEdge,
            self.innerArc,
            self.startAngleEdge,
        ]
        builder.append(contentsOf: components)
    }

    // ...


    private var endAngleEdge: Line {
        return Line()
    }

    private var startAngleEdge: Line {
        return Line()
    }
}

实施Line

我们可以 stub out Line,但我们知道一条线只是连接两个点。太简单了,我们干脆完成吧:

struct Line: BezierPathRenderable {
    let start: CGPoint
    let end: CGPoint

    func render(into builder: BezierPathBuilder, _ path: BezierPath) {
        path.move(to: self.start)
        path.addLine(to: self.end)
    }
}

实施startAngleEdge/endAngleEdge

现在我们需要弄清楚我们的两条线的起点/终点是什么。RoundedAnnulusSectorCorner如果我们拥有startPoint: CGPointendPoint: CGPoint属性,那将非常方便。

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    private var endAngleEdge: Line {
        return Line(
            start: self.corner2.endPoint,
              end: self.corner3.startPoint)
    }

    private var startAngleEdge: Line {
        return Line(
            start: self.corner4.endPoint,
              end: self.corner1.startPoint)
    }
}

startAngleEdge/的第一个近似值endAngleEdge

让我们实现我们的愿望

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...
    var startPoint: CGPoint { return .zero }
    var   endPoint: CGPoint { return .zero }
}

因为这些都实现为CGPoint.zero,所以我们的边都不会绘制。

startAngleEdge/的第二个近似值endAngleEdge

所以让我们实现一些更好的近似startPoint/ endPoint。假设我们的观点有一个rawCornerPoint: CGPoint。如果没有圆角(即圆角半径 = 0),这将是角的位置。在不四舍五入的世界中,我们的startPoint/endPoint都将是rawCornerPoint. 让我们存根并使用它:

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...

    var rawCornerPoint: CGPoint { return .zero }
    var startPoint: CGPoint { return self.rawCornerPoint }
    var   endPoint: CGPoint { return self.rawCornerPoint }
}

实施rawCornerPoint

现在,我们需要得出它的真正价值。rawCornerPoint取决于两件事:

  1. 父形状的中心,
  2. 角的位置,相对于父形状的中心。这本身依赖于:
    1. 到父图形中心的距离
    2. 相对于父图形中心的角度

这些东西中的每一个都是我们父形状的参数,因此这些属性实际上将被存储(并由父形状初始化)。然后我们可以使用它们来计算偏移量,并将该偏移量添加到parentCenter.

struct RoundedAnnulusSectorCorner {
    let parentCenter: CGPoint
    let distanceToParentCenter: CGFloat
    let angleToParentCenter: CGFloat

    var rawCornerPoint: CGPoint {
        let inset = CGPoint(
            radius: self.distanceToParentCenter,
            angle: self.angleToParentCenter
        )

        return self.parentCenter + inset
    }

    // ...
}

显然,一个CGPoint从极坐标初始化它的初始化器会很棒。

另外,写作.applying(CGAffineTransform(translationX: deltaX, y: deltaY)很烦人,如果有一个+操作员就好了。

实现一些CGPoint实用程序

让我们实现更多的愿望:

// Follows UIBezierPath convention on angles.
// 0 is "right" at 3 o'clock, and angle increase clockwise.
extension CGPoint {
    init(radius: CGFloat, angle: CGFloat) {
        self.init(x: radius * cos(angle), y: radius * sin(angle))
    }


    static func + (l: CGPoint, r: CGPoint) -> CGPoint {
        return CGPoint(x: l.x + r.x, y: l.y + r.y)
    }

    static func - (l: CGPoint, r: CGPoint) -> CGPoint {
        return CGPoint(x: l.x - r.x, y: l.y - r.y)
    }
}

填充角落的字段

现在我们的角实际上已经存储了属性,我们可以回到我们的RoundedAnnulusSector并填充它们。

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...
    private var corner1: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            distanceToParentCenter: self.outerRadius,
            angleToParentCenter: self.startAngle
        )
    }

    private var corner2: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            distanceToParentCenter: self.outerRadius,
            angleToParentCenter: self.endAngle
        )
    }

    private var corner3: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            distanceToParentCenter: self.innerRadius,
            angleToParentCenter: self.endAngle
        )
    }

    private var corner4: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            distanceToParentCenter: self.innerRadius,
            angleToParentCenter: self.startAngle

        )
    }
// ...
}

第二次渲染

我们的主要形状的渲染列表已经包含了我们的线条,但是现在我们已经实现了它们的近似值,我们可以实际测试它们。如果到目前为止我的解释正确,那么此时应该有一个封闭的形状,带有尖角。成功!(我希望)

四舍五入

听起来我们差不多完成了,但没办法哈哈,这就是好东西开始的地方。

首先,我们应该添加所有带有圆角的东西。我们知道我们的圆角将是弧线,幸运的是,我们已经实现了这些!

弧需要一个开始和结束的角度,所以我们也需要这些。我们可以用0和对它们进行存根,这样我们就不必担心圆角弧的方向。现在,它们只是完整的圆圈。

struct RoundedAnnulusSectorCorner {
    // ...
    let radius: CGFloat

    var arc: Arc {
        return Arc(center: rawCornerPoint, radius: radius, startAngle: startAngle, endAngle: endAngle, clockwise: true)
    }

    /// The angle at which this corner's arc starts.
    var startAngle: CGFloat { return 0 }

    /// The angle at which this corner's arc ends.
    var endAngle: CGFloat { return 2 * .pi }
}

第三次渲染

现在我们的角已经有了弧线,我们可以将这些弧线添加到渲染列表中进行绘制:

struct RoundedAnnulusSector: BezierPathRenderable {
    let center: CGPoint
    let innerRadius: CGFloat
    let outerRadius: CGFloat
    let startAngle: CGFloat
    let endAngle: CGFloat
    let cornerRadius: CGFloat

    func render(into builder: BezierPathBuilder, _ path: UIBezierPath) {
        let components: [BezierPathRenderable] = [
            self.corner1.arc,
            self.outerArc,
            self.corner2.arc,
            self.endAngleEdge,
            self.corner3.arc,
            self.innerArc,
            self.corner4.arc,
            self.startAngleEdge,
        ]
        builder.append(contentsOf: components)
    }
}

看哪,圆圈!

但是,哦,它们以rawCornerPoint. 我想这应该不足为奇,因为这就是我们定义我们的Arc.

插入中心

但要做到这一点,我们需要插入弧的中心。我们称它为center. 中心需要插入,使其更靠近 的“内部” RoundedAnnulusSector,以便在添加圆角半径后,笔划与形状的其余部分对齐。

此插入由两个部分组成: 1. 中心需要相对于 旋转parentCenter一个角度(称为rotationalInsetAngle),使其位于内圆弧或外圆弧上,使其圆弧旋转延伸以达到半径

这是一张照片供参考:

转角细节图

  1. 蓝色圆圈是我们角落的当前圆圈,以 为中心rawCornerPoint
  2. 标记的小箭头1是插图的旋转分量。
  3. 粉红色的圆圈是我们角落的圆圈,在旋转插图之后。
  4. 标记的小箭头2是插图的径向平移分量
  5. 绿色圆圈是我们想要的圆角,以 为中心center
  6. 标记的虚线箭头是和offset之间的偏移,作为旋转和径向平移分量的组合获得。rawCornerPointcenter
struct RoundedAnnulusSectorCorner {
    // ...
    /// The center of this rounded corner's arc
    ///
    /// ...after insetting from the `rawCornerPoint`, so that this rounded corner's arc
    /// aligns perfectly with the curves adjacent to it.
    var center: CGPoint {
        return self.rawCornerPoint
            .rotated(around: self.parentCenter, by: self.rotationalInsetAngle)
            .translated(towards: self.edgeAngle, by: self.radialInsetDistance)
    }
}

我们在这里有一个很长的愿望清单:rotationalInsetAngle, radialInsetDistance, CGPoint.rotated(around:by:), CGPoint.translated(towards:, by:)

更多 CGPoint 实用程序

幸运的是,现在我们有了极坐标初始化器,这些都非常容易实现。

extension CGPoint {
    func translated(towards angle: CGFloat, by r: CGFloat) -> CGPoint {
        return self + CGPoint(radius: r, angle: angle)
    }

    func rotated(around pivot: CGPoint, by angle: CGFloat) -> CGPoint {
        return (self - pivot).applying(CGAffineTransform(rotationAngle: angle)) + pivot
    }
}

陷入困境rotationalInsetAngleradialInsetDistance

这是狗屎击中风扇的地方。我们知道我们必须通过将其翻译来插入我们的角落radius。但朝哪个方向?

内部的两个角(#3 和#4)需要从父级径向平移而外部的两个角(#1 和#3)需要向父级径向向内平移。

同样,我们的旋转插图也需要变化。对于起始边上的两个角(#1 和#4),我们需要从顺时针startEdge插入,而结束边上的两个角(#2 和#3)需要从逆时针endEdge插入。

但是到目前为止,我们的数据模型仅根据角度和距离告诉我们的角落它们在哪里。它没有指定他们需要保持相对于定义distanceFromCenter和的哪一侧angleToParentCenter

这将需要一些相当大的重构。

实施RadialPositionRotationalPosition

让我们实现一个名为RadialPosition. 它不仅会捕获径向位置(即形成中心点的距离),还会捕获该距离的哪一侧“停留”。一个包含radialDistance: CGFloatand的结构isInsideOfRadialDistance: Bool会做,但我知道我经常因为不正确的处理条件而产生很多错误。相反,我将使用两种情况的枚举,其中内部/外部的区别更明确,更难错过。因为枚举关联值访问起来很麻烦,所以我将添加一个辅助计算属性 ,distanceFromCenter来隐藏那个烦人的switch语句。

struct RoundedAnnulusSectorCorner {
   // ...
    enum RadialPosition {
        case outside(ofRadius: CGFloat)
        case  inside(ofRadius: CGFloat)

        var distanceFromCenter: CGFloat {
            switch self {
            case .outside(ofRadius: let d), .inside(ofRadius: let d): return d
            }
        }
    }
    // ...
}

接下来,我将做类似的事情RotationalPosition

struct RoundedAnnulusSectorCorner {
   // ...
    enum RotationalPosition {
        case cw(of: CGFloat)
        case ccw(of: CGFloat)

        var edgeAngle: CGFloat {
            switch self {
            case .cw(of: let angle), .ccw(of: let angle): return angle
            }
        }
    }
    // ...
}

现在我必须删除现有的distanceToParentCenter: CGFloatangleToParentCenter: CGFloat属性并用这些新模型替换它们。我们需要将他们的呼叫站点迁移到radialPosition.distanceFromCenterRotationalPosition. edgeAngle。这是最终的一组存储属性RoundedAnnulusSectorCorner

struct RoundedAnnulusSectorCorner {
    let parentCenter: CGPoint
    let radius: CGFloat
    let radialPosition: RadialPosition
    let rotationalPosition: RotationalPosition

    // ...

    /// The location of the corner, if this rounded wasn't rounded.
    private var rawCornerPoint: CGPoint {
        let inset = CGPoint(
            radius: self.radialPosition.distanceFromCenter,
            angle: self.rotationalPosition.edgeAngle
        )

        return self.parentCenter + inset
    }

    // ...
}

我们必须更新我们的角定义以提供这些新数据。这些是角的最终定义集。

struct RoundedAnnulusSector: BezierPathRenderable {
    // ...
    private var corner1: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            radius: self.cornerRadius,
            radialPosition: .inside(ofRadius: self.outerRadius),
            rotationalPosition: .cw(of: self.startAngle)
        )
    }

    private var corner2: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            radius: self.cornerRadius,
            radialPosition: .inside(ofRadius: self.outerRadius),
            rotationalPosition: .ccw(of: self.endAngle)
        )
    }

    private var corner3: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            radius: self.cornerRadius,
            radialPosition: .outside(ofRadius: self.innerRadius),
            rotationalPosition: .ccw(of: self.endAngle)
        )
    }

    private var corner4: RoundedAnnulusSectorCorner {
        return RoundedAnnulusSectorCorner(
            parentCenter: self.center,
            radius: self.cornerRadius,
            radialPosition: .outside(ofRadius: self.innerRadius),
            rotationalPosition: .cw(of: self.startAngle)
        )
    }
    // ...
}

第四次渲染

再次运行这段代码,我们看到现在圆圈仍然以它们为中心rawCornerPoint,但这很好。这意味着我们的重构并没有破坏我们已经工作的功能。如果我们一直都有单元测试,那么这就是它们有用的地方。

待续

我的下一个答案中,因为我刚刚遇到了一个 StackOverflow 错误,这是我以前遇到的两倍:

正文限制为 30000 个字符; 你输入了 31245


推荐阅读