简体   繁体   中英

How the iOS system draw the view corner shape?

Sorry that my description is not clear, my purpose is to display some unusual borders, such as dashed lines, dots, gradients, etc., but using Bezier curves to draw cannot work with layer.cornerRadius . Of course, it can use layer.mask . to solve, for performance considerations and my curiosity, I want to combine them perfectly.

Original intention

I want to draw a custom border to UIView by my self. In this process, I have encountered some problems that confuse me:

  • The edge of the Bezier curve cannot be aligned with the edge of the view.
  • The rounded edge of the system is larger than the rounded range drawn by the Bezier curve.

Try to solve

Q1:

I have solved the first problem, because the pen tip for drawing the curve is in the middle of the line width, so it cannot be drawn along the edge of the view. It needs a certain distance。

To solve it, I separate the line segment from the view so that I can better observe:

let bgView = UIView(frame: CGRect(x: 0, y: 0, width: 80, height: 80))
bgView.backgroundColor = UIColor.red
bgView.layer.cornerRadius = 10
view.addSubview(bgView)
let imageView = UIImageView()
imageView.frame = CGRect(x: 100, y: 100, width: 100, height: 100)
imageView.backgroundColor = UIColor.clear
view.addSubview(imageView)
bgView.center = imageView.center

//draw a 80 * 80 image in the context center
UIGraphicsBeginImageContextWithOptions(CGSize(width: 100, height: 100), false, UIScreen.main.scale)
let borderWidth = 4
let rect = CGRect(x: 10 , y: 10, width: 80, height: 80)
let roundPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: 10, height: 10))
roundPath.lineWidth = 4
roundPath.lineJoinStyle = .round
UIColor.black.withAlphaComponent(0.3).setStroke()
roundPath.stroke()

imageView.image = UIGraphicsGetImageFromCurrentImageContext()
UIGraphicsEndImageContext()

Run and enlarge, you will see:

第一季度的结果

You can see that the edge of the view is in the middle of the line width, so the solution is to adjust the drawing of the Bezier curve rect.

//change
//let rect = CGRect(x: 10 , y: 10, width: 80, height: 80)
//to
let rect = CGRect(x: 10 + 2, y: 10 + 2, width: 80 - 4, height: 80 - 4)

but I did not find the relevant documentation to explain it. Does anyone know?

Q2

Because of Q1's solution, Q2 was also found:

Q2问题

Only the rounded corners of the View exceed the range of the Bezier curve.

At first I thought it was a problem with the way of drawing the Bezier curve, so I broke down the drawing steps:

let roundPath3 = UIBezierPath()
roundPath3.lineWidth = 4
roundPath3.lineJoinStyle = .round
roundPath3.move(to: CGPoint(x: rect.minX + 10, y: rect.minY))
roundPath3.addLine(to: CGPoint(x: rect.maxX - 10, y: rect.minY))
roundPath3.addArc(withCenter: CGPoint(x: rect.maxX - 10, y: rect.minY + 10), radius: 10, startAngle: 1.5 * CGFloat.pi, endAngle: 2 * CGFloat.pi, clockwise: true)
roundPath3.move(to: CGPoint(x: rect.maxX, y: rect.minY + 10))
roundPath3.addLine(to: CGPoint(x: rect.maxX, y: rect.maxY - 10))
roundPath3.addArc(withCenter: CGPoint(x: rect.maxX - 10, y: rect.maxY - 10), radius: 10, startAngle: 0 * CGFloat.pi, endAngle: 0.5 * CGFloat.pi, clockwise: true)
roundPath3.move(to: CGPoint(x: rect.maxX - 10, y: rect.maxY))
roundPath3.addLine(to: CGPoint(x: rect.minX + 10, y: rect.maxY))
roundPath3.addArc(withCenter: CGPoint(x: rect.minX + 10, y: rect.maxY - 10), radius: 10, startAngle: 0.5 * CGFloat.pi, endAngle: 1 * CGFloat.pi, clockwise: true)
roundPath3.move(to: CGPoint(x: rect.minX, y: rect.maxY - 10))
roundPath3.addLine(to: CGPoint(x: rect.minX, y: rect.minY + 10))
roundPath3.addArc(withCenter: CGPoint(x: rect.minX + 10, y: rect.minY + 10), radius: 10, startAngle: 1 * CGFloat.pi, endAngle: 1.5 * CGFloat.pi, clockwise: true)
UIColor.blue.withAlphaComponent(0.5).setStroke()
roundPath3.stroke()

