macos - CustomView parece extraño en mi proyecto, pero bueno en el patio de recreo
swift cocoa (2)
Lo arreglé por reemplazar este código de línea:
self.dotLayer.shadowOffset = CGSize(width: 0, height: 2)
con:
self.dotLayer.shadowOffset = CGSize(width: 0, height: -2)
y reemplace innerShadowLayer.shadowOffset = CGSize(width: 0, height: 2)
con:
innerShadowLayer.shadowOffset = CGSize(width: 0, height: -2)
Creo que obtendrás el mismo resultado así:
Izquierda = en el patio de recreo .
Derecha = en mi proyecto .
Parece que el Playground
muestra la ruta Bezier en el sistema de coordenadas LLO, puede visitar el enlace: https://forums.developer.apple.com/message/39277#39277
Así que he creado un NSButton
personalizado para tener un hermoso botón de NSButton
, pero estoy experimentando un error muy extraño.
Mi botón de radio se ve bien en el patio de recreo, pero cuando lo agrego a mi proyecto, parece extraño.
Aquí hay capturas de pantalla:
Izquierda = en el patio de recreo .
Derecha = en mi proyecto .
Como puede ver, a la derecha (en mi proyecto), el punto azul se ve horrible , no es liso , lo mismo para el círculo blanco (es menos visible con el fondo oscuro).
En mi proyecto, NSShadow
en mi CALayer
también se voltea, incluso si la propiedad geometryFlipped
en mi CALayer principal (_containerLayer_) se establece en true
. -> CORREGIDO: ver respuesta @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)
Estoy usando exactamente el mismo código en mi proyecto y en el patio de recreo.
Aquí hay un zip que contiene el patio de recreo y el proyecto.
Para que quede claro: quiero saber por qué los dibujos circulares son suaves en el patio de recreo pero no en los proyectos. (Ver la respuesta de @Bannings, es más obvio con sus capturas de pantalla)
Tomó tiempo pero creo que finalmente descubrí todo o casi todo.
Primero algo de ciencia: los círculos o arcos no se pueden representar a través de las curvas de Bézier. Esa es una propiedad de las curvas de Bézier como se indica aquí: https://en.wikipedia.org/wiki/Bézier_curve
Por lo tanto, al usar
NSBezierPath(ovalInRect:)
, de hecho estás generando una curva de Bézier que se aproxima a un círculo. Esto puede conducir a una diferencia en apariencia y a cómo se renderiza la forma. Aún así, esto no debería ser un problema en nuestro caso porque la diferencia está entre dos curvas de Bézier (la de Playground y la de un proyecto de OS X real), pero aún así me parece interesante observar en caso de que creas que el círculo no es lo suficientemente perfecto .En segundo lugar, como se indica en esta pregunta (Cómo dibujar un círculo suave con CAShapeLayer y UIBezierPath), existen diferencias en la forma en que se aplicará el antialiasing a su ruta, dependiendo de dónde se usa la ruta.
NSView''s
drawRect:
siendo el lugar donde el antialiasing de la ruta será el mejor yCAShapeLayer
será el peor .También encontré que la documentación de CAShapeLayer tiene una nota que dice esto:
La rasterización de formas puede favorecer la velocidad sobre la precisión. Por ejemplo, los píxeles con múltiples segmentos de ruta de intersección pueden no dar resultados exactos.
La respuesta de Glen Low a la pregunta que mencioné anteriormente parece funcionar bien en nuestro caso:
layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor; layer.shouldRasterize = true;
Vea las diferencias aquí:
Otra solución es aprovechar los radios de esquina en lugar de los caminos de Bézier para simular un círculo, y esta vez es bastante preciso:
Por último, mi suposición sobre la diferencia entre Playground y el proyecto real OS X es que Apple configuró Playground para que algunas optimizaciones se apaguen, por lo que la ruta, incluso pensada con
CAShapeLayer
obtiene el mejor anti-aliasing posible. Después de todo lo que está haciendo prototipos, las actuaciones no son realmente importantes, especialmente en las operaciones de dibujo.No estoy seguro de estar en lo cierto, pero creo que no sería sorprendente. Si alguien tiene alguna fuente, felizmente la agregaré.
Para mí, la mejor solución si realmente necesitas el mejor círculo posible es usar radios de esquina.
También según lo indicado por @Bannings en otra respuesta a esta publicación. Las sombras se invierten debido a que el patio de recreo se renderiza en un sistema de coordenadas diferente. Vea su respuesta para arreglar esto.