使用 UIBezierPath 绘制自定义视图会导致形状不对称

new*_*per 2 iphone uiview ios uibezierpath swift

我正在尝试绘制带有一些“曲线边缘”的 UIView。

它应该是这样的:

在此处输入图片说明

这是我得到的:

在此处输入图片说明

请注意右上角 (TR) 角如何与右下角 (BR) 不对称?BR 角与我想要实现的非常相似,但我无法使 TR 角正确对齐(以一堆不同的开始和结束角度播放)。

这是代码:

struct Constants {
    static let cornerRadius: CGFloat = 15.0 // used for left-top and left-bottom curvature
    static let rightTipWidth: CGFloat = 40.0 // the max. width for the right tip thingy
    static let rightCornerRadius: CGFloat = 10.0 // the radius for the right tip
    static let rightEdgeRadius: CGFloat = 10.0 // the radius for the top right and bottom right curvature
}
    override func draw(_ rect: CGRect) {
    super.draw(rect)

    // Initialize the path.
    let path = UIBezierPath()

    // starting point
    let startingPoint = CGPoint(x: Constants.cornerRadius, y: 0.0)
    path.move(to: startingPoint)

    // create a center point for the arc for the top left corner
    let leftTopCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: Constants.cornerRadius)
    path.addArc(withCenter: leftTopCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 270.degreesToRadians, endAngle: 180.degreesToRadians, clockwise: false)

    // move the path to the bottom left corner
    path.addLine(to: CGPoint(x: 0.0, y: frame.size.height - Constants.cornerRadius))

    // add the arc to bottom left
    let leftBottomCircleCenterPoint = CGPoint(x: Constants.cornerRadius, y: frame.size.height - Constants.cornerRadius)
    path.addArc(withCenter: leftBottomCircleCenterPoint, radius: Constants.cornerRadius, startAngle: 180.degreesToRadians, endAngle: 90.degreesToRadians, clockwise: false)

    // move along the bottom to the right edge - rightTipWidth
    let maxXRightEdge = frame.size.width - Constants.rightTipWidth
    path.addLine(to: CGPoint(x: maxXRightEdge, y: frame.size.height))

    // add a curve at the bottom before tipping up at 45 degrees
    let bottomRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: frame.size.height - Constants.rightEdgeRadius)
    path.addArc(withCenter: bottomRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 90.degreesToRadians, endAngle: 45.degreesToRadians, clockwise: false)

    // figure out the center for the right side curvature
    let rightMidPointY = frame.size.height / 2.0
    let halfRadius = (Constants.rightCornerRadius / 2.0)

    // move up till the mid point corner radius
    path.addLine(to: CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY + halfRadius)))

    // the destination for the curve (end point of the curve)
    let rightEndPoint = CGPoint(x: frame.size.width - Constants.rightCornerRadius, y: (rightMidPointY - halfRadius))

    // figure out the right side tip's control point (See: https://developer.apple.com/documentation/uikit/uibezierpath/1624351-addquadcurve)
    let rightControlPoint = CGPoint(x: frame.size.width - halfRadius, y: rightMidPointY)

    // add the curve for the right side tip
    path.addQuadCurve(to: rightEndPoint, controlPoint: rightControlPoint)

    // move up at 45 degrees
    path.addLine(to: CGPoint(x: maxXRightEdge + Constants.rightEdgeRadius, y: Constants.rightEdgeRadius))

    let topRightEdgeControlPoint = CGPoint(x: maxXRightEdge, y: Constants.rightEdgeRadius)
    path.addArc(withCenter: topRightEdgeControlPoint, radius: Constants.rightEdgeRadius, startAngle: 315.degreesToRadians, endAngle: 270.degreesToRadians, clockwise: false) // straight

    path.close()

    // Specify the fill color and apply it to the path.
    UIColor.orange.setFill()
    path.fill()

    // Specify a border (stroke) color.
    UIColor.orange.setStroke()
    path.stroke()
}

extension BinaryInteger {
    var degreesToRadians: CGFloat { return CGFloat(Int(self)) * .pi / 180 }
}
Run Code Online (Sandbox Code Playgroud)

简单总结一下我的思考过程:

  • 创建一个 bezierPath 并将其移动到起始点
  • 添加 LT(左上)曲线并将线向下移动
  • 沿左边缘移动线并将 LB(左下)曲线和沿底部的移动线添加到右边缘
  • 移动线直到 frame.size.width - Constants.rightTipWidth
  • 添加中心点为x = currentPoint和的圆弧y = height- rightEdgeRadius
  • 将线向上移动直到 y = (height / 2.0) + (Constants.rightCornerRadius / 2.0)
  • 添加 QuadCurve 的终点为 y = (height / 2.0) - (Constants.rightCornerRadius / 2.0)
  • 将线向上移动直到 x = maxXRightEdge + Constants.rightEdgeRadius
  • 添加右上角 (TR) 曲线 ---> 导致非对称曲率

Rob*_*Rob 5

这是另一种演绎:

