如何屏蔽和擦除 CAShapeLayer?

How do I mask and erase a CAShapeLayer?

我正在尝试让擦除功能与 CAShapeLayer 一起使用。当前代码:

class MaskTestVC: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()

        view.backgroundColor = .white
        let image = UIImage(named: "test.jpg")!

        let testLayer = CAShapeLayer()
        testLayer.contents = image.cgImage
        testLayer.frame = view.bounds

        let maskLayer = CAShapeLayer()
        maskLayer.opacity = 1.0
        maskLayer.lineWidth = 20.0
        maskLayer.strokeColor = UIColor.black.cgColor
        maskLayer.fillColor = UIColor.clear.cgColor
        maskLayer.frame = view.bounds
        testLayer.mask = maskLayer

        view.layer.addSublayer(testLayer)
    }

}

我在想如果我创建一个遮罩层然后我可以在遮罩层上画一条路径并擦除部分图像例如:

let path = UIBezierPath()
path.move(to: CGPoint(x: 100.0, y: 100.0))
path.addLine(to: CGPoint(x: 200.0, y: 200.0))
maskLayer.path = path.cgPath

然而,当我添加 maskLayer 时,它似乎覆盖了整个图像,我看不到它下面的内容。我在这里做错了什么?

maskLayer 没有初始路径因此其内容未填充,同样用.clear 填充将完全遮住图像。

这对我有用:

override func viewDidLoad() {
   super.viewDidLoad()
        
   view.backgroundColor = .white
   let image = UIImage(named: "test.jpg")!
       
   let testLayer = CAShapeLayer()
   testLayer.contents = image.cgImage
   testLayer.frame = view.bounds
        
   let maskLayer = CAShapeLayer()
   maskLayer.strokeColor = UIColor.black.cgColor
   maskLayer.fillColor = UIColor.black.cgColor
   maskLayer.path = UIBezierPath(rect: view.bounds).cgPath
   testLayer.mask = maskLayer
        
   view.layer.addSublayer(testLayer)
     
   /* optional for testing   
   let path = UIBezierPath()
   path.move(to: CGPoint(x: 100.0, y: 100.0))
   path.addLine(to: CGPoint(x: 200.0, y: 200.0))
   maskLayer.path = path.cgPath
   */
}

不确定笔画和填充的组合是否有效,但也许其他人提出了解决方案。如果你想从你的路径中切出形状,你可以尝试这样的事情:

override func viewDidLoad() {
   super.viewDidLoad()
        
   view.backgroundColor = .white
   let image = UIImage(named: "test.jpg")!
        
   let testLayer = CAShapeLayer()
   testLayer.contents = image.cgImage
   testLayer.frame = view.bounds
        
   let maskLayer = CAShapeLayer()
   maskLayer.fillColor = UIColor.black.cgColor
   maskLayer.fillRule = .evenOdd
   testLayer.mask = maskLayer
        
   view.layer.addSublayer(testLayer)
        
   let path = UIBezierPath(rect: CGRect(x: 100, y: 100, width: 200, height: 20))
   let fillPath = UIBezierPath(rect: view.bounds)
   fillPath.append(path)
   maskLayer.path = fillPath.cgPath
}

您可以使用贝塞尔路径掩码通过创建自定义 CALayer 子类并覆盖 draw(in ctx: CGContext):

来“擦除”路径
class MyCustomLayer: CALayer {
    
    var myPath: CGPath?
    var lineWidth: CGFloat = 24.0

    override func draw(in ctx: CGContext) {

        // fill entire layer with solid color
        ctx.setFillColor(UIColor.gray.cgColor)
        ctx.fill(self.bounds);

        // we want to "clear" the stroke
        ctx.setStrokeColor(UIColor.clear.cgColor);
        // any color will work, as the mask uses the alpha value
        ctx.setFillColor(UIColor.white.cgColor)
        ctx.setLineWidth(self.lineWidth)
        ctx.setLineCap(.round)
        ctx.setLineJoin(.round)
        if let pth = self.myPath {
            ctx.addPath(pth)
        }
        ctx.setBlendMode(.sourceIn)
        ctx.drawPath(using: .fillStroke)

    }
    
}

这是一个完整的示例...我们将创建一个 UIView 子类,由于效果是“刮掉图像”,我们将其命名为 ScratchOffImageView:

class ScratchOffImageView: UIView {

    public var image: UIImage? {
        didSet {
            self.scratchOffImageLayer.contents = image?.cgImage
        }
    }

