在 Swift 中连续绘制 UIBezierPath 期间消除滞后延迟
Removing lagging latency during continuous period of drawing UIBezierPath in Swift
下面的代码通过覆盖触摸来绘制线条,但是在连续不间断绘制的一段时间内开始出现滞后。随着手指在屏幕上移动的时间越长,这种滞后会累积并变得更糟。结果是 CPU 在实际设备上几乎达到最大值(CPU 98%+),并且绘图持续的时间越长,生成的图像看起来越不稳定。
此外,当画得特别快时,特别是在圆圈中,在 path
和 temporaryPath
(或 localPath
)之间绘制的路径存在差异。虽然它们是在不同的时间绘制的,但它们似乎同时出现在屏幕上,这在视觉上分散了两条路径快速绘制的注意力。内部路径 (path
) 似乎与外部路径 (temporaryPath
) 有一段距离,在下图中以红色突出显示。
1 - 如何消除连续绘制一段时间后的滞后延迟?
2 - 如何消除绘制路径中的差异?
3 - 如何更改 path
和 temporaryPath
的 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()
}
}
你问过:
- How can the lagging latency over a period of continuous drawing be eliminated?
正如您正确推测的那样,是的,创建快照并重置路径可以通过限制路径的长度来解决此问题。
我知道您知道这一点,但为了其他读者的利益,在 iOS9 中您也可以使用预测触摸。在这个特定的算法中(其中(a)你只是添加到路径,但是(b)根据下一个点调整每第四个点以确保两条三次贝塞尔曲线连接处没有不连续点)这是有点棘手,但可以完成。
- How can the discrepancy in the paths drawn be eliminated?
这是因为快照包含了临时路径。但是该临时路径的全部目的是随着更多点的进入,它将被丢弃。所以你不应该将它包含在你创建中间手势的快照中。
因此,我建议向快照函数添加一个参数,指示是否应包含 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()
}
}
顺便说一句,虽然我是提供路径生成代码的人,但我意识到它可以稍微简化一下。我也修复了一个错误。见上面的代码。
你接着问:
- How can the alpha/opacity of the path and temporaryPath be changed?
您只需调整调用 setStroke
所用的颜色,使其具有适当的 alpha。例如,如果您希望临时路径位于主路径 alpha
的一半,您可以这样做:
override func drawRect(rect: CGRect) {
snapshotImage?.drawInRect(rect)
strokeColor.setStroke()
path?.stroke()
strokeColor.colorWithAlphaComponent(0.5).setStroke()
temporaryPath?.stroke()
}
下面的代码通过覆盖触摸来绘制线条,但是在连续不间断绘制的一段时间内开始出现滞后。随着手指在屏幕上移动的时间越长,这种滞后会累积并变得更糟。结果是 CPU 在实际设备上几乎达到最大值(CPU 98%+),并且绘图持续的时间越长,生成的图像看起来越不稳定。
此外,当画得特别快时,特别是在圆圈中,在 path
和 temporaryPath
(或 localPath
)之间绘制的路径存在差异。虽然它们是在不同的时间绘制的,但它们似乎同时出现在屏幕上,这在视觉上分散了两条路径快速绘制的注意力。内部路径 (path
) 似乎与外部路径 (temporaryPath
) 有一段距离,在下图中以红色突出显示。
1 - 如何消除连续绘制一段时间后的滞后延迟?
2 - 如何消除绘制路径中的差异?
3 - 如何更改 path
和 temporaryPath
的 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()
}
}
你问过:
- How can the lagging latency over a period of continuous drawing be eliminated?
正如您正确推测的那样,是的,创建快照并重置路径可以通过限制路径的长度来解决此问题。
我知道您知道这一点,但为了其他读者的利益,在 iOS9 中您也可以使用预测触摸。在这个特定的算法中(其中(a)你只是添加到路径,但是(b)根据下一个点调整每第四个点以确保两条三次贝塞尔曲线连接处没有不连续点)这是有点棘手,但可以完成。
- How can the discrepancy in the paths drawn be eliminated?
这是因为快照包含了临时路径。但是该临时路径的全部目的是随着更多点的进入,它将被丢弃。所以你不应该将它包含在你创建中间手势的快照中。
因此,我建议向快照函数添加一个参数,指示是否应包含 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()
}
}
顺便说一句,虽然我是提供路径生成代码的人,但我意识到它可以稍微简化一下。我也修复了一个错误。见上面的代码。
你接着问:
- How can the alpha/opacity of the path and temporaryPath be changed?
您只需调整调用 setStroke
所用的颜色,使其具有适当的 alpha。例如,如果您希望临时路径位于主路径 alpha
的一半,您可以这样做:
override func drawRect(rect: CGRect) {
snapshotImage?.drawInRect(rect)
strokeColor.setStroke()
path?.stroke()
strokeColor.colorWithAlphaComponent(0.5).setStroke()
temporaryPath?.stroke()
}