问题描述
抱歉我的描述不清楚,我的目的是为了显示一些不常见的边框,比如虚线、圆点、渐变等,但是使用贝塞尔曲线绘制不能用layer.cornerRadius
。当然,它可以使用layer.mask
。为了解决,出于性能考虑和我的好奇心,我想将它们完美结合。
初衷
我想自己为 UIView
绘制自定义边框。在这个过程中,遇到了一些让我困惑的问题:
- 贝塞尔曲线的边缘无法与视图的边缘对齐。
- 系统的圆角边缘大于贝塞尔曲线绘制的圆角范围。
尝试解决
第一季度:
第一个问题我已经解决了,因为绘制曲线的笔尖在线宽的中间,所以不能沿着视图的边缘绘制。需要一定的距离。
为了解决它,我把线段从视图中分离出来,以便更好地观察:
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,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()
运行放大,你会看到:
可以看到view的边缘在线宽的中间,所以解决方法是调整Bezier曲线rect的绘制。
//change
//let rect = CGRect(x: 10,height: 80)
//to
let rect = CGRect(x: 10 + 2,y: 10 + 2,width: 80 - 4,height: 80 - 4)
但是我没有找到相关的文档来解释它。有人知道吗?
第 2 季度
因为Q1的解,Q2也被找到了:
只有View的圆角超出了贝塞尔曲线的范围。
一开始以为是贝塞尔曲线的绘制方式有问题,所以分解了绘制步骤:
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),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,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,startAngle: 1 * CGFloat.pi,endAngle: 1.5 * CGFloat.pi,clockwise: true)
UIColor.blue.withAlphaComponent(0.5).setstroke()
roundpath3.stroke()
不幸的是,它与上面的结果相同。我还尝试将绘制的弧的半径扩大 1pt。虽然它覆盖了视图的圆角,但结果很丑陋。
仔细观察,猜测iOS系统的实现看起来不像是一个纯圆,更像是一个椭圆。于是就有了调整二次贝塞尔曲线的控制点来模拟的想法,但是一直没有办法计算出合适的控制点。
解决方法
您在这里遇到了几个问题...
首先,.cornerRadius
有两种曲线类型:
-
.round
-- 默认值。非常适合制作“圆形”视图 -
.continuous
-- 更令人愉悦的视觉曲线,最适合“圆角”
为了演示不同之处,这里是在红色视图之上的绿色视图,其中红色视图使用 .round
,绿色视图使用 .continuous
:
请注意,如果我们将两者都设置为 .continuous
,我们会得到:
A UIBezierPath(roundedRect: ...)
使用 .continuous
曲线,所以如果我们屏蔽绿色视图(而不是设置它的 .cornerRadius
)并使用红色视图的默认曲线,它看起来是一样的作为第一个例子:
如果我们屏蔽绿色视图并将红色视图设置为 .continuous
,我们会得到与第二个示例相同的结果:
如果您仔细观察,您会看到一个微弱的红色“边缘”——这是由于 UIKit
抗锯齿。如果我们将“绿色视图”背景色设置为白色,就很明显了:
但是,这是一个不同的问题,它可能不会影响您尝试做的事情。
下一部分是尝试“概述”视图。如您所知,路径的 stroke
设置为曲线的中心线:
我们有一半的笔画宽度在里面,一半的笔画宽度在外面。
因此,尝试“修复”即按笔画宽度的 1/2 插入轮廓视图......但是,正如您所见,我们最终得到了这个:
那是因为笔画以3个不同的半径结束!
让我们使用 40
的角半径和 16
的笔触宽度使其更容易看到。
这是具有相同大小的视图:
如果我们简单地将“轮廓视图”插入边框宽度的 1/2 并且不改变半径,我们会得到:
所以,让我们将贝塞尔曲线的角半径调整为笔画宽度的 1/2:
我们的最终结果(没有中心线形状层)是:
这里有一些代码可以用来检查和检查发生了什么——每次点击都会执行我上面描述的步骤:
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,stepLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor,constant: -20.0),infoLabel.topAnchor.constraint(equalTo: stepLabel.bottomAnchor,constant: 8.0),infoLabel.leadingAnchor.constraint(equalTo: g.leadingAnchor,infoLabel.trailingAnchor.constraint(equalTo: g.trailingAnchor,])
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,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,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,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,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,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,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,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,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,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,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,lineWidth == borderWidth",lineWidth == borderWidth\n frame inset by 1/2 borderWidth",lineWidth == borderWidth\n frame inset by 1/2 borderWidth\n showing border center line",lineWidth == borderWidth\n frame inset by 1/2 borderWidth\n radius adjusted by 1/2 borderWidth\n showing border center line",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...",]
}