iOS UIView - 4 个角各有不同的圆角半径

iOS UIView - Different rounding radius for each of the 4 corners

如何为 UIView 的 4 个角分别设置完全不同的圆角半径?

UIBezierPath 允许我为一个或多个特定角设置一个值,但不能为每个角设置不同的值。

我认为理论上应该可以使用自定义 CGPath,但我无法实现它。

试一试 - 您可以将它直接粘贴到 Playground 页面中,看看它是如何工作的:

import UIKit
import PlaygroundSupport

extension Int {
    var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
    var degreesToRadians: Self { return self * .pi / 180 }
    var radiansToDegrees: Self { return self * 180 / .pi }
}

class VariableCornerRadiusView: UIView  {

    var upperLeftCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var upperRightCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var lowerLeftCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var lowerRightCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }


    func layoutMask() -> Void {

        var pt = CGPoint.zero

        let myBezier = UIBezierPath()

        pt.x = upperLeftCornerRadius
        pt.y = 0

        myBezier.move(to: pt)

        pt.x = bounds.width - upperRightCornerRadius
        pt.y = 0

        myBezier.addLine(to: pt)

        pt.x = bounds.width - upperRightCornerRadius
        pt.y = upperRightCornerRadius

        myBezier.addArc(withCenter: pt, radius: upperRightCornerRadius, startAngle: CGFloat(270.degreesToRadians), endAngle: CGFloat(0.degreesToRadians), clockwise: true)

        pt.x = bounds.width
        pt.y = bounds.height - lowerRightCornerRadius

        myBezier.addLine(to: pt)

        pt.x = bounds.width - lowerRightCornerRadius
        pt.y = bounds.height - lowerRightCornerRadius

        myBezier.addArc(withCenter: pt, radius: lowerRightCornerRadius, startAngle: CGFloat(0.degreesToRadians), endAngle: CGFloat(90.degreesToRadians), clockwise: true)

        pt.x = lowerLeftCornerRadius
        pt.y = bounds.height

        myBezier.addLine(to: pt)

        pt.x = lowerLeftCornerRadius
        pt.y = bounds.height - lowerLeftCornerRadius

        myBezier.addArc(withCenter: pt, radius: lowerLeftCornerRadius, startAngle: CGFloat(90.degreesToRadians), endAngle: CGFloat(180.degreesToRadians), clockwise: true)

        pt.x = 0
        pt.y = upperLeftCornerRadius

        myBezier.addLine(to: pt)

        pt.x = upperLeftCornerRadius
        pt.y = upperLeftCornerRadius

        myBezier.addArc(withCenter: pt, radius: upperLeftCornerRadius, startAngle: CGFloat(180.degreesToRadians), endAngle: CGFloat(270.degreesToRadians), clockwise: true)

        myBezier.close()

        let maskForPath = CAShapeLayer()
        maskForPath.path = myBezier.cgPath
        layer.mask = maskForPath

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutMask()
    }

}

var testSize = CGSize(width: 200, height: 200)

// set up an orange view to hold it...
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 260))

containerView.backgroundColor = UIColor.orange

// create a VariableCornerRadiusView, just a little smaller than the container view
let TestView = VariableCornerRadiusView(frame: containerView.bounds.insetBy(dx: 20, dy: 20))

// set different radius for each corner
TestView.upperLeftCornerRadius = 20.0
TestView.upperRightCornerRadius = 40.0
TestView.lowerRightCornerRadius = 60.0
TestView.lowerLeftCornerRadius = 80.0

// give it a blue background
TestView.backgroundColor = UIColor.blue

// add it to the container
containerView.addSubview(TestView)

// show it
PlaygroundPage.current.liveView = containerView

结果应如下所示:


编辑另一种方法...使用填充形状图层(而不是蒙版)和阴影:

import UIKit
import PlaygroundSupport

extension Int {
    var degreesToRadians: Double { return Double(self) * .pi / 180 }
}
extension FloatingPoint {
    var degreesToRadians: Self { return self * .pi / 180 }
    var radiansToDegrees: Self { return self * 180 / .pi }
}

class VariableCornerRadiusShadowView: UIView  {

    var upperLeftCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var upperRightCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var lowerLeftCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var lowerRightCornerRadius:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var fillColor: UIColor = .white {
        didSet {
            self.setNeedsLayout()
        }
    }

    var shadowColor: UIColor = .black {
        didSet {
            self.setNeedsLayout()
        }
    }

    var shadowOffset: CGSize = CGSize(width: 0.0, height: 2.0) {
        didSet {
            self.setNeedsLayout()
        }
    }

