CAShapeLayer 描边问题
CAShapeLayer stroke issue
我使用 CAShapeLayer
而不是 CALayer
的唯一原因是动画 属性。
view.layer
的红色边框是参考。如果为 shapeLayer
设置 lineWidth
则此图层超出 red
框架。
但我希望它适合 red
框。 (适合NSView
)
代码:
CustomView.swift:
class CustomView: NSView{
let shapeLayer = CAShapeLayer()
init(){
super.init(frame: .zero)
wantsLayer = true
layer?.borderWidth = 1.0
layer?.borderColor = NSColor.red.cgColor
layer?.masksToBounds = false
layer!.addSublayer(shapeLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = CGMutablePath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: rect.width/2, y:rect.height))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.closeSubpath()
shapeLayer.path = path
shapeLayer.lineWidth = 30
shapeLayer.strokeColor = NSColor.lightGray.cgColor
shapeLayer.fillColor = .white
}
}
ViewController.swift
class ViewController: NSViewController {
private lazy var customView: CustomView = {
let customView = CustomView()
view.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
customView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
customView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
customView.heightAnchor.constraint(equalToConstant: 144),
customView.widthAnchor.constraint(equalToConstant: 144)
])
return customView
}()
override func viewDidLoad() {
super.viewDidLoad()
customView.shapeLayer.fillColor = NSColor.systemGreen.cgColor
}
}
截图:
更新:
根据对这个问题的回答和评论。我确实通过以下代码
更新了 override func draw(_ rect: NSRect)
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = CGMutablePath()
let lineWidth: CGFloat = 30
path.move(to: .init(x: lineWidth/2, y: lineWidth/2))
path.addLine(to: .init(x: rect.width/2, y: rect.height - lineWidth/2))
path.addLine(to: .init(x: rect.width - lineWidth/2, y: lineWidth/2))
path.closeSubpath()
shapeLayer.path = path
shapeLayer.lineWidth = lineWidth
}
CustomView.init 是,
init(){
super.init(frame: .zero)
wantsLayer = true
layer?.borderWidth = 1.0
layer?.borderColor = NSColor.red.cgColor
layer?.masksToBounds = false
layer!.addSublayer(shapeLayer)
shapeLayer.strokeColor = NSColor.lightGray.cgColor
shapeLayer.fillColor = .white
}
输出:
但是还是画不对
更新:
修改了路径中的第一行...所以整个路径将是,
let path = CGMutablePath()
let lineWidth: CGFloat = 30
path.move(to: .init(x: lineWidth/2, y: lineWidth/2))
path.addLine(to: .init(x: rect.width/2, y: rect.height - lineWidth))
path.addLine(to: .init(x: rect.width - lineWidth/2, y: lineWidth/2))
path.closeSubpath()
输出:
现在,我对修改第二行感到困惑。我不知道如何解决它。请hint/help我来解决这个问题。提前致谢...
edit/update:
将线宽乘以 2 并为形状添加遮罩会更容易:
class Triangle: NSView {
let shapeLayer = CAShapeLayer()
var lineWidth: CGFloat
var strokeColor: NSColor = .clear
var fillColor: NSColor = .clear
init(size: CGSize, lineWidth: CGFloat = 10, strokeColor: NSColor = .white, fillColor: NSColor = .black) {
self.lineWidth = lineWidth*2
self.strokeColor = strokeColor
self.fillColor = fillColor
super.init(frame: .init(origin: .zero, size: size))
wantsLayer = true
let path = NSBezierPath()
path.move(to: .zero)
path.line(to: .init(x: size.width/2 , y: size.height))
path.line(to: .init(x: size.width, y: .zero))
path.close()
let mask = CAShapeLayer()
mask.path = path.cgPath
shapeLayer.mask = mask
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = self.lineWidth
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = fillColor.cgColor
// layer?.borderWidth = 1
// layer?.borderColor = NSColor.red.cgColor
layer?.addSublayer(shapeLayer)
}
convenience init(width: CGFloat, height: CGFloat, lineWidth: CGFloat = 10, strokeColor: NSColor = .white, fillColor: NSColor = .black) {
self.init(size: .init(width: width, height: height), lineWidth: lineWidth, strokeColor: strokeColor, fillColor: fillColor)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NSBezierPath {
var cgPath: CGPath {
let path = CGMutablePath()
var points: [CGPoint] = .init(repeating: .zero, count: 3)
for i in 0..<elementCount {
switch element(at: i, associatedPoints: &points) {
case .moveTo: path.move(to: points[0])
case .lineTo: path.addLine(to: points[0])
case .curveTo: path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath: path.closeSubpath()
@unknown default: fatalError()
}
}
return path
}
}
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let triangle = Triangle(width: 200, height: 200, lineWidth: 1)
view.addSubview(triangle)
}
}
我并没有真正开发 iOS 应用程序。但请看下面
// View controller //
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 200, height: 200))
let triangleView = TriangleView(frame: rect, backColor: UIColor.green, strokeColor: UIColor.blue, lineWidth: 10)
view.addSubview(triangleView)
}
}
// Subclass of UIView //
import UIKit
class TriangleView: UIView {
var backColor: UIColor
var strokeColor: UIColor
var lineWidth: CGFloat
init(frame: CGRect, backColor: UIColor, strokeColor: UIColor, lineWidth: CGFloat) {
self.backColor = backColor
self.strokeColor = strokeColor
self.lineWidth = lineWidth
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
backColor.set()
//UIRectFill(rect)
let shapeLayer = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.addLine(to: CGPoint(x: lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.addLine(to: CGPoint(x: rect.width - lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.addLine(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.close()
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = backColor.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
layer.insertSublayer(shapeLayer, at: 0)
layer.masksToBounds = false
}
}
以下为Cocoa版本
// View controller //
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 200, height: 200))
let myView = MyView(frame: rect, fillColor: NSColor.green, strokeColor: NSColor.red, lineWidth: 10.0)
myView.wantsLayer = true
view.addSubview(myView)
}
}
// Subclass of NSView //
import Cocoa
class MyView: NSView {
override var isFlipped: Bool { return true }
var fillColor: NSColor
var strokeColor: NSColor
var lineWidth: CGFloat
init(frame: CGRect, fillColor: NSColor, strokeColor: NSColor, lineWidth: CGFloat){
self.fillColor = fillColor
self.strokeColor = strokeColor
self.lineWidth = lineWidth
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = NSBezierPath()
path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.line(to: CGPoint(x: lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.line(to: CGPoint(x: rect.width - lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.line(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.close()
fillColor.setFill()
path.fill()
path.lineWidth = lineWidth
strokeColor.set()
path.stroke()
}
}
“带掩码的双线宽”实现的另一种变体:
class TriangleView: NSView {
let lineWidth: CGFloat = 20
private lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = NSColor.clear.cgColor
shapeLayer.strokeColor = NSColor.red.cgColor
shapeLayer.lineWidth = lineWidth * 2
return shapeLayer
}()
private let maskLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = NSColor.white.cgColor
shapeLayer.strokeColor = NSColor.clear.cgColor
shapeLayer.lineWidth = 0
return shapeLayer
}()
override init(frame frameRect: NSRect = .zero) {
super.init(frame: frameRect)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
wantsLayer = true
layer?.borderColor = NSColor.blue.cgColor
layer?.borderWidth = 1
layer?.addSublayer(shapeLayer)
}
override func layout() {
super.layout()
let path = CGMutablePath()
path.move(to: NSPoint(x: bounds.midX, y: bounds.maxY))
path.addLine(to: NSPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: NSPoint(x: bounds.minX, y: bounds.minY))
path.closeSubpath()
shapeLayer.path = path
maskLayer.path = path
shapeLayer.mask = maskLayer
}
}
概念与接受的答案相同(重新加倍线宽和掩码)。
如您所见,当使用CAShapeLayer
时,我们没有实现draw
, but rather let the shape layer take care of the rendering. But we do want to respond to frame changes, so we set (and reset) the path in layout
。
无论如何,结果是:
我使用 CAShapeLayer
而不是 CALayer
的唯一原因是动画 属性。
view.layer
的红色边框是参考。如果为 shapeLayer
设置 lineWidth
则此图层超出 red
框架。
但我希望它适合 red
框。 (适合NSView
)
代码:
CustomView.swift:
class CustomView: NSView{
let shapeLayer = CAShapeLayer()
init(){
super.init(frame: .zero)
wantsLayer = true
layer?.borderWidth = 1.0
layer?.borderColor = NSColor.red.cgColor
layer?.masksToBounds = false
layer!.addSublayer(shapeLayer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = CGMutablePath()
path.move(to: CGPoint.zero)
path.addLine(to: CGPoint(x: rect.width/2, y:rect.height))
path.addLine(to: CGPoint(x: rect.width, y: 0))
path.closeSubpath()
shapeLayer.path = path
shapeLayer.lineWidth = 30
shapeLayer.strokeColor = NSColor.lightGray.cgColor
shapeLayer.fillColor = .white
}
}
ViewController.swift
class ViewController: NSViewController {
private lazy var customView: CustomView = {
let customView = CustomView()
view.addSubview(customView)
customView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
customView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
customView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
customView.heightAnchor.constraint(equalToConstant: 144),
customView.widthAnchor.constraint(equalToConstant: 144)
])
return customView
}()
override func viewDidLoad() {
super.viewDidLoad()
customView.shapeLayer.fillColor = NSColor.systemGreen.cgColor
}
}
截图:
更新:
根据对这个问题的回答和评论。我确实通过以下代码
更新了override func draw(_ rect: NSRect)
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = CGMutablePath()
let lineWidth: CGFloat = 30
path.move(to: .init(x: lineWidth/2, y: lineWidth/2))
path.addLine(to: .init(x: rect.width/2, y: rect.height - lineWidth/2))
path.addLine(to: .init(x: rect.width - lineWidth/2, y: lineWidth/2))
path.closeSubpath()
shapeLayer.path = path
shapeLayer.lineWidth = lineWidth
}
CustomView.init 是,
init(){
super.init(frame: .zero)
wantsLayer = true
layer?.borderWidth = 1.0
layer?.borderColor = NSColor.red.cgColor
layer?.masksToBounds = false
layer!.addSublayer(shapeLayer)
shapeLayer.strokeColor = NSColor.lightGray.cgColor
shapeLayer.fillColor = .white
}
输出:
但是还是画不对
更新:
修改了路径中的第一行...所以整个路径将是,
let path = CGMutablePath()
let lineWidth: CGFloat = 30
path.move(to: .init(x: lineWidth/2, y: lineWidth/2))
path.addLine(to: .init(x: rect.width/2, y: rect.height - lineWidth))
path.addLine(to: .init(x: rect.width - lineWidth/2, y: lineWidth/2))
path.closeSubpath()
输出:
现在,我对修改第二行感到困惑。我不知道如何解决它。请hint/help我来解决这个问题。提前致谢...
edit/update:
将线宽乘以 2 并为形状添加遮罩会更容易:
class Triangle: NSView {
let shapeLayer = CAShapeLayer()
var lineWidth: CGFloat
var strokeColor: NSColor = .clear
var fillColor: NSColor = .clear
init(size: CGSize, lineWidth: CGFloat = 10, strokeColor: NSColor = .white, fillColor: NSColor = .black) {
self.lineWidth = lineWidth*2
self.strokeColor = strokeColor
self.fillColor = fillColor
super.init(frame: .init(origin: .zero, size: size))
wantsLayer = true
let path = NSBezierPath()
path.move(to: .zero)
path.line(to: .init(x: size.width/2 , y: size.height))
path.line(to: .init(x: size.width, y: .zero))
path.close()
let mask = CAShapeLayer()
mask.path = path.cgPath
shapeLayer.mask = mask
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = self.lineWidth
shapeLayer.strokeColor = strokeColor.cgColor
shapeLayer.fillColor = fillColor.cgColor
// layer?.borderWidth = 1
// layer?.borderColor = NSColor.red.cgColor
layer?.addSublayer(shapeLayer)
}
convenience init(width: CGFloat, height: CGFloat, lineWidth: CGFloat = 10, strokeColor: NSColor = .white, fillColor: NSColor = .black) {
self.init(size: .init(width: width, height: height), lineWidth: lineWidth, strokeColor: strokeColor, fillColor: fillColor)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
extension NSBezierPath {
var cgPath: CGPath {
let path = CGMutablePath()
var points: [CGPoint] = .init(repeating: .zero, count: 3)
for i in 0..<elementCount {
switch element(at: i, associatedPoints: &points) {
case .moveTo: path.move(to: points[0])
case .lineTo: path.addLine(to: points[0])
case .curveTo: path.addCurve(to: points[2], control1: points[0], control2: points[1])
case .closePath: path.closeSubpath()
@unknown default: fatalError()
}
}
return path
}
}
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let triangle = Triangle(width: 200, height: 200, lineWidth: 1)
view.addSubview(triangle)
}
}
我并没有真正开发 iOS 应用程序。但请看下面
// View controller //
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = CGRect(origin: CGPoint.zero, size: CGSize(width: 200, height: 200))
let triangleView = TriangleView(frame: rect, backColor: UIColor.green, strokeColor: UIColor.blue, lineWidth: 10)
view.addSubview(triangleView)
}
}
// Subclass of UIView //
import UIKit
class TriangleView: UIView {
var backColor: UIColor
var strokeColor: UIColor
var lineWidth: CGFloat
init(frame: CGRect, backColor: UIColor, strokeColor: UIColor, lineWidth: CGFloat) {
self.backColor = backColor
self.strokeColor = strokeColor
self.lineWidth = lineWidth
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: CGRect) {
super.draw(rect)
backColor.set()
//UIRectFill(rect)
let shapeLayer = CAShapeLayer()
let path = UIBezierPath()
path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.addLine(to: CGPoint(x: lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.addLine(to: CGPoint(x: rect.width - lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.addLine(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.close()
shapeLayer.path = path.cgPath
shapeLayer.lineWidth = lineWidth
shapeLayer.fillColor = backColor.cgColor
shapeLayer.strokeColor = strokeColor.cgColor
layer.insertSublayer(shapeLayer, at: 0)
layer.masksToBounds = false
}
}
以下为Cocoa版本
// View controller //
import Cocoa
class ViewController: NSViewController {
override func viewDidLoad() {
super.viewDidLoad()
let rect = CGRect(origin: CGPoint(x: 50, y: 50), size: CGSize(width: 200, height: 200))
let myView = MyView(frame: rect, fillColor: NSColor.green, strokeColor: NSColor.red, lineWidth: 10.0)
myView.wantsLayer = true
view.addSubview(myView)
}
}
// Subclass of NSView //
import Cocoa
class MyView: NSView {
override var isFlipped: Bool { return true }
var fillColor: NSColor
var strokeColor: NSColor
var lineWidth: CGFloat
init(frame: CGRect, fillColor: NSColor, strokeColor: NSColor, lineWidth: CGFloat){
self.fillColor = fillColor
self.strokeColor = strokeColor
self.lineWidth = lineWidth
super.init(frame: frame)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(_ rect: NSRect) {
super.draw(rect)
let path = NSBezierPath()
path.move(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.line(to: CGPoint(x: lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.line(to: CGPoint(x: rect.width - lineWidth/2.0, y: rect.height - lineWidth/2.0))
path.line(to: CGPoint(x: rect.width/2.0, y: lineWidth/2.0))
path.close()
fillColor.setFill()
path.fill()
path.lineWidth = lineWidth
strokeColor.set()
path.stroke()
}
}
“带掩码的双线宽”实现的另一种变体:
class TriangleView: NSView {
let lineWidth: CGFloat = 20
private lazy var shapeLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = NSColor.clear.cgColor
shapeLayer.strokeColor = NSColor.red.cgColor
shapeLayer.lineWidth = lineWidth * 2
return shapeLayer
}()
private let maskLayer: CAShapeLayer = {
let shapeLayer = CAShapeLayer()
shapeLayer.fillColor = NSColor.white.cgColor
shapeLayer.strokeColor = NSColor.clear.cgColor
shapeLayer.lineWidth = 0
return shapeLayer
}()
override init(frame frameRect: NSRect = .zero) {
super.init(frame: frameRect)
configure()
}
required init?(coder: NSCoder) {
super.init(coder: coder)
configure()
}
func configure() {
wantsLayer = true
layer?.borderColor = NSColor.blue.cgColor
layer?.borderWidth = 1
layer?.addSublayer(shapeLayer)
}
override func layout() {
super.layout()
let path = CGMutablePath()
path.move(to: NSPoint(x: bounds.midX, y: bounds.maxY))
path.addLine(to: NSPoint(x: bounds.maxX, y: bounds.minY))
path.addLine(to: NSPoint(x: bounds.minX, y: bounds.minY))
path.closeSubpath()
shapeLayer.path = path
maskLayer.path = path
shapeLayer.mask = maskLayer
}
}
概念与接受的答案相同(重新加倍线宽和掩码)。
如您所见,当使用CAShapeLayer
时,我们没有实现draw
, but rather let the shape layer take care of the rendering. But we do want to respond to frame changes, so we set (and reset) the path in layout
。
无论如何,结果是: