简体   繁体   English

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

[英]Drawing a custom view using UIBezierPath results in a non-symmetrical shape

I'm trying to draw an UIView with some 'curvy edges'. 我正在尝试绘制带有一些“弯曲边缘”的UIView。

Here's what it's supposed to look like: 它应该是这样的:

在此处输入图片说明

here's what I got: 这是我得到的:

在此处输入图片说明

Notice how the top right (TR) corner is not symmetrical to the bottom right (BR) corner ? 请注意,右上角(TR)与右下角(BR)如何不对称? The BR corner is very similar to what I want to achieve but I can't get the TR corner to align correctly (played around with bunch of different start and end angles). BR角与我要实现的角非常相似,但是我无法使TR角正确对齐(使用一堆不同的起始角度和结束角度)。

here's the code: 这是代码:

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 }
}

Just a quick summary of my thought process: 简要概述一下我的思考过程:

  • Create a bezierPath and move it to the startingPoint 创建一个bezierPath并将其移动到startingPoint
  • Add the LT (left-top) curve and move the line downards 添加LT(左上)曲线并向下移动线
  • Move the line along the left edge and add the LB (left-bottom) curve and the move line along the bottom to the right edge 沿着左边缘移动线,并添加LB(左下)曲线,然后沿着底部向右边缘移动线
  • Move the line till frame.size.width - Constants.rightTipWidth 将线移动到frame.size.width - Constants.rightTipWidth
  • Add an arc with a center point at x = currentPoint and y = height- rightEdgeRadius 添加一个圆心,其中心点为x = currentPointy = height- rightEdgeRadius
  • Move the line up until y = (height / 2.0) + (Constants.rightCornerRadius / 2.0) 向上移动行,直到y = (height / 2.0) + (Constants.rightCornerRadius / 2.0)
  • Add the QuadCurve with an end point of y = (height / 2.0) - (Constants.rightCornerRadius / 2.0) 添加端点为y = (height / 2.0) - (Constants.rightCornerRadius / 2.0)的QuadCurve
  • Move the line up till x = maxXRightEdge + Constants.rightEdgeRadius 将线向上移动直到x = maxXRightEdge + Constants.rightEdgeRadius
  • Add the top right (TR) curve ---> resulting in a non-symmetrical curvature 添加右上(TR)曲线--->导致非对称曲率

Here is another rendition: 这是另一种形式:

@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))
    }
}

And because that is @IBDesignable , I can put it in a separate framework target and then optionally use it (and customize it) right in Interface Builder: 而且因为这是@IBDesignable ,所以我可以将其放在单独的框架目标中,然后可以选择在Interface Builder中使用它(并对其进行自定义):

在此处输入图片说明

The only change I made in parameters was to not use the width of the tip, but rather the angle of the tip. 我对参数所做的唯一更改是不使用笔尖的宽度,而是使用笔尖的角度。 That way, if the size changes as constraints (or whatever) change, it preserves the desired shape. 这样,如果大小随约束(或其他任何因素)的变化而变化,它将保留所需的形状。

I also changed this to use a CAShapeLayer rather that a custom draw(_:) method to enjoy any efficiencies that Apple has built in to shape layers. 我也将其更改为使用CAShapeLayer而不是自定义draw(_:)方法,以享受Apple内置的用于塑造图层的任何效率。

I don't know your implementation but I think it will be easy if you implemented it like that , that way you cam achieve symmetric shape perfectly 我不知道您的实现方式,但我认为如果这样实现,那会很容易,这样您就可以完美地实现对称形状

在此处输入图片说明

to draw a triangle , just tweak the positions of triangle points 绘制三角形,只需调整三角形点的位置

 class TriangleView : UIView {

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

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

override func draw(_ rect: CGRect) {

    guard let context = UIGraphicsGetCurrentContext() else { return }

    context.beginPath()
    context.move(to: CGPoint(x: rect.minX, y: rect.maxY))
    context.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY))
    context.addLine(to: CGPoint(x: (rect.maxX / 2.0), y: rect.minY))
    context.closePath()

    context.setFillColor(red: 1.0, green: 0.5, blue: 0.0, alpha: 0.60)
    context.fillPath()
}
}

Here, you forgot halfRadius 在这里,你忘了halfRadius

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

Full code: 完整代码:

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 - halfRadius))

    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()
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM