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.
I want to draw a custom border to UIView
by my self. In this process, I have encountered some problems that confuse me:
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?
Because of Q1's solution, Q2 was also found:
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.