NSView 中的 CATiledLayer 在更改 contentsScale 时闪烁

CATiledLayer in NSView flashes on changing contentsScale

我在 NSView 中有一个 CATiledLayer,它是一个 documentView 属性 NSScrollView。 故事板设置非常简单:将 NSScrollView 添加到默认视图控制器并将 View class 分配给 NSView 的剪裁视图。

下面的代码绘制了一些随机颜色的方块。滚动在 CATiledLayer 中完全正常,但缩放效果不佳:

发现大量 CATiledLayer 问题,所有建议的解决方案都不适合我(比如 subclassing with 0 fadeDuration 或禁用 [​​=22=]CATransaction 操作)。我想 setNeedsDisplay() 搞砸了这一切,但无法找到正确的方法。如果我使用 CALayer,那么我看不到闪烁问题,但我无法处理内部数千个盒子的大层。

查看class来源:

import Cocoa
import CoreGraphics
import Combine

let rows = 1000
let columns = 1000
let width = 50.0
let height = 50.0

class View: NSView {
    typealias Coordinate = (x: Int, y: Int)
    
    private let colors: [[CGColor]]
    private let rect = CGRect(origin: .zero, size: CGSize(width: width, height: height))
    private var store = Set<AnyCancellable>()
    private var scale: CGFloat {
        guard let scrollView = self.superview?.superview as? NSScrollView else { fatalError() }
        return NSScreen.main!.backingScaleFactor * scrollView.magnification
    }
    
    required init?(coder: NSCoder) {
        colors = (0..<rows).map { _ in (0..<columns).map { _ in .random } }
        super.init(coder: coder)
        
        setFrameSize(NSSize(width: width * CGFloat(columns), height: height * CGFloat(rows)))
        
        wantsLayer = true
        
        NotificationCenter.default.publisher(for: NSScrollView.didEndLiveMagnifyNotification).sink { [unowned self] _ in
            self.layer?.contentsScale = scale
            self.layer?.setNeedsDisplay()
        }.store(in: &store)
    }
    
    override func makeBackingLayer() -> CALayer {
        let layer = CATiledLayer()
        layer.tileSize = CGSize(width: 1000, height: 1000)
        return layer
    }
    
    override func draw(_ dirtyRect: NSRect) {
        guard let context = NSGraphicsContext.current?.cgContext else { return }
        
        let (min, max) = coordinates(in: dirtyRect)
        
        context.translateBy(x: CGFloat(min.x) * width, y: CGFloat(min.y) * height)
        
        (min.y...max.y).forEach { row in
            context.saveGState()
            
            (min.x...max.x).forEach { column in
                context.setFillColor(colors[row][column])
                context.addRect(rect)
                context.drawPath(using: .fillStroke)
                
                context.translateBy(x: width, y: 0)
            }
            
            context.restoreGState()
            context.translateBy(x: 0, y: height)
        }
    }
    
    private func coordinates(in rect: NSRect) -> (Coordinate, Coordinate) {
        var minX = Int(rect.minX / width)
        var minY = Int(rect.minY / height)
        var maxX = Int(rect.maxX / width)
        var maxY = Int(rect.maxY / height)
        
        if minX >= columns {
            minX = columns - 1
        }
        
        if maxX >= columns {
            maxX = columns - 1
        }
        
        if minY >= rows {
            minY = rows - 1
        }
        
        if maxY >= rows {
            maxY = rows - 1
        }
        
        return ((minX, minY), (maxX, maxY))
    }
}


extension CGColor {
    class var random: CGColor {
        let random = { CGFloat(arc4random_uniform(255)) / 255.0 }
        return CGColor(red: random(), green: random(), blue: random(), alpha: random())
    }
}

为了能够支持放大到 CATiledLayer,您需要设置图层的 levelOfDetailBias。您无需观察滚动视图的放大通知、更改图层 contentScale 或触发手动重绘。

这是一个快速实现,显示了在不同缩放级别下您获得的 dirtyRects 类型:

class View: NSView {
    
    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        wantsLayer = true
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        wantsLayer = true
    }
    
    override func makeBackingLayer() -> CALayer {
        let layer = CATiledLayer()
        layer.tileSize = CGSize(width: 400, height: 400)
        layer.levelsOfDetailBias = 3
        return layer
    }
    
    override func draw(_ dirtyRect: NSRect) {
        let context = NSGraphicsContext.current!
        let scale = context.cgContext.ctm.a
        
        NSColor.red.setFill()
        dirtyRect.frame(withWidth: 10 / scale, using: .overlay)
        
        NSColor.black.setFill()
        let string: NSString = "Scale: \(scale)" as NSString
        let attributes = [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 40 / scale)]
        let size = string.size(withAttributes: attributes)
        string.draw(at: CGPoint(x: dirtyRect.midX - size.width / 2, y: dirtyRect.midY - size.height / 2),
                    withAttributes: attributes)
    }
    
}

当前绘图上下文已经缩放以匹配当前缩放级别(并且 dirtyRect 会随着每个细节级别的降低而变得越来越小)。如果需要,您可以从 CGContext 的转换矩阵中提取当前比例,如上所示。