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

花了一些时间,但我想我终于想通了一切或几乎一切。

  1. 首先是一些科学知识:圆或弧不能用贝塞尔曲线表示。这是一条 属性 的贝塞尔曲线,如下所述:https://en.wikipedia.org/wiki/Bézier_curve

    因此,当使用 NSBezierPath(ovalInRect:) 时,您实际上是在生成贝塞尔曲线 近似 一个圆。这可能会导致外观和形状呈现方式的差异。在我们的案例中这仍然不是问题,因为区别在于两条贝塞尔曲线(Playground 中的一条和真实 OS X 项目中的一条),但我仍然发现它很有趣,以防您认为圆圈不够完美

  2. 其次,如本 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;
    

    在这里查看不同之处:

    另一个解决方案是利用角半径而不是贝塞尔路径来模拟一个圆,这次它非常准确:

  3. 最后,我对 Playground 和真正的 OS X 项目的区别的假设是 Apple 配置了 Playground 以便关闭一些优化,因此甚至认为使用 CAShapeLayer 绘制的路径可以获得最佳的抗锯齿效果。毕竟你是在做原型,性能并不是很重要,尤其是在绘图操作上。

    我不确定这一点是否正确,但我认为这不足为奇。如果有人有任何来源,我很乐意添加。

对我来说,如果你真的需要最好的圆,最好的解决方案是使用圆角半径。

也如 @Bannings 在对此 post 的另一个回答中所述。由于游乐场在不同的坐标系中渲染,因此阴影会反转。请参阅他的回答以解决此问题。