ios - 在 Swift 中绘制工件

标签 ios swift drawing uibezierpath

下面的代码通过覆盖触摸来绘制线条,但是在绘制时存在一个伪像,如下图所示。

当在屏幕上锯齿状绘制时改变方向,有时线条会变成平坦的直角而不是保持圆形。在原地画小圆圈时也有这种现象,画点在手指离开屏幕时会闪烁半圈,有时会留下半圈和部分圆圈残留。

人工制品是断断续续的,不是完全一致或可预测的模式,因此很难在代码中找到问题。它存在于 iOS7 - iOS9 的模拟器和设备上。

包含两个绘图点和线的视频屏幕截图的 zip 以及 Xcode 项目以名为 Archive.zip (23MB) 的文件上传到 DropBox https://www.dropbox.com/s/hm39rdiuk0mf578/Archive.zip?dl=0

问题:

1 - 在代码中,是什么导致了这个点/半圆伪影,如何纠正?

enter image description here

class SmoothCurvedLinesView: UIView {
    var strokeColor = UIColor.blueColor()
    var lineWidth: CGFloat = 20
    var snapshotImage: UIImage?

    private var path: UIBezierPath?
    private var temporaryPath: UIBezierPath?
    private var points = [CGPoint]()
    private var totalPointCount = 0

    override func drawRect(rect: CGRect) {
        snapshotImage?.drawInRect(rect)

        strokeColor.setStroke()

        path?.stroke()
        temporaryPath?.stroke()
    }

    override func touchesBegan(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch: AnyObject? = touches.first
        points = [touch!.locationInView(self)]
        totalPointCount = totalPointCount + 1
    }

    override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
        let touch: AnyObject? = touches.first
        let point = touch!.locationInView(self)

        points.append(point)
        totalPointCount = totalPointCount + 1

        updatePaths()

        if totalPointCount > 50 {
            constructIncrementalImage(includeTemporaryPath: false)
            path = nil
            totalPointCount = 0
        }

        setNeedsDisplay()
    }

    private func updatePaths() {
        // update main path

        while points.count > 4 {
            points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)

            if path == nil {
                path = createPathStartingAtPoint(points[0])
            }

            path?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])

            points.removeFirst(3)
        }

        // build temporary path up to last touch point

        let pointCount = points.count

        if pointCount == 2 {
            temporaryPath = createPathStartingAtPoint(points[0])
            temporaryPath?.addLineToPoint(points[1])
        } else if pointCount == 3 {
            temporaryPath = createPathStartingAtPoint(points[0])
            temporaryPath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
        } else if pointCount == 4 {
            temporaryPath = createPathStartingAtPoint(points[0])
            temporaryPath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
        }
    }

    override func touchesEnded(touches: Set<UITouch>, withEvent event: UIEvent?) {
        constructIncrementalImage()
        path = nil
        setNeedsDisplay()
    }

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

    private func createPathStartingAtPoint(point: CGPoint) -> UIBezierPath {
        let localPath = UIBezierPath()

        localPath.moveToPoint(point)

        localPath.lineWidth = lineWidth
        localPath.lineCapStyle = .Round
        localPath.lineJoinStyle = .Round

        return localPath
    }

    private func constructIncrementalImage(includeTemporaryPath includeTemporaryPath: Bool = true) {
        UIGraphicsBeginImageContextWithOptions(bounds.size, false, 0.0)
        strokeColor.setStroke()
        snapshotImage?.drawAtPoint(CGPointZero)
        path?.stroke()
        if (includeTemporaryPath) { temporaryPath?.stroke() }
        snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
        UIGraphicsEndImageContext()
    }
}

最佳答案

这似乎是 addQuadCurveToPointaddCurveToPoint 中的一个迷人错误,如果控制点与两个端点在同一条线上,则不支持 lineJoinStyle。所以你可以对此进行测试(通过查看各个点的 atan2 并确保它们不相同),如果是这样,只需执行 addLineToPoint 即可:

我发现这个修改后的代码删除了那些工件:

private func updatePaths() {
    // update main path

    while points.count > 4 {
        points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)

        if path == nil {
            path = createPathStartingAtPoint(points[0])
        }

        addCubicCurveToPath(path)

        points.removeFirst(3)
    }

    // build temporary path up to last touch point

    let pointCount = points.count

    if pointCount == 2 {
        temporaryPath = createPathStartingAtPoint(points[0])
        temporaryPath?.addLineToPoint(points[1])
    } else if pointCount == 3 {
        temporaryPath = createPathStartingAtPoint(points[0])
        addQuadCurveToPath(temporaryPath)
    } else if pointCount == 4 {
        temporaryPath = createPathStartingAtPoint(points[0])
        addCubicCurveToPath(temporaryPath)
    }
}

/// Add cubic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than cubic bezier path.

private func addCubicCurveToPath(somePath: UIBezierPath?) {
    let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
    let m23 = atan2(points[2].x - points[3].x, points[2].y - points[3].y)
    let m03 = atan2(points[0].x - points[3].x, points[0].y - points[3].y)
    if m01 == m03 || m23 == m03 || points[0] == points[3] {
        somePath?.addLineToPoint(points[1])
        somePath?.addLineToPoint(points[2])
        somePath?.addLineToPoint(points[3])
    } else {
        somePath?.addCurveToPoint(points[3], controlPoint1: points[1], controlPoint2: points[2])
    }
}

/// Add quadratic curve to path
///
/// Because of bug with bezier curves that fold back on themselves do no honor `lineJoinStyle`,
/// check to see if this occurs, and if so, just add lines rather than quadratic bezier path.

private func addQuadCurveToPath(somePath: UIBezierPath?) {
    let m01 = atan2(points[0].x - points[1].x, points[0].y - points[1].y)
    let m12 = atan2(points[1].x - points[2].x, points[1].y - points[2].y)
    let m02 = atan2(points[0].x - points[2].x, points[0].y - points[2].y)
    if m01 == m02 || m12 == m02 || points[0] == points[2] {
        somePath?.addLineToPoint(points[1])
        somePath?.addLineToPoint(points[2])
    } else {
        somePath?.addQuadCurveToPoint(points[2], controlPoint: points[1])
    }
}

此外,这可能过于谨慎,但使用 guard 语句确保两个连续的点永远不会相同可能是谨慎的做法:

override func touchesMoved(touches: Set<UITouch>, withEvent event: UIEvent?) {
    let touch: AnyObject? = touches.first
    let point = touch!.locationInView(self)

    guard point != points.last else { return }

    points.append(point)
    totalPointCount = totalPointCount + 1

    updatePaths()

    if totalPointCount > 50 {
        constructIncrementalImage(includeTemporaryPath: false)
        path = nil
        totalPointCount = 0
    }

    setNeedsDisplay()
}

如果你发现其他情况有问题,你可以重复我刚才做的调试练习。也就是说,运行代码直到出现问题,但立即停止并查看 points 数组的日志以查看哪些点导致了问题,然后创建一个 init?(coder:) 在 100% 的时间内始终重现问题,例如:

required init?(coder aDecoder: NSCoder) {
    super.init(coder: aDecoder)

    points.append(CGPoint(x: 239.33332824707, y: 419.0))
    points.append(CGPoint(x: 239.33332824707, y: 420.0))
    points.append(CGPoint(x: 239.33332824707, y: 419.3))

    updatePaths()
}

然后,有了一个始终可重现的问题,调试就很容易了。因此,在诊断出问题后,我修改了 updatePaths 直到问题得到解决。然后我注释掉了 init? 并重复了整个练习。

关于ios - 在 Swift 中绘制工件,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35608766/

相关文章:

ios - 自定义部分标题 View 时部分标题标题丢失

ios - 获取可选链的负数

javascript - 绘图应用程序撤消按钮

c# - 夹一个 4 :3 image in WPF with a circle

java - Android Java,在缩放的 Canvas 上绘制

ios - 如何编辑标签

ios - 如何简单地用评论填充 tableView?

ios - 清除 iPhone 和 iPad 上的 View Controller

iphone - 在 iOS 上绘制带阴影的矩形

swift - 如何在 SwiftUI 中的 LineMark 图表上添加标签