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 的转换矩阵中提取当前比例,如上所示。
我在 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 的转换矩阵中提取当前比例,如上所示。