将 UIView CALayer 绘制与 UIScrollView 同步

Synchronize UIView CALayer draw with UIScrollView

提问人:Alexey Zhuravlev 提问时间:10/19/2023 最后编辑:Alexey Zhuravlev 更新时间:10/29/2023 访问量:87

问:

我正在尝试制作一个将在 pdf 之上绘制的应用程序。要求将图形形状绑定到页面上的特定位置。

为了解决这个问题,我使用 PDFView 和 CALayer,监听内部 UIScrollView scrollViewDidScroll 和 scrollViewDidZoom 的事件来重绘 CALayer

我找到了两种绘画方法:

  1. 使用 CAShapeLayer 的“path”属性(示例 1)
  2. 重写 CALayer 的“draw”方法(示例 2)

我注意到,在第二种情况下,滚动或缩放时,该图明显滞后于 pdf 文档。

问题是,在导致即时重绘的“路径”方法中做了什么“魔术”?是否可以修改“draw”方法,使其与“path”同步工作?“路径”方法在未来会有多稳定?

查看要编译和运行的项目:https://github.com/alzhuravlev/DrawTest.git

示例代码:

class AnnotationsLayer: CAShapeLayer {
...

  override func draw(in ctx: CGContext) {
        guard let backgroundView else { return }

        ctx.setFillColor(UIColor.red.withAlphaComponent(0.4).cgColor)
        ctx.setStrokeColor(UIColor.green.withAlphaComponent(1.0).cgColor)
        ctx.setLineWidth(10)

        backgroundView.pages.forEach { page in
            let p1 = backgroundView.convert(point: CGPoint(x: page.size.width/3, y: page.size.height/3), from: page.index)
            let p2 = backgroundView.convert(point: CGPoint(x: page.size.width/3*2, y: page.size.height/3*2), from: page.index)

            ctx.move(to: CGPoint(x: p1.x, y: p1.y))
            ctx.addLine(to: CGPoint(x: p2.x, y: p1.y))
            ctx.addLine(to: CGPoint(x: p2.x, y: p2.y))
            ctx.addLine(to: CGPoint(x: p1.x, y: p2.y))
            ctx.closePath()
        }


        ctx.drawPath(using: CGPathDrawingMode.fillStroke)
    }

    func rebuildShapes() {
        
        // false - example 1
        // true - example 2
        if (false) { 
            setNeedsDisplay()
        } else {
            
            guard let backgroundView else { return }
            
            let p = UIBezierPath()
            
            fillColor = UIColor.red.withAlphaComponent(0.4).cgColor
            
            strokeColor = UIColor.green.withAlphaComponent(1.0).cgColor
            lineWidth = 10
            
            backgroundView.pages.forEach { page in
                let p1 = backgroundView.convert(point: CGPoint(x: page.size.width/3, y: page.size.height/3), from: page.index)
                let p2 = backgroundView.convert(point: CGPoint(x: page.size.width/3*2, y: page.size.height/3*2), from: page.index)
                
                p.move(to: CGPoint(x: p1.x, y: p1.y))
                p.addLine(to: CGPoint(x: p2.x, y: p1.y))
                p.addLine(to: CGPoint(x: p2.x, y: p2.y))
                p.addLine(to: CGPoint(x: p1.x, y: p2.y))
                p.close()
            }
            
            path = p.cgPath
        }
    }
...
iOS UIscrollView UIKiter Calayer CashapeLayer

评论

0赞 DonMag 10/25/2023
CAShapeLayerwith path 由 Apple 工程师高度优化。如果无法运行代码,就很难知道代码在哪里减慢了速度。如果您真的想覆盖绘制,请发布一个我们可以运行的最小可重现示例
0赞 Alexey Zhuravlev 10/26/2023
这是 github.com/alzhuravlev/DrawTest.git
0赞 DonMag 10/28/2023
我看了一下你的回购...如果不花大量时间在仪器上,很难分辨,但我的猜测是,覆盖最终的效率远低于使用 .也许值得一提的是......您可以通过覆盖而不是添加图层来简化此操作。您还可以获得清晰、锐利的边缘(而不是当前实现中的模糊边缘)。draw(in ctx: CGContext)CAShapeLayer.pathdraw(_ rect: CGRect)AnnotationsView
0赞 DonMag 10/28/2023
供参考:i.stack.imgur.com/U7kDd.png
0赞 Alexey Zhuravlev 10/29/2023
我有一个假设,即 CALayer.draw 方法中的绘图滞后,因为绘图发生在缓冲区中,该缓冲区在下一帧延迟传输到屏幕,即我们收集的坐标相对于 scrollView 的状态已经过时了。通过将形状设置为 CAShapeLayer.path 属性,将立即重置此缓冲区

答:

0赞 DonMag 10/29/2023 #1

根据 OP 的评论,以下是我将使用的内容,而不是在 :draw(in ctx: CGContext)CAShapeLayer

class AnnotationsView: UIView {
    var backgroundView: BackgroundView? {
        didSet {
            print("did set background view")
        }
    }
    
    override init(frame: CGRect) {
        super.init(frame: frame)
        commonInit()
    }
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commonInit()
    }
    func commonInit() {
        backgroundColor = .clear
    }
    func rebuildShapes() {
        setNeedsDisplay()
    }
    
    override func draw(_ rect: CGRect) {
        super.draw(rect)
        
        guard let backgroundView else { return }
        
        let p = UIBezierPath()
        
        let fillColor = UIColor.red.withAlphaComponent(0.4)
        
        let strokeColor = UIColor.green.withAlphaComponent(1.0)
        let lineWidth = 10
        
        backgroundView.pages.forEach { page in
            let p1 = backgroundView.convert(point: CGPoint(x: page.size.width/3, y: page.size.height/3), from: page.index)
            let p2 = backgroundView.convert(point: CGPoint(x: page.size.width/3*2, y: page.size.height/3*2), from: page.index)
            let r: CGRect = .init(x: p1.x, y: p1.y, width: p2.x - p1.x, height: p2.y - p1.y)
            if r.intersects(rect) {
                p.append(.init(rect: r))
            }
        }
        
        fillColor.setFill()
        p.fill()
        strokeColor.setStroke()
        p.lineWidth = CGFloat(lineWidth)
        p.stroke()
    }
}

无需添加任何图层。