    var shadowOpacity: Float = 0.5 {
        didSet {
            self.setNeedsLayout()
        }
    }

    var shadowRadius: CGFloat = 8.0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    let theShapeLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }

    func commonInit() -> Void {
        layer.addSublayer(theShapeLayer)
    }

    func layoutShape() -> Void {

        var pt = CGPoint.zero

        let myBezier = UIBezierPath()

        pt.x = upperLeftCornerRadius
        pt.y = 0

        myBezier.move(to: pt)

        pt.x = bounds.width - upperRightCornerRadius
        pt.y = 0

        myBezier.addLine(to: pt)

        pt.x = bounds.width - upperRightCornerRadius
        pt.y = upperRightCornerRadius

        myBezier.addArc(withCenter: pt, radius: upperRightCornerRadius, startAngle: CGFloat(270.degreesToRadians), endAngle: CGFloat(0.degreesToRadians), clockwise: true)

        pt.x = bounds.width
        pt.y = bounds.height - lowerRightCornerRadius

        myBezier.addLine(to: pt)

        pt.x = bounds.width - lowerRightCornerRadius
        pt.y = bounds.height - lowerRightCornerRadius

        myBezier.addArc(withCenter: pt, radius: lowerRightCornerRadius, startAngle: CGFloat(0.degreesToRadians), endAngle: CGFloat(90.degreesToRadians), clockwise: true)

        pt.x = lowerLeftCornerRadius
        pt.y = bounds.height

        myBezier.addLine(to: pt)

        pt.x = lowerLeftCornerRadius
        pt.y = bounds.height - lowerLeftCornerRadius

        myBezier.addArc(withCenter: pt, radius: lowerLeftCornerRadius, startAngle: CGFloat(90.degreesToRadians), endAngle: CGFloat(180.degreesToRadians), clockwise: true)

        pt.x = 0
        pt.y = upperLeftCornerRadius

        myBezier.addLine(to: pt)

        pt.x = upperLeftCornerRadius
        pt.y = upperLeftCornerRadius

        myBezier.addArc(withCenter: pt, radius: upperLeftCornerRadius, startAngle: CGFloat(180.degreesToRadians), endAngle: CGFloat(270.degreesToRadians), clockwise: true)

        myBezier.close()

        theShapeLayer.path = myBezier.cgPath
        theShapeLayer.fillColor = fillColor.cgColor

        layer.shadowRadius = shadowRadius
        layer.shadowOffset = shadowOffset
        layer.shadowOpacity = shadowOpacity
        layer.shadowColor = shadowColor.cgColor

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutShape()
    }

}

var testSize = CGSize(width: 200, height: 200)

// set up an orange "container" view to hold it...
let containerView = UIView(frame: CGRect(x: 0, y: 0, width: 300, height: 260))

containerView.backgroundColor = UIColor.white

let sampleView = VariableCornerRadiusShadowView(frame: containerView.bounds.insetBy(dx: 20, dy: 20))

// set different radius for each corner
sampleView.upperLeftCornerRadius = 20.0
sampleView.upperRightCornerRadius = 40.0
sampleView.lowerRightCornerRadius = 60.0
sampleView.lowerLeftCornerRadius = 80.0

// if we want to adjust defaults
//sampleView.fillColor = .green
//sampleView.shadowOffset = CGSize(width: 2, height: 4)
//sampleView.shadowRadius = 4  // not quite so "fuzzy"
//sampleView.shadowOpacity = 0.8

// add view to container
containerView.addSubview(sampleView)

// show it
PlaygroundPage.current.liveView = containerView

具有 "default" 属性的结果:

属性更改为的结果:

.fillColor = .green
.shadowOffset = CGSize(width: 2, height: 4)
.shadowRadius = 4  // not quite so "fuzzy"
.shadowOpacity = 0.8


编辑 2 -

此版本现在是 @IBDesignable,重命名了一些属性以便在 IB 中显示得更好。还添加了边框宽度和颜色:

@IBDesignable
class VariableCornerRadiusShadowView: UIView  {

