osx – CustomView在我的项目中看起来很奇怪,但在操场上很好

所以我创建了一个自定义的NSButton来有一个漂亮的单选按钮,但我遇到一个非常奇怪的bug.

我的单选按钮在操场上看起来不错,但是当我把它添加到我的项目中时,看起来很奇怪.

以下是截图:


左=在操场上.
右=在我的项目.

正如你可以看到的,在右边(在我的项目中),蓝点看起来很可怕,它对于白色的圆圈来说并不光滑,同样的东西(这在黑暗的背景下不太明显).

在我的项目中,我的CALayer上的NSShadow也被翻转,即使我的main(_containerLayer_)CALayer上的geometryFlipped属性设置为true. – >固定:请参阅@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,points[0].y)
                    didClosePath = false
                case .CurvetoBezierpathelement:
                    CGPathAddCurvetoPoint(path,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,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,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)

我在我的项目和操场上都使用完全相同的代码.
Here is a zip包含游乐场和项目.

只是为了清楚:我想知道为什么圈子图画在操场上顺利,但不是在项目中. (参见@Bannings答案,他的截图更明显)

花时间,但我想我终于想出了一切或几乎所有的东西.

>首先一些科学:圆形或圆弧不能通过贝塞尔曲线来表示.这是Bézier曲线的属性,如下所示: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的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;

请看这里的差异:

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


>最后我假设游乐场和真正的OS X项目是苹果公司配置的Playground,以便一些优化被关闭,所以使用CAShapeLayer甚至想到的路径可以获得最佳的抗锯齿功能.毕竟你是原型,表演并不重要,特别是绘画操作.

我不知道这是对的,但我认为这并不奇怪.如果有人有任何来源,我乐意添加它.

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

还有@Bannings在这文章的另一个答案中说过.由于在不同的坐标系中渲染的事实,阴影是相反的.看到他的答案来解决这个问题.

相关文章

软件简介:蓝湖辅助工具,减少移动端开发中控件属性的复制和粘...
现实生活中,我们听到的声音都是时间连续的,我们称为这种信...
前言最近在B站上看到一个漂亮的仙女姐姐跳舞视频,循环看了亿...
【Android App】实战项目之仿抖音的短视频分享App(附源码和...
前言这一篇博客应该是我花时间最多的一次了,从2022年1月底至...
因为我既对接过session、cookie,也对接过JWT,今年因为工作...