Unfortunately it is the same as the result above. I also tried to extend the radius of the drawn arc by 1pt. Although it covered the rounded corners of the view, the result was ugly.

I observed carefully, and guess the implementation of the iOS system does not look like a pure circle, more like an ellipse. So I have an idea to adjust the control points of the quadratic Bezier curve to simulate, but I have no clue to calculate the appropriate control point.

You're running into a couple issues here...

First, there are two curve types for .cornerRadius :

  • .round -- the default. Works great for making a "round" view
  • .continuous -- a more pleasing visual curve, best for "rounded corners"

To demonstrate the difference, here is a green view on top of a red view, where the red view uses .round and the green view uses .continuous :

在此处输入图像描述

Note that if we set both to .continuous , we get this:

在此处输入图像描述

A UIBezierPath(roundedRect: ...) uses the .continuous curve, so if we mask the green view (instead of setting its .cornerRadius ) and use the default curve for the red view, it looks the same as the first example:

在此处输入图像描述

And if we mask the green view and set the red view to .continuous we get the same result as the second example:

在此处输入图像描述

If you're looking closely, you see a faint red "edge" -- this is due to UIKit anti-aliasing. If we set the "green view" background color to white, it's very obvious:

在此处输入图像描述

But, that's a different issue, which will probably not affect what you're trying to do.

The next part is trying to "outline" the view. As you already know, the path's stroke gets set to the centerline of the curve:

在此处输入图像描述

We have half of the stroke width inside, and half of the stroke width outside.

So, the attempt to "fix" that is to inset the outline view by 1/2 of the stroke width... but, again as you've seen, we end up with this:

在此处输入图像描述

That's because the stroke ends up with 3 Different Radii !

Let's use a corner radius of 40 and a stroke width of 16 to make it easier to see.

Here it is with equal sized views:

在此处输入图像描述

If we simply inset the "outline view" by 1/2 the border width and don't change the radius, we get this:

在此处输入图像描述

So, let's adjust the bezier path's corner radius by 1/2 of the stroke width:

在此处输入图像描述

and our final result (without the center-line shape layer) is:

在此处输入图像描述

Here's some code to play around and inspect what's going on -- each tap steps through what I've described above:

class CornersViewController: UIViewController {
    
    let stepLabel = UILabel()
    let infoLabel = UILabel()
    
    let bgViewWidth: CGFloat = 400
    let cornerRadius: CGFloat = 40
    let borderWidth: CGFloat = 16
    
    lazy var viewFrame: CGRect = CGRect(x: -200, y: 260, width: bgViewWidth, height: bgViewWidth)
    
    var step: Int = 1
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .white
        
