CustomView выглядит странно в моем проекте, но хорош на игровой площадке
Итак, я создал пользовательский NSButton
, чтобы иметь красивый переключатель, но я испытываю очень странную ошибку.
Моя радиокнопка хорошо выглядит на игровой площадке, но когда я добавляю ее в свой проект, она выглядит странно.
Вот скриншоты:
![radio button x400]()
Левая= на игровой площадке.
Правильно= в моем проекте.
Как вы можете видеть, в вправо (в моем проекте) синяя точка выглядит ужасно, она не гладкая, то же самое для белого круга (он менее заметен с темным фоном).
В моем проекте NSShadow
на моем CALayer
также перевернуто, даже если свойство geometryFlipped
на моем основном (_containerLayer_) CALayer
установлено на 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, 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)
Я использую тот же самый код как для моего проекта, так и на игровой площадке.
Вот zip, содержащий игровую площадку и проект.
Просто для того, чтобы быть ясным: я хочу знать, почему рисунки кругов гладкие на игровой площадке, но не в проектах. (См. @Bannings ответ, это более очевидно с его скриншотами)
Ответы
Ответ 1
Пришло время, но я думаю, что, наконец, понял все или почти все.
-
Сначала наука: Круги или дуги не могут быть представлены через кривые Безье. Это свойство кривых Безье, как указано здесь: https://en.wikipedia.org/wiki/Bézier_curve
Таким образом, при использовании NSBezierPath(ovalInRect:)
вы на самом деле генерируете кривую Безье , приближающую круг. Это может привести к различию внешнего вида и способа формирования фигуры. Тем не менее это не должно быть проблемой в нашем случае, потому что разница между двумя кривыми Безье (одна в Playground и одна в реальном проекте OS X), но все же мне интересно отметить, если вы считаете, что круг не достаточно хорошо.
-
Во-вторых, как указано в этом вопросе (Как нарисовать гладкий круг с CAShapeLayer и UIBezierPath) существуют различия в том, как сглаживание будет применяться к вашему путь в зависимости от того, где используется путь. NSView's
drawRect:
является местом, где сглаживание пути будет лучшим, а CAShapeLayer
- наихудшим.
Также я обнаружил, что документация CAShapeLayer имеет примечание, в котором говорится следующее:
Растеризация формы может способствовать ускорению по точности. Например, пиксели с несколькими пересекающимися сегментами пути могут не давать точных результатов.
Глен Низкий ответ на вопрос, который я ранее упомянул, кажется, отлично работает в нашем случае:
layer.rasterizationScale = 2.0 * self.window!.screen!.backingScaleFactor;
layer.shouldRasterize = true;
См. различия здесь:
![Простой путь Безье]()
![Путь Bézier с ранее упомянутым исправлением]()
Другим решением является использование угловых радиусов вместо путей Безье для имитации круга, и на этот раз это довольно точно:
![Использование радиусов]()
-
Наконец, мое предположение о различии между Playground и реальным проектом OS X заключается в том, что Apple настроил Игровая площадка, чтобы некоторые оптимизации были отключены, поэтому путь даже мысль, проведенная с использованием CAShapeLayer
, дает лучшее сглаживание. В конце концов вы прототипируете, выступления не очень важны, особенно при рисовании.
Я не уверен, что я прав, но думаю, что это не удивительно. Если у кого-то есть источник, я бы с радостью добавил его.
Для меня лучшим решением, если вам действительно нужен лучший круг, является использование угловых радиусов.
Также как указано @Bannings в другом ответе на этот пост. Тени меняются вспять из-за того, что игровая площадка воспроизводится в другой системе координат. См. Его ответ, чтобы исправить это.
Ответ 2
Я просто исправил его, заменив этот код строки:
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, вы можете перейти по ссылке:
https://forums.developer.apple.com/message/39277#39277