    // adjust drawing-line-width as desired
    //  or set from
    public var lineWidth: CGFloat = 24.0 {
        didSet {
            maskLayer.lineWidth = lineWidth
        }
    }

    private class MyCustomLayer: CALayer {
        
        var myPath: CGPath?
        var lineWidth: CGFloat = 24.0

        override func draw(in ctx: CGContext) {

            // fill entire layer with solid color
            ctx.setFillColor(UIColor.gray.cgColor)
            ctx.fill(self.bounds);

            // we want to "clear" the stroke
            ctx.setStrokeColor(UIColor.clear.cgColor);
            // any color will work, as the mask uses the alpha value
            ctx.setFillColor(UIColor.white.cgColor)
            ctx.setLineWidth(self.lineWidth)
            ctx.setLineCap(.round)
            ctx.setLineJoin(.round)
            if let pth = self.myPath {
                ctx.addPath(pth)
            }
            ctx.setBlendMode(.sourceIn)
            ctx.drawPath(using: .fillStroke)

        }
        
    }

    private let maskPath: UIBezierPath = UIBezierPath()
    private let maskLayer: MyCustomLayer = MyCustomLayer()
    private let scratchOffImageLayer: CALayer = CALayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {

        // Important, otherwise you will get a black rectangle
        maskLayer.isOpaque = false
        
        // add the image layer
        layer.addSublayer(scratchOffImageLayer)
        // assign the layer mask
        scratchOffImageLayer.mask = maskLayer
        
    }
    
    override func layoutSubviews() {
        super.layoutSubviews()

        // set frames for mask and image layers
        maskLayer.frame = bounds
        scratchOffImageLayer.frame = bounds

        // triggers drawInContext
        maskLayer.setNeedsDisplay()
    }

    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let currentPoint = touch.location(in: self)
        maskPath.move(to: currentPoint)
    }
    
    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let currentPoint = touch.location(in: self)
        // add line to our maskPath
        maskPath.addLine(to: currentPoint)
        // update the mask layer path
        maskLayer.myPath = maskPath.cgPath
        // triggers drawInContext
        maskLayer.setNeedsDisplay()
    }
    
}

以及一个示例视图控制器:

class ScratchOffViewController: UIViewController {
    
    override func viewDidLoad() {
        super.viewDidLoad()
        
        view.backgroundColor = .systemBackground
        
        guard let img = UIImage(named: "test.jpg") else {
            fatalError("Could not load image!!!!")
        }

        let scratchOffView = ScratchOffImageView()
        
        // set the "scratch-off" image
        scratchOffView.image = img
        
        // default line width is 24.0
        //  we can set it to a different width here
        //scratchOffView.lineWidth = 12

        // let's add a light-gray label with red text
        //  we'll overlay the scratch-off-view on top of the label
        //  so we can see the text "through" the image
        let backgroundLabel = UILabel()
        backgroundLabel.font = .italicSystemFont(ofSize: 36)
        backgroundLabel.text = "This is some text in a label so we can see that the path is clear -- so it appears as if the image is being \"scratched off\""
        backgroundLabel.numberOfLines = 0
        backgroundLabel.textColor = .red
        backgroundLabel.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        
        [backgroundLabel, scratchOffView].forEach { v in
            v.translatesAutoresizingMaskIntoConstraints = false
            view.addSubview(v)
        }
        
        let g = view.safeAreaLayoutGuide
        NSLayoutConstraint.activate([
            backgroundLabel.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.7),
            backgroundLabel.centerXAnchor.constraint(equalTo: g.centerXAnchor),
            backgroundLabel.centerYAnchor.constraint(equalTo: g.centerYAnchor),
            
            scratchOffView.widthAnchor.constraint(equalTo: g.widthAnchor, multiplier: 0.8),
            scratchOffView.heightAnchor.constraint(equalTo: scratchOffView.widthAnchor, multiplier: 2.0 / 3.0),
            scratchOffView.centerXAnchor.constraint(equalTo: backgroundLabel.centerXAnchor),
            scratchOffView.centerYAnchor.constraint(equalTo: backgroundLabel.centerYAnchor),
        ])
        
    }

}

它看起来像这样开始 - 我使用了 3:2 图像,并将其覆盖在带有红色文本的 light-gray 标签上,这样我们就可以看到我们正在“刮掉”图像:

然后,经过一点“挠”:

经过大量的“抓挠”: