如何在Swift中使用CAShapeLayer为CALayer.SubLayer创建橡皮擦 [英] How to create an Eraser for CALayer.SubLayer Using a CAShapeLayer in Swift

查看:266
本文介绍了如何在Swift中使用CAShapeLayer为CALayer.SubLayer创建橡皮擦的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我一直在到处寻找一个已经被问过很多次的问题的答案.我花了几个小时浏览SO和Google.必须有一个不会费力的答案.

我正在研究矢量绘图应用程序,终于使绘图和撤消功能正常工作了.现在我需要一个橡皮擦:-o

编辑:根据@DonMag的出色记录,我能够接近橡皮擦,但是仍然有些不正确.因此,我将尝试解释我的视图和图层在应用程序中的状态以及为什么这样做:

从底部视图/层到顶部...

  1. BackgroundImageView-我正在使用此图像视图来保存绘图表面的背景".它是可以更改的层,可以保存新的模板"并调入其中.我将其分开,以使用户无法擦除绘图表面.并且背景由CAShapeLayers组成,它们被绘制为代表不同的纸张类型.

  2. MainImageView-我正在使用此图像视图来执行用户启动的所有绘图.因此,我触摸并拖动手指,然后将新的CAShapeLayer添加到图像视图中.这使用户的绘图与绘图表面"分开.这也是我要擦除的地方

  3. PageImagesView-我使用此视图保存用户可以添加到页面的图像,并对其进行移动/调整大小.我不希望橡皮擦影响图像,但是如果MainImageView中绘制的线条越过图像并且需要擦除,则应该让图像显示出来,而不是去除图像的一部分.

我还添加了另一层尝试使橡皮擦正常工作,并将其命名为"EraserImageView",并在其中绘制了蒙版",然后尝试将该蒙版应用于MainImageView.

这是我的绘图代码,每次touchesMoved调用时都会调用:

将橡皮擦代码添加到我的绘图代码中.

  if eraser {
            let linePath = UIBezierPath()

            for (index, point) in line.enumerated() {
                if index == 0 {
                    midPoint = CGPoint(
                        x: (point.x + point.x) / 2,
                        y: (point.y + point.y) / 2
                    )
                    linePath.move(to: midPoint!)
                } else {
                    midPoint = CGPoint(
                        x: (point.x + line[index - 1].x) / 2,
                        y: (point.y + line[index - 1].y) / 2
                    )
                    linePath.addQuadCurve(to: midPoint!, controlPoint: line[index - 1])
                }
            }

            let maskLayer = CAShapeLayer()
            maskLayer.lineWidth = brush
            maskLayer.lineCap = .round
            maskLayer.strokeColor = UIColor.black.cgColor
            maskLayer.fillColor = nil
            maskLayer.frame = backgroundImageView.bounds
            maskLayer.path = linePath.cgPath
            //eraserImageView.layer.addSublayer(backgroundImageView.layer)
            eraserImageView.layer.addSublayer(maskLayer)
            eraserImageView.layer.mask = mainImageView.layer            
        }
 

上面的代码使所有用户图形消失,但被橡皮擦"触摸的部分除外.我知道我的东西出故障了,或者我使用的口罩不正确.有人有解决方案吗?

画一些线,看起来很棒...