        stepLabel.translatesAutoresizingMaskIntoConstraints = false
        stepLabel.font = .systemFont(ofSize: 12.0, weight: .bold)
        //stepLabel.textAlignment = .center
        view.addSubview(stepLabel)
        infoLabel.translatesAutoresizingMaskIntoConstraints = false
        infoLabel.numberOfLines = 0
        infoLabel.font = .systemFont(ofSize: 12.0, weight: .light)
        view.addSubview(infoLabel)
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            stepLabel.topAnchor.constraint(equalTo: g.topAnchor, constant: 20.0),
            stepLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            stepLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
            infoLabel.topAnchor.constraint(equalTo: stepLabel.bottomAnchor, constant: 8.0),
            infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor, constant: 20.0),
            infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor, constant: -20.0),
        ])

        let t = UITapGestureRecognizer(target: self, action: #selector(gotTap(_:)))
        view.addGestureRecognizer(t)
        
        nextStep()
    }
    
    @objc func gotTap(_ g: UITapGestureRecognizer) -> Void {
        nextStep()
    }
    
    func nextStep() {
        
        // remove existing example views, but not the "info" label
        view.subviews.forEach { v in
            if !(v is UILabel) {
                v.removeFromSuperview()
            }
        }
        
        stepLabel.text = "Step: \(step) of \(infoStrings.count)"
        infoLabel.text = infoStrings[step - 1]
        
        // red:     .cornerCurve = .round (default)
        // green:   .cornerCurve = .continuous
        if step == 1 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            
            let greenView = UIView(frame: viewFrame)
            view.addSubview(greenView)
            
            greenView.backgroundColor = UIColor.green
            greenView.layer.cornerRadius = cornerRadius
            greenView.layer.cornerCurve = .continuous
        }

        // red:     .cornerCurve = .continuous
        // green:   .cornerCurve = .continuous
        if step == 2 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous

            let greenView = UIView(frame: viewFrame)
            view.addSubview(greenView)
            
            greenView.backgroundColor = UIColor.green
            greenView.layer.cornerRadius = cornerRadius
            greenView.layer.cornerCurve = .continuous
        }
        
        // red:     .cornerCurve = .round (default)
        // green:   masked with UIBezierPath(roundedRect: ...)
        if step == 3 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            
            let greenView = UIView(frame: viewFrame)

            view.addSubview(greenView)
            
            greenView.backgroundColor = UIColor.green
            
            let maskLayer = CAShapeLayer()
            let maskBez = UIBezierPath(roundedRect: greenView.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
            maskLayer.path = maskBez.cgPath
            greenView.layer.mask = maskLayer
        }

        // red:     .cornerCurve = .continuous
        // green:   masked with UIBezierPath(roundedRect: ...)
        if step == 4 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let greenView = UIView(frame: viewFrame)
            
            view.addSubview(greenView)
            
            greenView.backgroundColor = UIColor.green
            
            let maskLayer = CAShapeLayer()
            let maskBez = UIBezierPath(roundedRect: greenView.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
            maskLayer.path = maskBez.cgPath
            greenView.layer.mask = maskLayer
        }
        
        // red:         .cornerCurve = .continuous
        // bordered:    sublayer with UIBezierPath(roundedRect: ...)
        //              clear fill, 30%-black stroke, lineWidth == borderWidth
        if step == 5 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let borderedView = UIView(frame: viewFrame)

            view.addSubview(borderedView)
            
            borderedView.backgroundColor = UIColor.clear

            borderedView.layer.cornerRadius = cornerRadius
            borderedView.layer.cornerCurve = .continuous

            let borderLayer = CAShapeLayer()
            let roundPath = UIBezierPath(roundedRect: borderedView.bounds, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))

            borderLayer.path = roundPath.cgPath

            borderLayer.lineWidth = borderWidth
            borderLayer.fillColor = UIColor.clear.cgColor
            borderLayer.strokeColor = UIColor.black.withAlphaComponent(0.3).cgColor

            borderedView.layer.addSublayer(borderLayer)
        }

        // red:         .cornerCurve = .continuous
        // bordered:    sublayer with UIBezierPath(roundedRect: ...)
        //              clear fill, 30%-black stroke, lineWidth == borderWidth
        //              frame inset by 1/2 borderWidth
        if step == 6 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let borderedView = UIView(frame: viewFrame)

            view.addSubview(borderedView)
            
            borderedView.backgroundColor = UIColor.red
            
            borderedView.layer.cornerRadius = cornerRadius
            borderedView.layer.cornerCurve = .continuous
            
            let borderLayer = CAShapeLayer()
            
            let rect = borderedView.bounds.insetBy(dx: borderWidth * 0.5, dy: borderWidth * 0.5)
            
            let roundPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
            
            borderLayer.path = roundPath.cgPath
            
            borderLayer.lineWidth = borderWidth
            borderLayer.fillColor = UIColor.clear.cgColor
            borderLayer.strokeColor = UIColor.black.withAlphaComponent(0.3).cgColor
            
            borderedView.layer.addSublayer(borderLayer)
        }
        
        // red:         .cornerCurve = .continuous
        // bordered:    sublayer with UIBezierPath(roundedRect: ...)
        //              clear fill, 30%-black stroke, lineWidth == borderWidth
        //              frame inset by 1/2 borderWidth
        //              showing border centerLine
        if step == 7 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let borderedView = UIView(frame: viewFrame)
            
            view.addSubview(borderedView)
            
            borderedView.backgroundColor = UIColor.red
            
            borderedView.layer.cornerRadius = cornerRadius
            borderedView.layer.cornerCurve = .continuous
            
            let borderLayer = CAShapeLayer()
            
            let rect = borderedView.bounds.insetBy(dx: borderWidth * 0.5, dy: borderWidth * 0.5)
            
            let roundPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: cornerRadius, height: cornerRadius))
            
            borderLayer.path = roundPath.cgPath
            
            borderLayer.lineWidth = borderWidth
            borderLayer.fillColor = UIColor.clear.cgColor
            borderLayer.strokeColor = UIColor.black.withAlphaComponent(0.3).cgColor
            
            borderedView.layer.addSublayer(borderLayer)
            
            let centerLineLayer = CAShapeLayer()
            
            centerLineLayer.path = roundPath.cgPath
            
            centerLineLayer.lineWidth = 1
            centerLineLayer.fillColor = UIColor.clear.cgColor
            centerLineLayer.strokeColor = UIColor.cyan.cgColor
            
            borderedView.layer.addSublayer(centerLineLayer)
        }
        
        // red:         .cornerCurve = .continuous
        // bordered:    sublayer with UIBezierPath(roundedRect: ...)
        //              clear fill, 30%-black stroke, lineWidth == borderWidth
        //              frame inset by 1/2 borderWidth
        //              radius adjusted by 1/2 borderWidth
        //              showing border centerLine
        if step == 8 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let borderedView = UIView(frame: viewFrame)
            
            view.addSubview(borderedView)
            
            borderedView.backgroundColor = UIColor.red
            
            borderedView.layer.cornerRadius = cornerRadius
            borderedView.layer.cornerCurve = .continuous
            
            let borderLayer = CAShapeLayer()
            
            let rect = borderedView.bounds.insetBy(dx: borderWidth * 0.5, dy: borderWidth * 0.5)
            let adjustedRadius = cornerRadius - (borderWidth * 0.5)
            
            let roundPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: adjustedRadius, height: adjustedRadius))
            
            borderLayer.path = roundPath.cgPath
            
            borderLayer.lineWidth = borderWidth
            borderLayer.fillColor = UIColor.clear.cgColor
            borderLayer.strokeColor = UIColor.black.withAlphaComponent(0.3).cgColor
            
            borderedView.layer.addSublayer(borderLayer)
            
            let centerLineLayer = CAShapeLayer()
            
            centerLineLayer.path = roundPath.cgPath
            
            centerLineLayer.lineWidth = 1
            centerLineLayer.fillColor = UIColor.clear.cgColor
            centerLineLayer.strokeColor = UIColor.cyan.cgColor
            
            borderedView.layer.addSublayer(centerLineLayer)
        }
        
        // red:         .cornerCurve = .continuous
        // bordered:    sublayer with UIBezierPath(roundedRect: ...)
        //              clear fill, 30%-black stroke, lineWidth == borderWidth
        //              frame inset by 1/2 borderWidth
        //              radius adjusted by 1/2 borderWidth
        if step == 9 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let borderedView = UIView(frame: viewFrame)

            view.addSubview(borderedView)
            
            borderedView.backgroundColor = UIColor.red
            
            borderedView.layer.cornerRadius = cornerRadius
            borderedView.layer.cornerCurve = .continuous
            
            let borderLayer = CAShapeLayer()
            
            let rect = borderedView.bounds.insetBy(dx: borderWidth * 0.5, dy: borderWidth * 0.5)
            let adjustedRadius = cornerRadius - (borderWidth * 0.5)
            
            let roundPath = UIBezierPath(roundedRect: rect, byRoundingCorners: .allCorners, cornerRadii: CGSize(width: adjustedRadius, height: adjustedRadius))
            
            borderLayer.path = roundPath.cgPath
            
            borderLayer.lineWidth = borderWidth
            borderLayer.fillColor = UIColor.clear.cgColor
            borderLayer.strokeColor = UIColor.black.withAlphaComponent(0.3).cgColor
            
            borderedView.layer.addSublayer(borderLayer)
        }
        
        // red:     .cornerCurve = .continuous
        // white:   .cornerCurve = .continuous
        if step == 10 {
            let redView = UIView(frame: viewFrame)
            view.addSubview(redView)
            
            redView.backgroundColor = UIColor.red
            redView.layer.cornerRadius = cornerRadius
            redView.layer.cornerCurve = .continuous
            
            let whiteView = UIView(frame: viewFrame)
            view.addSubview(whiteView)
            
            whiteView.backgroundColor = UIColor.white
            whiteView.layer.cornerRadius = cornerRadius
            whiteView.layer.cornerCurve = .continuous
        }
        
        step += 1
        if step > infoStrings.count {
            step = 1
        }
        
    }
    
    let infoStrings: [String] = [
        "redView:\n    .cornerCurve = .round (default)\n\ngreenView:\n    .cornerCurve = .continuous",
        "redView:\n    .cornerCurve = .continuous\n\ngreenView:\n    .cornerCurve = .continuous",
        "redView:\n    .cornerCurve = .round (default)\n\ngreenView:\n    masked with UIBezierPath(roundedRect: ...)",
        "redView:\n    .cornerCurve = .continuous\n\ngreenView:\n    masked with UIBezierPath(roundedRect: ...)",
        "redView:\n    .cornerCurve = .continuous\n\nborderedView:\n    sublayer with UIBezierPath(roundedRect: ...)\n    clear fill, 30%-black stroke, lineWidth == borderWidth",
        "redView:\n    .cornerCurve = .continuous\n\nborderedView:\n    sublayer with UIBezierPath(roundedRect: ...)\n    clear fill, 30%-black stroke, lineWidth == borderWidth\n    frame inset by 1/2 borderWidth",
        "redView:\n    .cornerCurve = .continuous\n\nborderedView:\n    sublayer with UIBezierPath(roundedRect: ...)\n    clear fill, 30%-black stroke, lineWidth == borderWidth\n    frame inset by 1/2 borderWidth\n    showing border center line",
        "redView:\n    .cornerCurve = .continuous\n\nborderedView:\n    sublayer with UIBezierPath(roundedRect: ...)\n    clear fill, 30%-black stroke, lineWidth == borderWidth\n    frame inset by 1/2 borderWidth\n    radius adjusted by 1/2 borderWidth\n    showing border center line",
        "redView:\n    .cornerCurve = .continuous\n\nborderedView:\n    sublayer with UIBezierPath(roundedRect: ...)\n    clear fill, 30%-black stroke, lineWidth == borderWidth\n    frame inset by 1/2 borderWidth\n    radius adjusted by 1/2 borderWidth\n    final result",
        "redView:\n    .cornerCurve = .continuous\n\nwhiteView:\n    .cornerCurve = .continuous\n\n    Showing the Anti-Aliasing...",
    ]
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

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