@IBDesignable
open class PointerView: UIView {

    /// The left-top and left-bottom curvature
    @IBInspectable var cornerRadius: CGFloat      = 15      { didSet { updatePath() } }

    /// The radius for the right tip
    @IBInspectable var rightCornerRadius: CGFloat = 10      { didSet { updatePath() } }

    /// The radius for the top right and bottom right curvature
    @IBInspectable var rightEdgeRadius: CGFloat   = 10      { didSet { updatePath() } }

    /// The fill color
    @IBInspectable var fillColor: UIColor         = .blue   { didSet { shapeLayer.fillColor = fillColor.cgColor } }

    /// The stroke color
    @IBInspectable var strokeColor: UIColor       = .clear  { didSet { shapeLayer.strokeColor = strokeColor.cgColor } }

    /// The angle of the tip
    @IBInspectable var angle: CGFloat             = 90      { didSet { updatePath() } }

    /// The line width
    @IBInspectable var lineWidth: CGFloat         = 0       { didSet { updatePath() } }

    /// The shape layer for the pointer
    private lazy var shapeLayer: CAShapeLayer = {
        let _shapeLayer = CAShapeLayer()
        _shapeLayer.fillColor = fillColor.cgColor
        _shapeLayer.strokeColor = strokeColor.cgColor
        _shapeLayer.lineWidth = lineWidth
        return _shapeLayer
    }()

    public override init(frame: CGRect) {
        super.init(frame: frame)

        configure()
    }

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

        configure()
    }

    private func configure() {
        layer.addSublayer(shapeLayer)
    }

    open override func layoutSubviews() {
        super.layoutSubviews()

        updatePath()
    }

    private func updatePath() {
        let path = UIBezierPath()

        let offset = lineWidth / 2
        let boundingRect = bounds.insetBy(dx: offset, dy: offset)

        let arrowTop = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.minY)
        let arrowRight = CGPoint(x: boundingRect.maxX, y: boundingRect.midY)
        let arrowBottom = CGPoint(x: boundingRect.maxX - boundingRect.height / 2 / tan(angle * .pi / 180 / 2), y: boundingRect.maxY)
        let start = CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.minY)

        // top left
        path.move(to: start)
        path.addQuadCurve(to: CGPoint(x: boundingRect.minX, y: boundingRect.minY + cornerRadius), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.minY))

        // left
        path.addLine(to: CGPoint(x: boundingRect.minX, y: boundingRect.maxY - cornerRadius))

        // lower left
        path.addQuadCurve(to: CGPoint(x: boundingRect.minX + cornerRadius, y: boundingRect.maxY), controlPoint: CGPoint(x: boundingRect.minX, y: boundingRect.maxY))

        // bottom
        path.addLine(to: calculate(from: path.currentPoint, to: arrowBottom, less: rightEdgeRadius))

        // bottom right (before tip)
        path.addQuadCurve(to: calculate(from: arrowRight, to: arrowBottom, less: rightEdgeRadius), controlPoint: arrowBottom)

        // bottom edge of tip
        path.addLine(to: calculate(from: path.currentPoint, to: arrowRight, less: rightCornerRadius))

        // tip
        path.addQuadCurve(to: calculate(from: arrowTop, to: arrowRight, less: rightCornerRadius), controlPoint: arrowRight)

        // top edge of tip
        path.addLine(to: calculate(from: path.currentPoint, to: arrowTop, less: rightEdgeRadius))

        // top right (after tip)
        path.addQuadCurve(to: calculate(from: start, to: arrowTop, less: rightEdgeRadius), controlPoint: arrowTop)

        path.close()

        shapeLayer.lineWidth = lineWidth
        shapeLayer.path = path.cgPath
    }

    /// Calculate some point between `startPoint` and `endPoint`, but `distance` from `endPoint
    ///
    /// - Parameters:
    ///   - startPoint: The starting point.
    ///   - endPoint: The ending point.
    ///   - distance: Distance from the ending point
    /// - Returns: Returns the point that is `distance` from the `endPoint` as you travel from `startPoint` to `endPoint`.

    private func calculate(from startPoint: CGPoint, to endPoint: CGPoint, less distance: CGFloat) -> CGPoint {
        let angle = atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x)
        let totalDistance = hypot(endPoint.y - startPoint.y, endPoint.x - startPoint.x) - distance

        return CGPoint(x: startPoint.x + totalDistance * cos(angle),
                       y: startPoint.y + totalDistance * sin(angle))
    }
}
Run Code Online (Sandbox Code Playgroud)

正因为如此@IBDesignable,我可以将它放在一个单独的框架目标中,然后有选择地在 Interface Builder 中使用它(并自定义它):

在此处输入图片说明

我对参数所做的唯一更改是不使用尖端的宽度,而是使用尖端的角度。这样,如果尺寸随着约束(或其他)的变化而变化,它会保留所需的形状。

我还改变了这一点,使用一种CAShapeLayer而不是自定义draw(_:)方法来享受 Apple 为塑造图层而内置的任何效率。