CustomView 在我的项目中看起来很奇怪,但在操场上很好
CustomView looks odd in my project, but good in the playground
所以我创建了一个自定义 NSButton
来拥有一个漂亮的单选按钮,但是我遇到了一个非常奇怪的错误。
我的单选按钮在 playground 中看起来不错,但是当我将它添加到我的项目中时,它看起来很奇怪。
截图如下:
左 = 在游乐场。
对 = 在我的项目.
如你所见,在右侧(在我的项目中),蓝点看起来很可怕,它不是平滑,白色圆圈也一样(在深色背景下不太明显)。
在我的项目中,我的 CALayer
上的 NSShadow
也翻转了,即使我的主 (_containerLayer_) 上的 geometryFlipped
属性 CALayer
设置为 true
。 -> FIXED:请参阅@Bannings 答案。
import AppKit
extension NSColor {
static func colorWithDecimal(deviceRed deviceRed: Int, deviceGreen: Int, deviceBlue: Int, alpha: Float) -> NSColor {
return NSColor(
deviceRed: CGFloat(Double(deviceRed)/255.0),
green: CGFloat(Double(deviceGreen)/255.0),
blue: CGFloat(Double(deviceBlue)/255.0),
alpha: CGFloat(alpha)
)
}
}
extension NSBezierPath {
var CGPath: CGPathRef {
return self.toCGPath()
}
/// Transforms the NSBezierPath into a CGPathRef
///
/// :returns: The transformed NSBezierPath
private func toCGPath() -> CGPathRef {
// Create path
let path = CGPathCreateMutable()
var points = UnsafeMutablePointer<NSPoint>.alloc(3)
let numElements = self.elementCount
if numElements > 0 {
var didClosePath = true
for index in 0..<numElements {
let pathType = self.elementAtIndex(index, associatedPoints: points)
switch pathType {
case .MoveToBezierPathElement:
CGPathMoveToPoint(path, nil, points[0].x, points[0].y)
case .LineToBezierPathElement:
CGPathAddLineToPoint(path, nil, points[0].x, points[0].y)
didClosePath = false
case .CurveToBezierPathElement:
CGPathAddCurveToPoint(path, nil, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y)
didClosePath = false
case .ClosePathBezierPathElement:
CGPathCloseSubpath(path)
didClosePath = true
}
}
if !didClosePath { CGPathCloseSubpath(path) }
}
points.dealloc(3)
return path
}
}
class RadioButton: NSButton {
private var containerLayer: CALayer!
private var backgroundLayer: CALayer!
private var dotLayer: CALayer!
private var hoverLayer: CALayer!
required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupLayers(radioButtonFrame: CGRectZero)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let radioButtonFrame = CGRect(
x: 0,
y: 0,
width: frameRect.height,
height: frameRect.height
)
self.setupLayers(radioButtonFrame: radioButtonFrame)
}
override func drawRect(dirtyRect: NSRect) {
}
private func setupLayers(radioButtonFrame radioButtonFrame: CGRect) {
//// Enable view layer
self.wantsLayer = true
self.setupBackgroundLayer(radioButtonFrame)
self.setupDotLayer(radioButtonFrame)
self.setupHoverLayer(radioButtonFrame)
self.setupContainerLayer(radioButtonFrame)
}
private func setupContainerLayer(frame: CGRect) {
self.containerLayer = CALayer()
self.containerLayer.frame = frame
self.containerLayer.geometryFlipped = true
//// Mask
let mask = CAShapeLayer()
mask.path = NSBezierPath(ovalInRect: frame).CGPath
mask.fillColor = NSColor.blackColor().CGColor
self.containerLayer.mask = mask
self.containerLayer.addSublayer(self.backgroundLayer)
self.containerLayer.addSublayer(self.dotLayer)
self.containerLayer.addSublayer(self.hoverLayer)
self.layer!.addSublayer(self.containerLayer)
}
private func setupBackgroundLayer(frame: CGRect) {
self.backgroundLayer = CALayer()
self.backgroundLayer.frame = frame
self.backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
}
private func setupDotLayer(frame: CGRect) {
let dotFrame = frame.rectByInsetting(dx: 6, dy: 6)
let maskFrame = CGRect(origin: CGPointZero, size: dotFrame.size)
self.dotLayer = CALayer()
self.dotLayer.frame = dotFrame
self.dotLayer.shadowColor = NSColor.colorWithDecimal(deviceRed: 46, deviceGreen: 146, deviceBlue: 255, alpha: 1.0).CGColor
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
self.dotLayer.shadowOpacity = 0.4
self.dotLayer.shadowRadius = 2.0
//// Mask
let maskLayer = CAShapeLayer()
maskLayer.path = NSBezierPath(ovalInRect: maskFrame).CGPath
maskLayer.fillColor = NSColor.blackColor().CGColor
//// Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(origin: CGPointZero, size: dotFrame.size)
gradientLayer.colors = [
NSColor.colorWithDecimal(deviceRed: 29, deviceGreen: 114, deviceBlue: 253, alpha: 1.0).CGColor,
NSColor.colorWithDecimal(deviceRed: 59, deviceGreen: 154, deviceBlue: 255, alpha: 1.0).CGColor
]
gradientLayer.mask = maskLayer
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: maskFrame.rectByInsetting(dx: 0.5, dy: 0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.12).CGColor
strokeLayer.lineWidth = 1.0
self.dotLayer.addSublayer(gradientLayer)
self.dotLayer.addSublayer(strokeLayer)
}
private func setupHoverLayer(frame: CGRect) {
self.hoverLayer = CALayer()
self.hoverLayer.frame = frame
//// Inner Shadow
let innerShadowLayer = CAShapeLayer()
let ovalPath = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -10, dy: -10))
let cutout = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -1, dy: -1)).bezierPathByReversingPath
ovalPath.appendBezierPath(cutout)
innerShadowLayer.path = ovalPath.CGPath
innerShadowLayer.shadowColor = NSColor.blackColor().CGColor
innerShadowLayer.shadowOpacity = 0.2
innerShadowLayer.shadowRadius = 2.0
innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
self.hoverLayer.addSublayer(innerShadowLayer)
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -0.5, dy: -0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.22).CGColor
strokeLayer.lineWidth = 2.0
self.hoverLayer.addSublayer(strokeLayer)
}
}
let rbFrame = NSRect(
x: 87,
y: 37,
width: 26,
height: 26
)
let viewFrame = CGRect(
x: 0,
y: 0,
width: 200,
height: 100
)
let view = NSView(frame: viewFrame)
view.wantsLayer = true
view.layer!.backgroundColor = NSColor.colorWithDecimal(deviceRed: 40, deviceGreen: 40, deviceBlue: 40, alpha: 1.0).CGColor
let rb = RadioButton(frame: rbFrame)
view.addSubview(rb)
我在我的项目和 playground 中使用完全相同的代码。
Here is a zip 包含游乐场和项目。
明确一点:我想知道为什么圆图在操场上很流畅,但在项目中却不流畅。 (看@Bannings的回答,他的截图更明显)
我只是通过替换这行代码来修复它:
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
与:
self.dotLayer.shadowOffset = CGSize(width: 0, height: -2)
并将 innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
替换为:
innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2)
我想你会得到同样的结果:
左 = 在游乐场。
对 = 在我的项目.
好像是Playground
显示的是LLO坐标系下的bezier路径,你可以访问link:
https://forums.developer.apple.com/message/39277#39277
花了一些时间,但我想我终于想通了一切或几乎一切。
首先是一些科学知识:圆或弧不能用贝塞尔曲线表示。这是一条 属性 的贝塞尔曲线,如下所述:https://en.wikipedia.org/wiki/Bézier_curve
因此,当使用 NSBezierPath(ovalInRect:)
时,您实际上是在生成贝塞尔曲线 近似 一个圆。这可能会导致外观和形状呈现方式的差异。在我们的案例中这仍然不是问题,因为区别在于两条贝塞尔曲线(Playground 中的一条和真实 OS X 项目中的一条),但我仍然发现它很有趣,以防您认为圆圈不够完美。
其次,如本 question (How to draw a smooth circle with CAShapeLayer and UIBezierPath) 所述,根据路径的使用位置,抗锯齿应用于路径的方式会有所不同。 NSView's
drawRect:
是路径抗锯齿 最佳 和 CAShapeLayer
是 最差 的地方.
我还发现 CAShapeLayer documentation 有一条注释是这样说的:
Shape rasterization may favor speed over accuracy. For example, pixels with multiple intersecting path segments may not give exact results.
Glen Low 对我之前提到的问题的回答在我们的案例中似乎很管用:
layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor;
layer.shouldRasterize = true;
在这里查看不同之处:
另一个解决方案是利用角半径而不是贝塞尔路径来模拟一个圆,这次它非常准确:
最后,我对 Playground 和真正的 OS X 项目的区别的假设是 Apple 配置了 Playground 以便关闭一些优化,因此甚至认为使用 CAShapeLayer
绘制的路径可以获得最佳的抗锯齿效果。毕竟你是在做原型,性能并不是很重要,尤其是在绘图操作上。
我不确定这一点是否正确,但我认为这不足为奇。如果有人有任何来源,我很乐意添加。
对我来说,如果你真的需要最好的圆,最好的解决方案是使用圆角半径。
也如 @Bannings 在对此 post 的另一个回答中所述。由于游乐场在不同的坐标系中渲染,因此阴影会反转。请参阅他的回答以解决此问题。
所以我创建了一个自定义 NSButton
来拥有一个漂亮的单选按钮,但是我遇到了一个非常奇怪的错误。
我的单选按钮在 playground 中看起来不错,但是当我将它添加到我的项目中时,它看起来很奇怪。
截图如下:
左 = 在游乐场。
对 = 在我的项目.
如你所见,在右侧(在我的项目中),蓝点看起来很可怕,它不是平滑,白色圆圈也一样(在深色背景下不太明显)。
在我的项目中,我的 CALayer
上的 NSShadow
也翻转了,即使我的主 (_containerLayer_) 上的 geometryFlipped
属性 CALayer
设置为 true
。 -> FIXED:请参阅@Bannings 答案。
import AppKit
extension NSColor {
static func colorWithDecimal(deviceRed deviceRed: Int, deviceGreen: Int, deviceBlue: Int, alpha: Float) -> NSColor {
return NSColor(
deviceRed: CGFloat(Double(deviceRed)/255.0),
green: CGFloat(Double(deviceGreen)/255.0),
blue: CGFloat(Double(deviceBlue)/255.0),
alpha: CGFloat(alpha)
)
}
}
extension NSBezierPath {
var CGPath: CGPathRef {
return self.toCGPath()
}
/// Transforms the NSBezierPath into a CGPathRef
///
/// :returns: The transformed NSBezierPath
private func toCGPath() -> CGPathRef {
// Create path
let path = CGPathCreateMutable()
var points = UnsafeMutablePointer<NSPoint>.alloc(3)
let numElements = self.elementCount
if numElements > 0 {
var didClosePath = true
for index in 0..<numElements {
let pathType = self.elementAtIndex(index, associatedPoints: points)
switch pathType {
case .MoveToBezierPathElement:
CGPathMoveToPoint(path, nil, points[0].x, points[0].y)
case .LineToBezierPathElement:
CGPathAddLineToPoint(path, nil, points[0].x, points[0].y)
didClosePath = false
case .CurveToBezierPathElement:
CGPathAddCurveToPoint(path, nil, points[0].x, points[0].y, points[1].x, points[1].y, points[2].x, points[2].y)
didClosePath = false
case .ClosePathBezierPathElement:
CGPathCloseSubpath(path)
didClosePath = true
}
}
if !didClosePath { CGPathCloseSubpath(path) }
}
points.dealloc(3)
return path
}
}
class RadioButton: NSButton {
private var containerLayer: CALayer!
private var backgroundLayer: CALayer!
private var dotLayer: CALayer!
private var hoverLayer: CALayer!
required init?(coder: NSCoder) {
super.init(coder: coder)
self.setupLayers(radioButtonFrame: CGRectZero)
}
override init(frame frameRect: NSRect) {
super.init(frame: frameRect)
let radioButtonFrame = CGRect(
x: 0,
y: 0,
width: frameRect.height,
height: frameRect.height
)
self.setupLayers(radioButtonFrame: radioButtonFrame)
}
override func drawRect(dirtyRect: NSRect) {
}
private func setupLayers(radioButtonFrame radioButtonFrame: CGRect) {
//// Enable view layer
self.wantsLayer = true
self.setupBackgroundLayer(radioButtonFrame)
self.setupDotLayer(radioButtonFrame)
self.setupHoverLayer(radioButtonFrame)
self.setupContainerLayer(radioButtonFrame)
}
private func setupContainerLayer(frame: CGRect) {
self.containerLayer = CALayer()
self.containerLayer.frame = frame
self.containerLayer.geometryFlipped = true
//// Mask
let mask = CAShapeLayer()
mask.path = NSBezierPath(ovalInRect: frame).CGPath
mask.fillColor = NSColor.blackColor().CGColor
self.containerLayer.mask = mask
self.containerLayer.addSublayer(self.backgroundLayer)
self.containerLayer.addSublayer(self.dotLayer)
self.containerLayer.addSublayer(self.hoverLayer)
self.layer!.addSublayer(self.containerLayer)
}
private func setupBackgroundLayer(frame: CGRect) {
self.backgroundLayer = CALayer()
self.backgroundLayer.frame = frame
self.backgroundLayer.backgroundColor = NSColor.whiteColor().CGColor
}
private func setupDotLayer(frame: CGRect) {
let dotFrame = frame.rectByInsetting(dx: 6, dy: 6)
let maskFrame = CGRect(origin: CGPointZero, size: dotFrame.size)
self.dotLayer = CALayer()
self.dotLayer.frame = dotFrame
self.dotLayer.shadowColor = NSColor.colorWithDecimal(deviceRed: 46, deviceGreen: 146, deviceBlue: 255, alpha: 1.0).CGColor
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
self.dotLayer.shadowOpacity = 0.4
self.dotLayer.shadowRadius = 2.0
//// Mask
let maskLayer = CAShapeLayer()
maskLayer.path = NSBezierPath(ovalInRect: maskFrame).CGPath
maskLayer.fillColor = NSColor.blackColor().CGColor
//// Gradient
let gradientLayer = CAGradientLayer()
gradientLayer.frame = CGRect(origin: CGPointZero, size: dotFrame.size)
gradientLayer.colors = [
NSColor.colorWithDecimal(deviceRed: 29, deviceGreen: 114, deviceBlue: 253, alpha: 1.0).CGColor,
NSColor.colorWithDecimal(deviceRed: 59, deviceGreen: 154, deviceBlue: 255, alpha: 1.0).CGColor
]
gradientLayer.mask = maskLayer
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: maskFrame.rectByInsetting(dx: 0.5, dy: 0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.12).CGColor
strokeLayer.lineWidth = 1.0
self.dotLayer.addSublayer(gradientLayer)
self.dotLayer.addSublayer(strokeLayer)
}
private func setupHoverLayer(frame: CGRect) {
self.hoverLayer = CALayer()
self.hoverLayer.frame = frame
//// Inner Shadow
let innerShadowLayer = CAShapeLayer()
let ovalPath = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -10, dy: -10))
let cutout = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -1, dy: -1)).bezierPathByReversingPath
ovalPath.appendBezierPath(cutout)
innerShadowLayer.path = ovalPath.CGPath
innerShadowLayer.shadowColor = NSColor.blackColor().CGColor
innerShadowLayer.shadowOpacity = 0.2
innerShadowLayer.shadowRadius = 2.0
innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
self.hoverLayer.addSublayer(innerShadowLayer)
//// Inner Stroke
let strokeLayer = CAShapeLayer()
strokeLayer.path = NSBezierPath(ovalInRect: frame.rectByInsetting(dx: -0.5, dy: -0.5)).CGPath
strokeLayer.fillColor = NSColor.clearColor().CGColor
strokeLayer.strokeColor = NSColor.blackColor().colorWithAlphaComponent(0.22).CGColor
strokeLayer.lineWidth = 2.0
self.hoverLayer.addSublayer(strokeLayer)
}
}
let rbFrame = NSRect(
x: 87,
y: 37,
width: 26,
height: 26
)
let viewFrame = CGRect(
x: 0,
y: 0,
width: 200,
height: 100
)
let view = NSView(frame: viewFrame)
view.wantsLayer = true
view.layer!.backgroundColor = NSColor.colorWithDecimal(deviceRed: 40, deviceGreen: 40, deviceBlue: 40, alpha: 1.0).CGColor
let rb = RadioButton(frame: rbFrame)
view.addSubview(rb)
我在我的项目和 playground 中使用完全相同的代码。
Here is a zip 包含游乐场和项目。
明确一点:我想知道为什么圆图在操场上很流畅,但在项目中却不流畅。 (看@Bannings的回答,他的截图更明显)
我只是通过替换这行代码来修复它:
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
与:
self.dotLayer.shadowOffset = CGSize(width: 0, height: -2)
并将 innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
替换为:
innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2)
我想你会得到同样的结果:
左 = 在游乐场。
对 = 在我的项目.
好像是Playground
显示的是LLO坐标系下的bezier路径,你可以访问link:
https://forums.developer.apple.com/message/39277#39277
花了一些时间,但我想我终于想通了一切或几乎一切。
首先是一些科学知识:圆或弧不能用贝塞尔曲线表示。这是一条 属性 的贝塞尔曲线,如下所述:https://en.wikipedia.org/wiki/Bézier_curve
因此,当使用
NSBezierPath(ovalInRect:)
时,您实际上是在生成贝塞尔曲线 近似 一个圆。这可能会导致外观和形状呈现方式的差异。在我们的案例中这仍然不是问题,因为区别在于两条贝塞尔曲线(Playground 中的一条和真实 OS X 项目中的一条),但我仍然发现它很有趣,以防您认为圆圈不够完美。其次,如本 question (How to draw a smooth circle with CAShapeLayer and UIBezierPath) 所述,根据路径的使用位置,抗锯齿应用于路径的方式会有所不同。
NSView's
drawRect:
是路径抗锯齿 最佳 和CAShapeLayer
是 最差 的地方.我还发现 CAShapeLayer documentation 有一条注释是这样说的:
Shape rasterization may favor speed over accuracy. For example, pixels with multiple intersecting path segments may not give exact results.
Glen Low 对我之前提到的问题的回答在我们的案例中似乎很管用:
layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor; layer.shouldRasterize = true;
在这里查看不同之处:
另一个解决方案是利用角半径而不是贝塞尔路径来模拟一个圆,这次它非常准确:
最后,我对 Playground 和真正的 OS X 项目的区别的假设是 Apple 配置了 Playground 以便关闭一些优化,因此甚至认为使用
CAShapeLayer
绘制的路径可以获得最佳的抗锯齿效果。毕竟你是在做原型,性能并不是很重要,尤其是在绘图操作上。我不确定这一点是否正确,但我认为这不足为奇。如果有人有任何来源,我很乐意添加。
对我来说,如果你真的需要最好的圆,最好的解决方案是使用圆角半径。
也如 @Bannings 在对此 post 的另一个回答中所述。由于游乐场在不同的坐标系中渲染,因此阴影会反转。请参阅他的回答以解决此问题。