草庐IT

关于ios:在Swift中连续绘制UIBezierPath期间消除滞后延迟

codeneng 2023-03-28 原文

Removing lagging latency during continuous period of drawing UIBezierPath in Swift

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

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
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。我还添加了 autoreleasepooldrawRect 以帮助避免类崩溃。不确定这是否有用或不必要,但我注意到它有时可以帮助避免过去的 CPU 崩溃。 Coalesced touches 还没有被用作 Ia€?m 在 iOS7 和 iOS9 上的实验。谢谢你。


你问过:

  • How can the lagging latency over a period of continuous drawing be eliminated?
  • 正如您正确推测的那样,是的,创建快照并重置路径可以通过限制路径的长度来解决此问题。

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

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

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

    例如:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    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?
  • 您可以使用适当的 alpha 调整调用 setStroke 时使用的颜色。例如,如果您希望临时路径位于主路径 alpha 的一半,您可以执行以下操作:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    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)touchesEnded 的开头。当 points.count = 2 双线出现但我无法弄清楚为什么要比较以前的代码时,结果是这样的。 (它不会发生在 34 上。)关于它可能是什么的任何想法?
    • 我在上面的代码中注意到了一个伪影。我在以下位置发布的详细信息:stackoverflow.com/questions/35608766

    有关关于ios:在Swift中连续绘制UIBezierPath期间消除滞后延迟的更多相关文章

    1. ruby - 如何以所有可能的方式将字符串拆分为长度最多为 3 的连续子字符串? - 2

      我试图获取一个长度在1到10之间的字符串,并输出将字符串分解为大小为1、2或3的连续子字符串的所有可能方式。例如:输入:123456将整数分割成单个字符,然后继续查找组合。该代码将返回以下所有数组。[1,2,3,4,5,6][12,3,4,5,6][1,23,4,5,6][1,2,34,5,6][1,2,3,45,6][1,2,3,4,56][12,34,5,6][12,3,45,6][12,3,4,56][1,23,45,6][1,2,34,56][1,23,4,56][12,34,56][123,4,5,6][1,234,5,6][1,2,345,6][1,2,3,456][123

    2. ruby - 如何验证 IO.copy_stream 是否成功 - 2

      这里有一个很好的答案解释了如何在Ruby中下载文件而不将其加载到内存中:https://stackoverflow.com/a/29743394/4852737require'open-uri'download=open('http://example.com/image.png')IO.copy_stream(download,'~/image.png')我如何验证下载文件的IO.copy_stream调用是否真的成功——这意味着下载的文件与我打算下载的文件完全相同,而不是下载一半的损坏文件?documentation说IO.copy_stream返回它复制的字节数,但是当我还没有下

    3. Ruby 文件 IO 定界符? - 2

      我正在尝试解析一个文本文件,该文件每行包含可变数量的单词和数字,如下所示:foo4.500bar3.001.33foobar如何读取由空格而不是换行符分隔的文件?有什么方法可以设置File("file.txt").foreach方法以使用空格而不是换行符作为分隔符? 最佳答案 接受的答案将slurp文件,这可能是大文本文件的问题。更好的解决方案是IO.foreach.它是惯用的,将按字符流式传输文件:File.foreach(filename,""){|string|putsstring}包含“thisisanexample”结果的

    4. Get https://registry-1.docker.io/v2/: net/http: request canceled while waiting - 2

      1.错误信息:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:requestcanceledwhilewaitingforconnection(Client.Timeoutexceededwhileawaitingheaders)或者:Errorresponsefromdaemon:Gethttps://registry-1.docker.io/v2/:net/http:TLShandshaketimeout2.报错原因:docker使用的镜像网址默认为国外,下载容易超时,需要修改成国内镜像地址(首先阿里

    5. ruby-on-rails - 在所有延迟的作业之前 Hook - 2

      是否可以在所有delayed_job任务之前运行一个方法?基本上,我们试图确保每个运行delayed_job的服务器都有我们代码的最新实例,所以我们想运行一个方法来在每个作业运行之前检查它。(我们已经有了“check”方法并在别处使用它。问题只是关于如何从delayed_job中调用它。) 最佳答案 现在有一种官方方法可以通过插件来做到这一点。这篇博文通过示例清楚地描述了如何执行此操作http://www.salsify.com/blog/delayed-jobs-callbacks-and-hooks-in-rails(本文中描述

    6. ruby - 为什么不能使用类IO的实例方法noecho? - 2

      print"Enteryourpassword:"pass=STDIN.noecho(&:gets)puts"Yourpasswordis#{pass}!"输出:Enteryourpassword:input.rb:2:in`':undefinedmethod`noecho'for#>(NoMethodError) 最佳答案 一开始require'io/console'后来的Ruby1.9.3 关于ruby-为什么不能使用类IO的实例方法noecho?,我们在StackOverflow上

    7. ruby-on-rails - 关于 Ruby 的一般问题 - 2

      我在我的rails应用程序中安装了来自github.com的acts_as_versioned插件,但有一段代码我不完全理解,我希望有人能帮我解决这个问题class_eval我知道block内的方法(或任何它是什么)被定义为类内的实例方法,但我在插件的任何地方都找不到定义为常量的CLASS_METHODS,而且我也不确定是什么here,并且有问题的代码从lib/acts_as_versioned.rb的第199行开始。如果有人愿意告诉我这里的内幕,我将不胜感激。谢谢-C 最佳答案 这是一个异端。http://en.wikipedia

    8. ruby - 如何用 Nokogiri 解析连续的标签? - 2

      我有这样的HTML代码:Label1Value1Label2Value2...我的代码不起作用。doc.css("first").eachdo|item|label=item.css("dt")value=item.css("dd")end显示所有首先标记,然后标记标签,我需要“标签:值” 最佳答案 首先,您的HTML应该有和中的元素:Label1Value1Label2Value2...但这不会改变您解析它的方式。你想找到s并遍历它们,然后在每个你可以使用next_element得到;像这样:doc=Nokogiri::HTML(

    9. 多种方法期间的 Ruby 救援异常 - 2

      我构建了一个简单的银行应用程序,它能够执行通常的操作;充值、提现等我的Controller方法执行这些操作并拯救由帐户或其他实体引发的异常。以下是Controller代码中使用的一些方法:defopen(type,with:)account=createtype,(holders.findwith)addaccountinit_yearly_interest_foraccountboundary.renderAccountSuccessMessage.new(account)rescueItemExistError=>messageboundary.rendermessageendde

    10. ruby-on-rails - 如何在记录更新期间从验证中排除密码字段? ( rails 3.0.4, ruby 1.9.2) - 2

      我有一个允许更新用户记录的表单。它包含:password和:password_confirmation字段,但我不希望在数据库中已存储加密密码时对它们运行验证。View文件中的字段:'ConfirmPassword'%>在互联网上搜索时,我发现了这段代码,我认为它是针对以前版本的Ruby/Rails的。(我会把它放在我的用户模型中。)validates_presence_of:password,:on=>create由于我的用户模型中密码验证的语法不同(如下),我对我需要的语法感到困惑。validates:password,:presence=>true,:confirmation=>

    随机推荐