【问题标题】:Removing lagging latency during continuous period of drawing UIBezierPath in Swift在 Swift 中连续绘制 UIBezierPath 期间消除滞后延迟
【发布时间】:2016-01-28 17:14:59
【问题描述】:

下面的代码通过覆盖触摸来绘制线条,但是在连续不间断的绘制期间开始出现滞后。手指在屏幕上移动的时间越长,这种滞后就会越积越多。结果是实际设备上的 CPU 几乎达到最大值(CPU 98%+),并且绘制的时间越长,生成的图像就越不稳定。

此外,当绘制速度特别快时,尤其是在圆圈中,pathtemporaryPath(或 localPath)之间绘制的路径存在差异。尽管它们是在不同时间绘制的,但它们似乎同时出现在屏幕上,这在视觉上分散了两条快速绘制的路径。在下面的一张图片中,内部路径 (path) 似乎与外部路径 (temporaryPath) 相距一段距离。

1 - 如何消除一段时间连续绘制的滞后延迟?

2 - 如何消除绘制路径的差异?

3 - 如何更改 pathtemporaryPath 的 alpha/opacity?

class swiftView: UIView {

var strokeColor = UIColor.blueColor()
var lineWidth: CGFloat = 5
var snapshotImage: UIImage?

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

var counterPoints:Int?

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

override func drawRect(rect: CGRect) {
    autoreleasepool {

    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)]

    counterPoints = 0
}

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

    points.append(point)
    let pointCount = points.count

    counterPoints = counterPoints! + 1

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

        if counterPoints! < 50 {
            self.setNeedsDisplay()
        } else {
            temporaryPath = nil
            self.constructIncrementalImage()
            path = nil
            self.setNeedsDisplay()
            counterPoints = 0
        }

    } else if pointCount == 5 {
        points[3] = CGPointMake((points[2].x + points[4].x)/2.0, (points[2].y + points[4].y)/2.0)

        // create a quad bezier up to point 4, too

        if points[4] != points[3] {
            let length = hypot(points[4].x - points[3].x, points[4].y - points[3].y) / 2.0
            let angle = atan2(points[3].y - points[2].y, points[4].x - points[3].x)
            let controlPoint = CGPoint(x: points[3].x + cos(angle) * length, y: points[3].y + sin(angle) * length)

            temporaryPath = createPathStartingAtPoint(points[3])
            temporaryPath?.addQuadCurveToPoint(points[4], controlPoint: controlPoint)
        } else {
            temporaryPath = nil
        }

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

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

        self.setNeedsDisplay()

        points = [points[3], points[4]]
    }
}

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

    counterPoints = 0
}

override func touchesCancelled(touches: Set<UITouch>?, withEvent event: UIEvent?) {
    self.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() {
    UIGraphicsBeginImageContextWithOptions(self.bounds.size, false, 0.0)
    strokeColor.setStroke()
    snapshotImage?.drawAtPoint(CGPointZero)
    path?.stroke()
    temporaryPath?.stroke()
    snapshotImage = UIGraphicsGetImageFromCurrentImageContext()
    UIGraphicsEndImageContext()
}

}

【问题讨论】:

  • 你说“我已经尝试在大约 50 个连续绘图点之后在 pointCount == 4 时缓存绘图”。好吧,也许您应该向我们展示该代码,因为这正是解决此问题的方法。但也许 50 太少了(因为当您谈论手势时,触摸会迅速增加,尤其是在使用合并触摸的情况下)。但拍摄快照是典型的解决方案(意识到快照过程本身很慢,因此您需要平衡快照的频率和路径的长度)。
  • @Rob 是的,一些更新的代码会很有帮助。我已经更新了,抱歉。我还更新了问题。代码中添加了一个变量counterPoints。我还将autoreleasepool 添加到drawRect 以帮助避免课程崩溃。不确定这是否有用或不必要,但我注意到它可以帮助避免过去的 CPU 崩溃。因为我在 iOS7 和 iOS9 上试验,所以没有使用合并的触摸。谢谢。

标签: ios swift drawing uibezierpath


【解决方案1】:

你问:

  1. 如何消除连续绘制一段时间内的滞后延迟?

正如您正确推测的那样,是的,创建快照并重置路径可以通过限制路径的长度来解决此问题。

我知道您已经意识到这一点,但为了其他读者的利益,在 iOS 9 中您也可以使用预测性触控。在这个特定的算法中(其中(a)您只是添加到路径,但(b)每四个点根据下一个点进行调整,以确保两条三次贝塞尔曲线连接处没有不连续性)有点棘手,但可以做到。

  1. 如何消除绘制路径的差异?

这是因为快照包含临时路径。但是该临时路径的全部目的是随着更多点的进入,它将被丢弃。因此您不应该将其包含在您创建的中间手势快照中。

所以,我建议在快照函数中添加一个参数,指示是否应包含temporaryPath。在手势中间调用它时,您可以将 includeTemporaryPath 指定为 false,但在手势结束时调用它时,includeTemporaryPath 将是 true

例如:

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
        temporaryPath = 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()
    }
}

顺便说一句,虽然我是提供路径生成代码的人,但我意识到它可以简化一点。我也修复了一个错误。见上面的代码。

然后你问:

  1. 如何更改路径和临时路径的 alpha/opacity?

您可以使用适当的 alpha 调整调用 setStroke 时使用的颜色。例如,如果您希望临时路径位于主路径的 alpha 的一半,您可以执行以下操作:

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

    strokeColor.setStroke()
    path?.stroke()

    strokeColor.colorWithAlphaComponent(0.5).setStroke()
    temporaryPath?.stroke()
}

【讨论】:

  • 谢谢。我喜欢简化代码的想法——紧凑!但我注意到,自从更改后,有时绘图末尾会出现一条双线,图片:i.imgur.com/vU96UT5.png 为了尝试追踪问题,我设置了lineWidth: CGFloat = 1 并将print(points.count) 添加到@ 的开头987654334@。当points.count = 2 出现双线时,结果出现了,但我不知道为什么要比较以前的代码。 (34 不会发生这种情况。)有什么想法吗?
  • 我注意到上面的代码中有一个伪影。我已经发布的详细信息:stackoverflow.com/questions/35608766
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-12-29
  • 2021-11-24
  • 1970-01-01
  • 2017-10-05
相关资源
最近更新 更多