[

当我尝试橡皮擦时,会发生这种情况...

正如您在上面看到的,我可以画线,但是一旦我将橡皮擦触摸到页面上,它将删除所有与橡皮擦接触的部分.

有人知道我要去哪里吗?

修改: 关闭! 当我移动手指时,我能够使橡皮擦去除部分画出的线条.但是它不是使用大小"进行绘制,而是在制作形状.使用橡皮擦后,只要我触摸绘图表面,它也将替换所有已擦除"的零件.

这是我的新橡皮擦代码:

if eraser {
            //var rect: CGRect = CGRect()
            let linePath = UIBezierPath(rect: mainImageView.bounds)

            for (index, point) in line.enumerated() {
                if index == 0 {
                    midPoint = CGPoint(
                        x: (point.x + point.x) / 2,
                        y: (point.y + point.y) / 2
                    )
                    //rect = CGRect(x: midPoint!.x, y: midPoint!.y, width: brush, height: brush)
                    linePath.move(to: midPoint!)
                } else {
                    midPoint = CGPoint(
                        x: (point.x + line[index - 1].x) / 2,
                        y: (point.y + line[index - 1].y) / 2
                    )
                    //rect = CGRect(x: midPoint!.x, y: midPoint!.y, width: brush, height: brush)
                    linePath.addQuadCurve(to: midPoint!, controlPoint: line[index - 1])
                }
            }

            let maskLayer = CAShapeLayer()
            maskLayer.lineWidth = brush
            maskLayer.lineCap = .round
            maskLayer.strokeColor = UIColor.clear.cgColor
            maskLayer.fillColor = UIColor.black.cgColor
            maskLayer.opacity = 1.0
            maskLayer.path = linePath.cgPath
            maskLayer.fillRule = .evenOdd
            mainImageView.layer.addSublayer(maskLayer)
            mainImageView.layer.mask = maskLayer

        }

结果如下:

关于如何使橡皮像线条一样绘制的任何想法?

应@DonMag的要求为背景绘图"添加代码

import Foundation
import UIKit

class DrawBulletLayer : UIView {

    private var bullet: CAShapeLayer?

    func drawBullets(coordinates: UIImageView, bulletColor: UIColor) -> CALayer {
        let bullet = self.bullet ?? CAShapeLayer()
        let bulletPath = UIBezierPath()

        bullet.contentsScale = UIScreen.main.scale

        var bullets: [CGPoint] = []
        let width = coordinates.frame.width
        let height = coordinates.frame.height

        let widthBullets = CGFloat(width / 55)
        let heightBullets = CGFloat(height / 39)

        var hb: CGFloat?
        var wb: CGFloat?

        for n in 1...39 {
            hb = heightBullets * CGFloat(n)
            for o in 1...55 {
                wb = widthBullets * CGFloat(o)
                bullets.append(CGPoint(x: wb!, y: hb!))
            }
        }

        UIColor.black.setStroke()

        bullets.forEach { point in
            bulletPath.move(to: point)
            bulletPath.addLine(to: point)
        }

        bullet.path = bulletPath.cgPath
        bullet.opacity = 1.0
        bullet.lineWidth = 2.0
        bullet.lineCap = .round
        bullet.fillColor = UIColor.clear.cgColor
        bullet.strokeColor = bulletColor.cgColor

        if self.bullet == nil {
            self.bullet = bullet
            layer.addSublayer(bullet)
        }

        return layer
    }
}

以下是将其添加到BackgroundImageView中的方法:

func updateTemplate() {
        let templates = TemplatePickerData()
        var loadLayer = templates.loadTemplateIds()
        if loadLayer.count == 0 {
            _ = templates.loadTemplates()
            loadLayer = templates.loadTemplateIds()
        }
        print("this is the template ID: \(templateId)")
        //let templateId = loadLayer[template].value(forKey: "templateId") as! Int
        if template < 0 {
            template = 0
        }

        switch template {
        case 0:
            //scrollView.image = UIImage(named: "habitTracker0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawBullets.drawBullets(coordinates: scrollView.backgroundImageView, bulletColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        case 1:
            //scrollView.image = UIImage(named: "monthTemplate0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawNotes.drawLines(coordinates: scrollView.backgroundImageView, lineColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        case 2:
            //scrollView.image = UIImage(named: "habitTracker0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawNotes2.drawLines(coordinates: scrollView.backgroundImageView, lineColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        default:
            if loadLayer.count > template {
                template = 0
            }
            print("this layer is named: \(loadLayer[template].value(forKey: "templateName") as! String)")
            let layer = loadLayer[template].value(forKey: "templatePath") as! String
            templateId = loadLayer[template].value(forKey: "templateId") as! Int
            let thisTemplate = templates.loadImage(image: layer)

            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawBullets.drawBullets(coordinates: scrollView.backgroundImageView, bulletColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.backgroundImageView.layer.addSublayer(thisTemplate)
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        }
        scrollView.setNeedsDisplay()

        if optionsMenuView.pageNameTextField.text != "" {
            if isYear {
                page = optionsMenuView.savePage(journalName: journalName, monthName: nil, weekName: nil, yearName: yearName, yearPosition: yearPosition, pageDrawingPath: pageDrawingPath, originalName: originalYearName, brushColor: 1, brushSize: brushSizeMenuView.brushSlider.value, templateId: templateId, pageDrawing: scrollView.mainImageView.layer)
            } else {
                page = optionsMenuView.savePage(journalName: journalName, monthName: monthName, weekName: weekName, yearName: nil, yearPosition: nil, pageDrawingPath: pageDrawingPath, originalName: originalWeekName, brushColor: 1, brushSize: brushSizeMenuView.brushSlider.value, templateId: templateId, pageDrawing: scrollView.mainImageView.layer)
            }
        }
        optionsMenuView.templateId = templateId
    }

希望有更多帮助...

贝塞尔曲线路径的

解决方案

擦除部分非常棘手...您可能需要计算相交(笔划宽度,不仅是路径本身),还可以将现有的线分成多个段.

这是另一种方法-不确定是否适合您,但可能值得考虑:

绘图"层可能已经存在. 橡皮擦"层将包含背景图像,然后将线"(贝塞尔曲线路径)用作遮罩,因此它将出现以擦除下面的层的一部分.

最后一行为黄色的绘图"层:

,最后一行作为橡皮擦"层:

这是我用于此的代码.我认为这很简单.没有实际的绘图"功能-它仅使用一组硬编码的坐标和属性,就像它们是通过触摸跟踪生成的一样.

运行它时,顶部的按钮将添加红色,绿色和蓝色线",然后在黄色线"和橡皮擦线"之间切换最后一组点.

//
//  ViewController.swift
//  VectorDrawTest
//
//  Created by Don Mag on 8/8/19.
//

import UIKit

enum LineType: Int {
    case DRAW
    case ERASE
}

class LineDef: NSObject {
    var lineType: LineType = .DRAW
    var color: UIColor = UIColor.black
    var opacity: Float = 1.0
    var lineWidth: CGFloat = 8.0
    var points: [CGPoint] = [CGPoint]()
}

class DrawingView: UIView {

    // the background image
    var bkgImage: UIImage = UIImage() {
        didSet {
            updateBkgImage()
        }
    }

    func updateBkgImage() -> Void {
        // if no layers have been added yet, add the background image layer
        if layer.sublayers == nil {
            let l = CALayer()
            layer.addSublayer(l)
        }
        guard let layers = layer.sublayers else { return }
        for l in layers {
            if let _ = l as? CAShapeLayer {
                // in case we're changing the backgound image after lines have been drawn
                // ignore shape layers
            } else {
                // this layer is NOT a CAShapeLayer, so it's either the first (background image) layer
                // or it's an eraser layer, so update the contents
                l.contents = bkgImage.cgImage
            }
        }
        setNeedsDisplay()
    }

    func undo() -> Void {
        // only remove a layer if it's not the first (background image) layer
        guard let n = layer.sublayers?.count, n > 1 else { return }
        _ = layer.sublayers?.popLast()
    }

    func addLineDef(_ def: LineDef) -> Void {

        if def.lineType == LineType.DRAW {

            // create new shape layer
            let newLayer = CAShapeLayer()

            // set "draw" properties
            newLayer.lineCap = .round
            newLayer.lineWidth = def.lineWidth
            newLayer.opacity = def.opacity
            newLayer.strokeColor = def.color.cgColor
            newLayer.fillColor = UIColor.clear.cgColor

            // create bezier path from LineDef points
            let drawPts = def.points
            let bez = UIBezierPath()
            for pt in drawPts {
                if pt == drawPts.first {
                    bez.move(to: pt)
                } else {
                    bez.addLine(to: pt)
                }
            }
            // set path
            newLayer.path = bez.cgPath

            // add layer
            layer.addSublayer(newLayer)

        } else {

            // create new layer
            let newLayer = CALayer()
            // set its contents to the background image
            newLayer.contents = bkgImage.cgImage
            newLayer.opacity = def.opacity

            // create a shape layer to use as a mask
            let maskLayer = CAShapeLayer()

            // set "draw" properties
            // strokeColor will always be black, because it just uses alpha for the mask
            maskLayer.lineCap = .round
            maskLayer.lineWidth = def.lineWidth
            maskLayer.strokeColor = UIColor.black.cgColor
            maskLayer.fillColor = UIColor.clear.cgColor

            // add mask
            newLayer.mask = maskLayer

            // create bezier path from LineDef points
            let drawPts = def.points
            let bez = UIBezierPath()
            for pt in drawPts {
                if pt == drawPts.first {
                    bez.move(to: pt)
                } else {
                    bez.addLine(to: pt)
                }
            }
            // set maskLayer's path
            maskLayer.path = bez.cgPath

            // add layer
            layer.addSublayer(newLayer)

        }

        setNeedsDisplay()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // update layer frames
        if let layers = layer.sublayers {
            for l in layers {
                l.frame = bounds
            }
        }
    }

}


class DrawViewController: UIViewController {

    let theDrawingView: DrawingView = {
        let v = DrawingView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    let demoButton: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        v.setTitleColor(.blue, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitle("Draw Red", for: .normal)
        return v
    }()

    let redLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .red
        d.lineWidth = 8.0
        d.points = [
            CGPoint(x: 20, y: 20),
            CGPoint(x: 40, y: 140),
            CGPoint(x: 280, y: 200),
        ]
        return d
    }()

    let greenLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .green
        d.lineWidth = 16.0
        d.points = [
            CGPoint(x: 20, y: 100),
            CGPoint(x: 80, y: 80),
            CGPoint(x: 240, y: 140),
            CGPoint(x: 100, y: 200),
        ]
        return d
    }()

    let blueLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .blue
        d.opacity = 0.5
        d.lineWidth = 24.0
        d.points = [
            CGPoint(x: 250, y: 20),
            CGPoint(x: 150, y: 240),
            CGPoint(x: 100, y: 60),
        ]
        return d
    }()

    let yellowLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .yellow
        d.lineWidth = 32.0
        d.points = [
            CGPoint(x: 30, y: 200),
            CGPoint(x: 250, y: 80),
            CGPoint(x: 250, y: 180),
        ]
        return d
    }()

    let eraserLine: LineDef = {
        let d = LineDef()
        d.lineType = .ERASE
        d.lineWidth = 32.0
        d.points = [
            CGPoint(x: 30, y: 200),
            CGPoint(x: 250, y: 80),
            CGPoint(x: 250, y: 180),
        ]
        return d
    }()

    var testErase = false

    override func viewDidLoad() {
        super.viewDidLoad()

        // add the drawing view
        view.addSubview(theDrawingView)

        // constrain it 300 x 300 centered X and Y
        NSLayoutConstraint.activate([
            theDrawingView.widthAnchor.constraint(equalToConstant: 300),
            theDrawingView.heightAnchor.constraint(equalToConstant: 300),
            theDrawingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            theDrawingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])

        let imgName = "TheCat"
        if let img = UIImage(named: imgName) {
            theDrawingView.bkgImage = img
        }

        // add a demo button
        view.addSubview(demoButton)

        // constrain it 20-pts from the top, centered X
        NSLayoutConstraint.activate([
            demoButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
            demoButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            demoButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            ])

        // add the touchUpInside target
        demoButton.addTarget(self, action: #selector(doTest), for: .touchUpInside)
    }

    @objc func doTest(_ sender: Any?) -> Void {

        if let b = sender as? UIButton {

            let t = b.currentTitle

            switch t {
            case "Draw Red":
                theDrawingView.addLineDef(redLine)
                b.setTitle("Draw Green", for: .normal)
            case "Draw Green":
                theDrawingView.addLineDef(greenLine)
                b.setTitle("Draw Blue", for: .normal)
            case "Draw Blue":
                theDrawingView.addLineDef(blueLine)
                b.setTitle("Draw Yellow", for: .normal)
            case "Draw Yellow":
                theDrawingView.addLineDef(yellowLine)
                b.setTitle("Toggle Yellow / Erase", for: .normal)
            default:
                toggle()
            }

        }
    }

    func toggle() -> Void {

        // undo the last action
        theDrawingView.undo()

        // toggle bool var
        testErase = !testErase

        // add either yellowLine or eraserLine
        theDrawingView.addLineDef(testErase ? eraserLine : yellowLine)

    }

}

一切都通过代码完成-没有@IBOutlets@IBActions-因此,只需启动一个新项目,并用上述代码替换ViewController.swift.

I have been looking everywhere for the answer to a question that has been asked a ton of times. I have spent hours looking through SO and Google. There has to be an answer that isn't going to take a mountain moving effort.

I am working on a vector drawing app and finally got the drawing and undo-ing functionality working. Now I need an eraser :-o

EDIT: Per the great write up from @DonMag I was able to get pretty close to an eraser, but something still isn't quite right. So I am going to try and explain how my views and layers are in the app and why I have done it this way:

Starting from the bottom view/layer to the top...

  1. BackgroundImageView - I am using this image view to hold the "background" for the drawing surface. It is a layer that can be changed and can have new "templates" saved and recalled into. I keep it separate so that the user can't erase the drawing surface. And the background consists of CAShapeLayers that are drawn to represent different paper types.

  2. MainImageView - I am using this image view to do all the drawing that the user initiates. So I touch and drag my finger, and new CAShapeLayer is added to the image view. This keeps the user's drawing separate from the "drawing surface". This is also the place I want the erasing to happen

  3. PageImagesView - I use this view to hold images that the user can add to the page, and move/resize them. I don't want the eraser to effect the image, but if a line drawn in MainImageView crosses over the image and needs to be erased it should let the image show through, and not remove parts of the image.

I also added another layer trying to get the eraser working, and called it "EraserImageView", and was drawing the "mask" into it, then trying to apply that mask to the MainImageView.

Here is my drawing code, called each time touchesMoved is called:

EDIT: Adding the code for eraser into my Drawing code.

 if eraser {
            let linePath = UIBezierPath()

            for (index, point) in line.enumerated() {
                if index == 0 {
                    midPoint = CGPoint(
                        x: (point.x + point.x) / 2,
                        y: (point.y + point.y) / 2
                    )
                    linePath.move(to: midPoint!)
                } else {
                    midPoint = CGPoint(
                        x: (point.x + line[index - 1].x) / 2,
                        y: (point.y + line[index - 1].y) / 2
                    )
                    linePath.addQuadCurve(to: midPoint!, controlPoint: line[index - 1])
                }
            }

            let maskLayer = CAShapeLayer()
            maskLayer.lineWidth = brush
            maskLayer.lineCap = .round
            maskLayer.strokeColor = UIColor.black.cgColor
            maskLayer.fillColor = nil
            maskLayer.frame = backgroundImageView.bounds
            maskLayer.path = linePath.cgPath
            //eraserImageView.layer.addSublayer(backgroundImageView.layer)
            eraserImageView.layer.addSublayer(maskLayer)
            eraserImageView.layer.mask = mainImageView.layer            
        }

