2015-08-12 16 views
15

Quindi ho creato un custom NSButton per avere un bellissimo pulsante radio, ma sto vivendo un bug molto strano.CustomView sembra strano nel mio progetto, ma buono nel parco giochi

Il pulsante di opzione sembra buono nel parco giochi, ma quando lo aggiungo al mio progetto, sembra strano.

Ecco le immagini:

radio button radio button x400

Sinistra = nel parco giochi .
Destra = nel mio progetto .

Come si può vedere, sulla destra (nel mio progetto), il punto blu sembra orribile, non è liscia, stessa cosa per il cerchio bianco (è meno visibile con lo sfondo scuro).

Nel mio progetto, il NSShadow sul mio CALayer è anche capovolto, anche se la proprietà geometryFlipped sul mio principale (_containerLayer_) CALayer è impostato su true. ->FISSO: vedere la risposta @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) 

Sto usando lo stesso codice esatto sia nel mio progetto che nel parco giochi.
Here is a zip contenente il parco giochi e il progetto.

Giusto per essere chiari: voglio sapere perché i disegni dei cerchi sono fluidi nel parco giochi ma non nei progetti. (Vedere @Bannings risposta, è più evidente con i suoi screenshots)

+0

Controllare la mia risposta aggiornata. – Bannings

risposta

16

preso tempo, ma penso che ho finalmente capito tutto o quasi tutto.

  1. Prima alcune scienza: cerchi o archi non possono essere rappresentati mediante curve di Bezier. Questa è una proprietà di curve di Bézier come indicato qui: https://en.wikipedia.org/wiki/Bézier_curve

    Così quando si utilizza NSBezierPath(ovalInRect:) si sono infatti generare una curva di Bézier approssimando un cerchio. Ciò può comportare una differenza nell'aspetto e il modo in cui la forma viene visualizzata. Tuttavia questo non dovrebbe essere un problema nel nostro caso perché la differenza è tra due curve di Bézier (quella in Playground e quella in un vero progetto OS X), ma comunque la trovo interessante da notare nel caso in cui pensi che il cerchio non sia perfetto sufficiente.

  2. Secondo come indicato in questo question (How to draw a smooth circle with CAShapeLayer and UIBezierPath) ci sono differenze nel modo in cui l'antialias si applicherà al percorso a seconda di dove viene utilizzato il percorso. drawRect: è il punto in cui l'antialiasing del percorso sarà il migliore e CAShapeLayerpeggiore.

    Inoltre ho scoperto che il CAShapeLayer documentation ha una nota dicendo questo:

    Forma rasterizzazione può favorire la velocità rispetto precisione. Ad esempio, i pixel con più segmenti di intersezione del percorso potrebbero non fornire risultati esatti.

    risposta di

    Glen bassa alla domanda che ho menzionato in precedenza sembra funzionare bene nel nostro caso:

    layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor; 
    layer.shouldRasterize = true; 
    

    Vedi le differenze qui:

    Simple Bézier path

    Bézier path with previously mentioned fix

    altro la soluzione è sfruttare i raggi d'angolo invece del percorso di Bézier s in modo da simulare un cerchio, e questa volta è abbastanza preciso:

    Using radiuses

  3. Infine la mia ipotesi sulla differenza tra il & progetto Playground reale OS X è che Apple configurato Playground in modo che alcune ottimizzazioni siano disattivate in modo che il percorso sia anche disegnato utilizzando CAShapeLayer per ottenere il migliore anti-aliasing possibile. Dopotutto tu stai prototipando, le prestazioni non sono davvero importanti, specialmente nelle operazioni di disegno.

    Non sono sicuro di avere ragione su questo, ma penso che non sarebbe sorprendente. Se qualcuno avesse qualche fonte, lo aggiungerei volentieri.

Per me la soluzione migliore se si ha realmente bisogno il miglior cerchio possibile è quella di utilizzare raggi d'angolo.

anche come affermato da @Bannings in un'altra risposta a questo post. Le ombre sono invertite a causa del rendering del campo di gioco in un sistema di coordinate diverso. Guarda la sua risposta per risolvere questo problema.

+2

Grazie per la risposta. Molto completo, lo apprezzo. Ora utilizzo il metodo dei raggi d'angolo e funziona alla grande. –

9

ho appena risolto il tutto per sostituire il codice di linea:

self.dotLayer.shadowOffset = CGSize(width: 0, height: 2) 

con:

self.dotLayer.shadowOffset = CGSize(width: 0, height: -2) 

e sostituire innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2) con:

innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2) 

Penso che otterrete lo stesso risultato in questo modo:

enter image description here enter image description here

Sinistra = nel parco giochi .
Destra = nel mio progetto .

Sembra che il Playground sta mostrando il percorso di Bezier a Llo sistema di coordinate, è possibile visitare il link: https://forums.developer.apple.com/message/39277#39277

+0

Questo sembra risolvere il problema con l'ombra invertita. E il problema del livellamento del disegno? Qualche suggerimento su questo? – Fantattitude

+0

Hai risolto il problema dell'ombra ma non è quello principale. Come puoi vedere chiaramente sui tuoi screenshot, nel parco giochi, i cerchi sembrano davvero belli, sono fluidi, ma guarda lo screenshot giusto, i cerchi sembrano strani, non sono fluidi. –

+0

@Fantattitude Sì, penso di sì – Bannings