    @IBInspectable
    var radTopLeft:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var radTopRright:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var radBotLeft:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var radBotRright:CGFloat = 0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var fillColor: UIColor = .white {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var borderColor: UIColor = .clear {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var borderWidth: CGFloat = 0.0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var shadowColor: UIColor = .black {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var shadowXOffset: CGFloat = 0.0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var shadowYOffset: CGFloat = 0.0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var shadowOpacity: Float = 0.5 {
        didSet {
            self.setNeedsLayout()
        }
    }

    @IBInspectable
    var shadowRadius: CGFloat = 8.0 {
        didSet {
            self.setNeedsLayout()
        }
    }

    let theShapeLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    override func prepareForInterfaceBuilder() {
        commonInit()
    }

    func commonInit() -> Void {
        backgroundColor = .clear
        layer.addSublayer(theShapeLayer)
    }

    func layoutShape() -> Void {

        var pt = CGPoint.zero

        let myBezier = UIBezierPath()

        pt.x = radTopLeft
        pt.y = 0

        myBezier.move(to: pt)

        pt.x = bounds.width - radTopRright
        pt.y = 0

        myBezier.addLine(to: pt)

        pt.x = bounds.width - radTopRright
        pt.y = radTopRright

        myBezier.addArc(withCenter: pt, radius: radTopRright, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)

        pt.x = bounds.width
        pt.y = bounds.height - radBotRright

        myBezier.addLine(to: pt)

        pt.x = bounds.width - radBotRright
        pt.y = bounds.height - radBotRright

        myBezier.addArc(withCenter: pt, radius: radBotRright, startAngle: 0, endAngle: .pi * 0.5, clockwise: true)

        pt.x = radBotLeft
        pt.y = bounds.height

        myBezier.addLine(to: pt)

        pt.x = radBotLeft
        pt.y = bounds.height - radBotLeft

        myBezier.addArc(withCenter: pt, radius: radBotLeft, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true)

        pt.x = 0
        pt.y = radTopLeft

        myBezier.addLine(to: pt)

        pt.x = radTopLeft
        pt.y = radTopLeft

        myBezier.addArc(withCenter: pt, radius: radTopLeft, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true)

        myBezier.close()

        theShapeLayer.path = myBezier.cgPath
        theShapeLayer.fillColor = fillColor.cgColor

        theShapeLayer.strokeColor = borderColor.cgColor
        theShapeLayer.lineWidth = borderWidth

        layer.shadowRadius = shadowRadius
        layer.shadowOffset = CGSize(width: shadowXOffset, height: shadowYOffset)
        layer.shadowOpacity = shadowOpacity
        layer.shadowColor = shadowColor.cgColor

    }

    override func layoutSubviews() {
        super.layoutSubviews()
        self.layoutShape()
    }

}

这是 UIBezierPath 的便利初始值设定项,具有不同的角半径。当所有半径都相等时,它应该给你与 UIBezierPath(roundedRect:byRoundingCorners:cornerRadii:).

相同的路径
extension UIBezierPath {
    convenience init(roundedRect bounds: CGRect,
                     cornerRadii radii: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat)) {
        self.init()
        func point(x: CGFloat, y: CGFloat) -> CGPoint {
            return CGPoint(x: bounds.origin.x + x, y: bounds.origin.y + y)
        }
        move(to: point(x: radii.topLeft, y: 0))
        addLine(to: point(x: bounds.width - radii.topRight, y: 0))
        addArc(withCenter: point(x: bounds.width - radii.topRight, y: radii.topRight),
               radius: radii.topRight, startAngle: .pi * 1.5, endAngle: 0, clockwise: true)
        addLine(to: point(x: bounds.width, y: bounds.height - radii.bottomRight))
        addArc(withCenter: point(x: bounds.width - radii.bottomRight, y: bounds.height - radii.bottomRight),
               radius: radii.bottomRight, startAngle: 0, endAngle: .pi * 0.5, clockwise: true)
        addLine(to: point(x: radii.bottomLeft, y: bounds.height))
        addArc(withCenter: point(x: radii.bottomLeft, y: bounds.height - radii.bottomLeft),
               radius: radii.bottomLeft, startAngle: .pi * 0.5, endAngle: .pi, clockwise: true)
        addLine(to: point(x: 0, y: radii.topLeft))
        addArc(withCenter: point(x: radii.topLeft, y: radii.topLeft),
               radius: radii.topLeft, startAngle: .pi, endAngle: .pi * 1.5, clockwise: true)
        close()
    }
}

您可以轻松地将其用于遮蔽或其他目的:

extension UIView {
    func layoutMask(cornerRadii radii: (topLeft: CGFloat, topRight: CGFloat, bottomLeft: CGFloat, bottomRight: CGFloat)) {
        let maskLayer = CAShapeLayer()
        maskLayer.path = UIBezierPath(roundedRect: bounds, cornerRadii: radii).cgPath
        layer.mask = maskLayer
    }
}