The code above causes all of the user drawing to disappear except the portion that is touched by the "eraser". I know that I have something out of order, or I'm applying the mask incorrectly. Does anyone have a solution?

Drawing some Lines, and it looks great...

[

When I attempt the eraser this is what happens...

As you can see above I can draw lines, but once I touch the eraser to the page it removes everything except for the part I touch with the eraser.

Does anyone know where I am going wrong??

Edit: SO CLOSE! I was able to get the eraser to remove part of the drawn line when I move my finger. But it isn't drawing using the Sizes and it is making shapes. It is also replacing all the "erased" parts as soon as I touch the drawing surface after using the eraser.

Here is my new eraser code:

if eraser {
            //var rect: CGRect = CGRect()
            let linePath = UIBezierPath(rect: mainImageView.bounds)

            for (index, point) in line.enumerated() {
                if index == 0 {
                    midPoint = CGPoint(
                        x: (point.x + point.x) / 2,
                        y: (point.y + point.y) / 2
                    )
                    //rect = CGRect(x: midPoint!.x, y: midPoint!.y, width: brush, height: brush)
                    linePath.move(to: midPoint!)
                } else {
                    midPoint = CGPoint(
                        x: (point.x + line[index - 1].x) / 2,
                        y: (point.y + line[index - 1].y) / 2
                    )
                    //rect = CGRect(x: midPoint!.x, y: midPoint!.y, width: brush, height: brush)
                    linePath.addQuadCurve(to: midPoint!, controlPoint: line[index - 1])
                }
            }

            let maskLayer = CAShapeLayer()
            maskLayer.lineWidth = brush
            maskLayer.lineCap = .round
            maskLayer.strokeColor = UIColor.clear.cgColor
            maskLayer.fillColor = UIColor.black.cgColor
            maskLayer.opacity = 1.0
            maskLayer.path = linePath.cgPath
            maskLayer.fillRule = .evenOdd
            mainImageView.layer.addSublayer(maskLayer)
            mainImageView.layer.mask = maskLayer

        }

Here is the result:

Any ideas on how to get the eraser to draw just like the lines?

EDIT: Adding the code for the background "drawing" at the request of @DonMag

import Foundation
import UIKit

class DrawBulletLayer : UIView {

    private var bullet: CAShapeLayer?

    func drawBullets(coordinates: UIImageView, bulletColor: UIColor) -> CALayer {
        let bullet = self.bullet ?? CAShapeLayer()
        let bulletPath = UIBezierPath()

        bullet.contentsScale = UIScreen.main.scale

        var bullets: [CGPoint] = []
        let width = coordinates.frame.width
        let height = coordinates.frame.height

        let widthBullets = CGFloat(width / 55)
        let heightBullets = CGFloat(height / 39)

        var hb: CGFloat?
        var wb: CGFloat?

        for n in 1...39 {
            hb = heightBullets * CGFloat(n)
            for o in 1...55 {
                wb = widthBullets * CGFloat(o)
                bullets.append(CGPoint(x: wb!, y: hb!))
            }
        }

        UIColor.black.setStroke()

        bullets.forEach { point in
            bulletPath.move(to: point)
            bulletPath.addLine(to: point)
        }

        bullet.path = bulletPath.cgPath
        bullet.opacity = 1.0
        bullet.lineWidth = 2.0
        bullet.lineCap = .round
        bullet.fillColor = UIColor.clear.cgColor
        bullet.strokeColor = bulletColor.cgColor

        if self.bullet == nil {
            self.bullet = bullet
            layer.addSublayer(bullet)
        }

        return layer
    }
}

Here is how it is added to the BackgroundImageView:

func updateTemplate() {
        let templates = TemplatePickerData()
        var loadLayer = templates.loadTemplateIds()
        if loadLayer.count == 0 {
            _ = templates.loadTemplates()
            loadLayer = templates.loadTemplateIds()
        }
        print("this is the template ID: \(templateId)")
        //let templateId = loadLayer[template].value(forKey: "templateId") as! Int
        if template < 0 {
            template = 0
        }

        switch template {
        case 0:
            //scrollView.image = UIImage(named: "habitTracker0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawBullets.drawBullets(coordinates: scrollView.backgroundImageView, bulletColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        case 1:
            //scrollView.image = UIImage(named: "monthTemplate0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawNotes.drawLines(coordinates: scrollView.backgroundImageView, lineColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        case 2:
            //scrollView.image = UIImage(named: "habitTracker0")!
            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawNotes2.drawLines(coordinates: scrollView.backgroundImageView, lineColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        default:
            if loadLayer.count > template {
                template = 0
            }
            print("this layer is named: \(loadLayer[template].value(forKey: "templateName") as! String)")
            let layer = loadLayer[template].value(forKey: "templatePath") as! String
            templateId = loadLayer[template].value(forKey: "templateId") as! Int
            let thisTemplate = templates.loadImage(image: layer)

            scrollView.backgroundImageView.layer.sublayers?.removeAll()
            scrollView.backgroundImageView.layer.addSublayer(drawBullets.drawBullets(coordinates: scrollView.backgroundImageView, bulletColor: UIColor(red: 214.0/255.0, green: 214.0/255.0, blue: 214.0/255.0, alpha: 1.0)))
            scrollView.backgroundImageView.layer.addSublayer(thisTemplate)
            scrollView.setNeedsLayout()
            scrollView.layoutIfNeeded()
            scrollView.setNeedsDisplay()
        }
        scrollView.setNeedsDisplay()

        if optionsMenuView.pageNameTextField.text != "" {
            if isYear {
                page = optionsMenuView.savePage(journalName: journalName, monthName: nil, weekName: nil, yearName: yearName, yearPosition: yearPosition, pageDrawingPath: pageDrawingPath, originalName: originalYearName, brushColor: 1, brushSize: brushSizeMenuView.brushSlider.value, templateId: templateId, pageDrawing: scrollView.mainImageView.layer)
            } else {
                page = optionsMenuView.savePage(journalName: journalName, monthName: monthName, weekName: weekName, yearName: nil, yearPosition: nil, pageDrawingPath: pageDrawingPath, originalName: originalWeekName, brushColor: 1, brushSize: brushSizeMenuView.brushSlider.value, templateId: templateId, pageDrawing: scrollView.mainImageView.layer)
            }
        }
        optionsMenuView.templateId = templateId
    }

Hope that helps more...

解决方案

Erasing part of a bezier path would be tricky... you'd probably need to calculate intersections (of the stroke width, not just of the path itself) and break existing lines into multiple segments.

Here is another approach - not sure if it will work for you, but might be worth considering:

The "Drawing" layers are probably what you already have. The "Eraser" layer would include the background image, and then the "line" (the bezier path) would be used as a mask, so it would appear to erase portions of the layers below.

With the final line as a yellow "Drawing" layer:

and with the final line as an "Eraser" layer:

Here is the code I used for this. I think it's pretty straight-forward to demonstrate the idea. No actual "drawing" feature -- it just uses a hard-coded set of coordinates and properties as if they had been generated by touch-tracking.

When you run it, the button at the top will add the Red, Green and Blue "lines," and then will toggle the last set of points between a "Yellow line" and an "Eraser line."

//
//  ViewController.swift
//  VectorDrawTest
//
//  Created by Don Mag on 8/8/19.
//

import UIKit

enum LineType: Int {
    case DRAW
    case ERASE
}

class LineDef: NSObject {
    var lineType: LineType = .DRAW
    var color: UIColor = UIColor.black
    var opacity: Float = 1.0
    var lineWidth: CGFloat = 8.0
    var points: [CGPoint] = [CGPoint]()
}

class DrawingView: UIView {

    // the background image
    var bkgImage: UIImage = UIImage() {
        didSet {
            updateBkgImage()
        }
    }

    func updateBkgImage() -> Void {
        // if no layers have been added yet, add the background image layer
        if layer.sublayers == nil {
            let l = CALayer()
            layer.addSublayer(l)
        }
        guard let layers = layer.sublayers else { return }
        for l in layers {
            if let _ = l as? CAShapeLayer {
                // in case we're changing the backgound image after lines have been drawn
                // ignore shape layers
            } else {
                // this layer is NOT a CAShapeLayer, so it's either the first (background image) layer
                // or it's an eraser layer, so update the contents
                l.contents = bkgImage.cgImage
            }
        }
        setNeedsDisplay()
    }

    func undo() -> Void {
        // only remove a layer if it's not the first (background image) layer
        guard let n = layer.sublayers?.count, n > 1 else { return }
        _ = layer.sublayers?.popLast()
    }

    func addLineDef(_ def: LineDef) -> Void {

        if def.lineType == LineType.DRAW {

            // create new shape layer
            let newLayer = CAShapeLayer()

            // set "draw" properties
            newLayer.lineCap = .round
            newLayer.lineWidth = def.lineWidth
            newLayer.opacity = def.opacity
            newLayer.strokeColor = def.color.cgColor
            newLayer.fillColor = UIColor.clear.cgColor

            // create bezier path from LineDef points
            let drawPts = def.points
            let bez = UIBezierPath()
            for pt in drawPts {
                if pt == drawPts.first {
                    bez.move(to: pt)
                } else {
                    bez.addLine(to: pt)
                }
            }
            // set path
            newLayer.path = bez.cgPath

            // add layer
            layer.addSublayer(newLayer)

        } else {

            // create new layer
            let newLayer = CALayer()
            // set its contents to the background image
            newLayer.contents = bkgImage.cgImage
            newLayer.opacity = def.opacity

            // create a shape layer to use as a mask
            let maskLayer = CAShapeLayer()

            // set "draw" properties
            // strokeColor will always be black, because it just uses alpha for the mask
            maskLayer.lineCap = .round
            maskLayer.lineWidth = def.lineWidth
            maskLayer.strokeColor = UIColor.black.cgColor
            maskLayer.fillColor = UIColor.clear.cgColor

            // add mask
            newLayer.mask = maskLayer

            // create bezier path from LineDef points
            let drawPts = def.points
            let bez = UIBezierPath()
            for pt in drawPts {
                if pt == drawPts.first {
                    bez.move(to: pt)
                } else {
                    bez.addLine(to: pt)
                }
            }
            // set maskLayer's path
            maskLayer.path = bez.cgPath

            // add layer
            layer.addSublayer(newLayer)

        }

        setNeedsDisplay()
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // update layer frames
        if let layers = layer.sublayers {
            for l in layers {
                l.frame = bounds
            }
        }
    }

}


class DrawViewController: UIViewController {

    let theDrawingView: DrawingView = {
        let v = DrawingView()
        v.translatesAutoresizingMaskIntoConstraints = false
        return v
    }()

    let demoButton: UIButton = {
        let v = UIButton()
        v.translatesAutoresizingMaskIntoConstraints = false
        v.backgroundColor = UIColor(white: 0.9, alpha: 1.0)
        v.setTitleColor(.blue, for: .normal)
        v.setTitleColor(.lightGray, for: .highlighted)
        v.setTitle("Draw Red", for: .normal)
        return v
    }()

    let redLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .red
        d.lineWidth = 8.0
        d.points = [
            CGPoint(x: 20, y: 20),
            CGPoint(x: 40, y: 140),
            CGPoint(x: 280, y: 200),
        ]
        return d
    }()

    let greenLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .green
        d.lineWidth = 16.0
        d.points = [
            CGPoint(x: 20, y: 100),
            CGPoint(x: 80, y: 80),
            CGPoint(x: 240, y: 140),
            CGPoint(x: 100, y: 200),
        ]
        return d
    }()

    let blueLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .blue
        d.opacity = 0.5
        d.lineWidth = 24.0
        d.points = [
            CGPoint(x: 250, y: 20),
            CGPoint(x: 150, y: 240),
            CGPoint(x: 100, y: 60),
        ]
        return d
    }()

    let yellowLine: LineDef = {
        let d = LineDef()
        d.lineType = .DRAW
        d.color = .yellow
        d.lineWidth = 32.0
        d.points = [
            CGPoint(x: 30, y: 200),
            CGPoint(x: 250, y: 80),
            CGPoint(x: 250, y: 180),
        ]
        return d
    }()

    let eraserLine: LineDef = {
        let d = LineDef()
        d.lineType = .ERASE
        d.lineWidth = 32.0
        d.points = [
            CGPoint(x: 30, y: 200),
            CGPoint(x: 250, y: 80),
            CGPoint(x: 250, y: 180),
        ]
        return d
    }()

    var testErase = false

    override func viewDidLoad() {
        super.viewDidLoad()

        // add the drawing view
        view.addSubview(theDrawingView)

        // constrain it 300 x 300 centered X and Y
        NSLayoutConstraint.activate([
            theDrawingView.widthAnchor.constraint(equalToConstant: 300),
            theDrawingView.heightAnchor.constraint(equalToConstant: 300),
            theDrawingView.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            theDrawingView.centerYAnchor.constraint(equalTo: view.centerYAnchor),
            ])

        let imgName = "TheCat"
        if let img = UIImage(named: imgName) {
            theDrawingView.bkgImage = img
        }

        // add a demo button
        view.addSubview(demoButton)

        // constrain it 20-pts from the top, centered X
        NSLayoutConstraint.activate([
            demoButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 20.0),
            demoButton.widthAnchor.constraint(equalTo: view.widthAnchor, multiplier: 0.8),
            demoButton.centerXAnchor.constraint(equalTo: view.centerXAnchor),
            ])

        // add the touchUpInside target
        demoButton.addTarget(self, action: #selector(doTest), for: .touchUpInside)
    }

    @objc func doTest(_ sender: Any?) -> Void {

        if let b = sender as? UIButton {

            let t = b.currentTitle

            switch t {
            case "Draw Red":
                theDrawingView.addLineDef(redLine)
                b.setTitle("Draw Green", for: .normal)
            case "Draw Green":
                theDrawingView.addLineDef(greenLine)
                b.setTitle("Draw Blue", for: .normal)
            case "Draw Blue":
                theDrawingView.addLineDef(blueLine)
                b.setTitle("Draw Yellow", for: .normal)
            case "Draw Yellow":
                theDrawingView.addLineDef(yellowLine)
                b.setTitle("Toggle Yellow / Erase", for: .normal)
            default:
                toggle()
            }

        }
    }

    func toggle() -> Void {

        // undo the last action
        theDrawingView.undo()

        // toggle bool var
        testErase = !testErase

        // add either yellowLine or eraserLine
        theDrawingView.addLineDef(testErase ? eraserLine : yellowLine)

    }

}

Everything is done via code - no @IBOutlets or @IBActions - so just start a new project and replace ViewController.swift with the above code.

这篇关于如何在Swift中使用CAShapeLayer为CALayer.SubLayer创建橡皮擦的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