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

无论如何,结果是: