Сделайте UIBezierPath выбираемым и измените его цвет - PullRequest
2 голосов
/ 17 октября 2019

У меня есть UIView, которые соответствуют пользовательскому классу Canvas. Это означает, что пользователь может рисовать в этом UIView.

Каждый раз, когда пользователь заканчивает рисовать, нужно будет нажать кнопку Добавить UIButton, и строка будет добавлена ​​к UITableView ниже.

Каждая строка содержит 2 свойства name: String и scribble: [UInt8]. Свойство scribble будет содержать позиции X и Y для рисунков, связанных с этой строкой.

Когда пользователь выбирает любую строку из этого UITableView, тогда цвет пикселей будет изменен на холсте для этого связанного каракуля. .

Здесь у меня есть демоверсия из версии для Android, и мне нужно сделать что-то похожее: http://g.recordit.co/ZY21ufz5kW.gif

А вот мой прогресс в проекте, но я застрял с логикой добавленияКоординаты X и Y, а также я не знаю, как сделать выбор каракули, чтобы иметь возможность изменить цвет на холсте:

https://github.com/tygruletz/AddScribblesOnImage

Вот мой Canvas класс:

/// A class which allow the user to draw inside a UIView which will inherit this class.
class Canvas: UIView {

    /// Closure to run on changes to drawing state
    var isDrawingHandler: ((Bool) -> Void)?

    /// The image drawn onto the canvas
    var image: UIImage?

    /// Caches the path for a line between touch down and touch up.
    public var path = UIBezierPath()

    /// An array of points that will be smoothed before conversion to a Bezier path
    private var points = Array(repeating: CGPoint.zero, count: 5)

    /// Keeps track of the number of points cached before transforming into a bezier
    private var pointCounter = Int(0)

    /// The colour to use for drawing
    public var strokeColor = UIColor.orange

    /// Width of drawn lines
    //private var strokeWidth = CGFloat(7)

    override func awakeFromNib() {

        isMultipleTouchEnabled = false
        path.lineWidth = 1
        path.lineCapStyle = .round
    }

    // public function
    func clear() {
        image = nil
        setNeedsDisplay()
    }

    override func draw(_ rect: CGRect) {
        // Draw the cached image into the view and then draw the current path onto it
        // This means the entire path is not drawn every time, just the currently smoothed section.
        image?.draw(in: rect)

        strokeColor.setStroke()
        path.stroke()
    }

    private func cacheImage() {

        let renderer = UIGraphicsImageRenderer(bounds: bounds)
        image = renderer.image(actions: { (context) in
            // Since we are not drawing a background color I've commented this out
            // I've left the code in case you want to use it in the future
//            if image == nil {
//                // Nothing cached yet, fill the background
//                let backgroundRect = UIBezierPath(rect: bounds)
//                backgroundColor?.setFill()
//                backgroundRect.fill()
//            }

            image?.draw(at: .zero)
            strokeColor.setStroke()
            path.stroke()
        })
    }
}

// UIResponder methods
extension Canvas {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first ?? UITouch()
        let point = touch.location(in: self)

        pointCounter = 0

        points[pointCounter] = point
        isDrawingHandler?(true)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        let touch = touches.first ?? UITouch()
        let point = touch.location(in: self)
        pointCounter += 1
        points[pointCounter] = point
        guard pointCounter == 4 else {
            // We need 5 points to convert to a smooth Bezier Curve
            return
        }

        // Smooth the curve
        points[3] = CGPoint(x: (points[2].x + points[4].x) / 2.0, y: (points[2].y + points [4].y) / 2.0)

        // Add a new bezier sub-path to the current path
        path.move(to: points[0])
        path.addCurve(to: points[3], controlPoint1: points[1], controlPoint2: points[2])

        // Explicitly shift the points up for the new segment points for the new segment
        points = [points[3], points[4], .zero, .zero, .zero]
        pointCounter = 1
        setNeedsDisplay()
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        cacheImage()
        setNeedsDisplay()
        path.removeAllPoints()
        pointCounter = 0
        isDrawingHandler?(false)
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchesEnded(touches, with: event)
    }
}

Вот мой ViewController класс:

class FirstVC: UIViewController {

    // Interface Links
    @IBOutlet private var canvas: Canvas! {
        didSet {
            canvas.isDrawingHandler = { [weak self] isDrawing in
                self?.clearBtn.isEnabled = !isDrawing
            }
        }
    }
    @IBOutlet weak var mainView: UIView!
    @IBOutlet weak var imageView: UIImageView!
    @IBOutlet var clearBtn: UIButton!
    @IBOutlet weak var itemsTableView: UITableView!
    @IBOutlet weak var addScribble: UIButton!

    // Properties
    var itemsName: [String] = ["Rust", "Ruptured", "Chipped", "Hole", "Cracked"]
    var addedItems: [DamageItem] = []

    // Life Cycle
    override func viewDidLoad() {
        super.viewDidLoad()
        imageView.image = UIImage(#imageLiteral(resourceName: "drawDamageOnTruck"))
        itemsTableView.tableFooterView = UIView()
    }

    @IBAction func nextBtn(_ sender: UIBarButtonItem) {
        guard
            let navigationController = navigationController,
            let secondVC = navigationController.storyboard?.instantiateViewController(withIdentifier: "SecondVC") as? SecondVC
            else { return }

        let signatureSaved = convertViewToImage(with: mainView)

        secondVC.signature = signatureSaved ?? UIImage()

        navigationController.pushViewController(secondVC, animated: true)
    }

    @IBAction func clearBtn(_ sender: UIButton) {
        canvas.clear()
        addedItems = []
        itemsTableView.reloadData()
    }

    @IBAction func addScribble(_ sender: UIButton) {

        let randomItem = itemsName.randomElement() ?? ""
        let drawedScribbles = [UInt8]()

        addedItems.append(DamageItem(name: randomItem, scribble: drawedScribbles))

        itemsTableView.reloadData()
    }

    // Convert an UIView to UIImage
    func convertViewToImage(with view: UIView) -> UIImage? {
        UIGraphicsBeginImageContextWithOptions(view.bounds.size, view.isOpaque, 0.0)
        defer { UIGraphicsEndImageContext() }
        if let context = UIGraphicsGetCurrentContext() {
            view.layer.render(in: context)
            let image = UIGraphicsGetImageFromCurrentImageContext()
            return image
        }
        return nil
    }
}

extension FirstVC: UITableViewDelegate, UITableViewDataSource {

    func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
        return addedItems.count
    }

    func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {

        let cell = tableView.dequeueReusableCell(withIdentifier: "itemCell", for: indexPath)

        cell.textLabel?.text = addedItems[indexPath.row].name

        return cell
    }

    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {

        print("Click on \(addedItems[indexPath.row].name)")

        // Bold the selected scribble on the image.

    }

    /// This method is used in iOS >= 11.0 instead of `editActionsForRowAt` to Delete a row.
    @available(iOS 11.0, *)
    func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {

        let actionHide = UIContextualAction(style: .destructive, title: "Delete") { action, view, handler in

            self.addedItems.remove(at: indexPath.row)
            self.itemsTableView.deleteRows(at: [indexPath], with: .none)
            handler(true)
        }
        actionHide.backgroundColor = UIColor.red
        return UISwipeActionsConfiguration(actions: [actionHide])
    }
}

Любая помощь будет высоко ценится! Спасибо за прочтение!

1 Ответ

1 голос
/ 18 октября 2019

Фундаментальная проблема заключается в том, что вы выбираете штриховые контуры и переходите к изображению. Это хорошая оптимизация (хотя обычно мы беспокоимся только о сотнях или тысячах точек для рендеринга), но она не позволит вам вернуться назад и повторно отрисовать отдельные пути разного цвета, если они уже отрисованы. внутри изображения.

Итак, решение состоит в том, чтобы сохранить ваш массив CGPoint для различных штрихов / путей (так называемых «каракулей» в вашем приложении). Это могут быть экземпляры, связанные с сохраненными экземплярами DamageItem, но мы бы хотели один для текущего жеста / касаний. Затем, когда вы выберете строку, связанную с конкретным DamageItem, вы откажетесь от сохраненного изображения, вернетесь назад и повторно отрендеретесь из массива штрихов с нуля, покрасив выбранный как соответствующий:

class Canvas: UIView {
    /// Closure to run on changes to drawing state
    var isDrawingHandler: ((Bool) -> Void)?

    /// The cached image drawn onto the canvas
    var image: UIImage?

    /// Caches the path for a line between touch down and touch up.
    public var damages: [DamageItem] = [] { didSet { invalidateCachedImage() } }

    /// The current scribble
    public var currentScribble: [CGPoint]?

    private var predictivePoints: [CGPoint]?

    /// Which path is currently selected
    public var selectedDamageIndex: Int? { didSet { invalidateCachedImage() } }

    /// The colour to use for drawing
    public var strokeColor: UIColor = .black
    public var selectedStrokeColor: UIColor = .orange

    /// Width of drawn lines
    private var lineWidth: CGFloat = 2 { didSet { invalidateCachedImage() } }

    override func awakeFromNib() {
        isMultipleTouchEnabled = false
    }

    override func draw(_ rect: CGRect) {
        strokePaths()
    }
}

// private utility methods

private extension Canvas {
    func strokePaths() {
        if image == nil {
            cacheImage()
        }

        image?.draw(in: bounds)

        if let currentScribble = currentScribble {
            strokeScribble(currentScribble + (predictivePoints ?? []), isSelected: true)
        }
    }

    func strokeScribble(_ points: [CGPoint], isSelected: Bool = false) {
        let path = UIBezierPath(simpleSmooth: points)
        let color = isSelected ? selectedStrokeColor : strokeColor
        path?.lineCapStyle = .round
        path?.lineJoinStyle = .round
        path?.lineWidth = lineWidth
        color.setStroke()
        path?.stroke()
    }

    func invalidateCachedImage() {
        image = nil
        setNeedsDisplay()
    }

    /// caches just the damages, but not the current scribble
    func cacheImage() {
        guard damages.count > 0 else { return }

        image = UIGraphicsImageRenderer(bounds: bounds).image { _ in
            for (index, damage) in damages.enumerated() {
                strokeScribble(damage.scribble, isSelected: selectedDamageIndex == index)
            }
        }
    }

    func append(_ touches: Set<UITouch>, with event: UIEvent?, includePredictive: Bool = false) {
        guard let touch = touches.first else { return }

        // probably should capture coalesced touches, too
        if let touches = event?.coalescedTouches(for: touch) {
            currentScribble?.append(contentsOf: touches.map { $0.location(in: self) })
        }

        currentScribble?.append(touch.location(in: self))

        if includePredictive {
            predictivePoints = event?
                .predictedTouches(for: touch)?
                .map { $0.location(in: self) }
        } else {
            predictivePoints = nil
        }

        setNeedsDisplay()
    }
}

// UIResponder methods
extension Canvas {
    override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
        guard let touch = touches.first else { return }
        let point = touch.location(in: self)
        currentScribble = [point]
        selectedDamageIndex = nil

        isDrawingHandler?(true)
    }

    override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
        append(touches, with: event)
    }

    override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
        append(touches, with: event, includePredictive: false)

        isDrawingHandler?(false)
    }

    override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
        touchesEnded(touches, with: event)
    }
}

См. https://github.com/tygruletz/AddScribblesOnImage/pull/1 для примера этой реализации. См. https://github.com/tygruletz/AddScribblesOnImage/pull/2 для примера, где у вас может быть несколько путей в виде набора «каракулей», связанных с конкретным DamageItem.

Примечание. Я бы лично сделал «сглаживание»штриховые пути являются частью процесса генерации UIBezierPath, но сохраняют фактический массив CGPoint пользователя в этом объекте модели. Я бы также предложил включить слитые касания (для точного захвата жеста в устройствах с высокой частотой кадров) и прогнозирующие касания (чтобы избежать ощутимой задержки в пользовательском интерфейсе). Все это включено в вышеупомянутый запрос на извлечение.


Не связано, но я мог бы сделать еще несколько предложений:

  • Я бы переименовал Canvasбыть CanvasView, поскольку подклассы UIView всегда несут суффикс View в соответствии с соглашением;

  • Я мог бы предложить подумать о том, чтобы выйти из процесса рисования путейсебя. Я бы обычно отображал пути в CAShapeLayer подслоях. Таким образом, вы наслаждаетесь оптимизацией Apple.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...