Как сделать гладкое, округленное, похожее на объем окно OS X с NSVisualEffectView?
В настоящее время я пытаюсь сделать окно, которое выглядит как окно Volume OS X:
Для этого у меня есть свой NSWindow
(используя пользовательский подкласс), который является прозрачным / без заголовка / без тени, который имеет NSVisualEffectView
внутри его contentView. Вот код моего подкласса, чтобы сделать просмотр содержимого круглым:
- (void)setContentView:(NSView *)aView {
aView.wantsLayer = YES;
aView.layer.frame = aView.frame;
aView.layer.cornerRadius = 14.0;
aView.layer.masksToBounds = YES;
[super setContentView:aView];
}
И вот результат (как вы можете видеть, углы зернистые, OS X более гладкие):
Есть идеи, как сделать углы более гладкими? Спасибо
5 ответов
Обновление для OS X El Capitan
Взлом, который я описал в моем первоначальном ответе ниже, больше не нужен на OS X El Capitan. NSVisualEffectView
"s maskImage
там должно работать правильно, если NSWindow
"s contentView
установлен быть NSVisualEffectView
(недостаточно, если это подпредставление contentView
).
Вот пример проекта: https://github.com/marcomasser/OverlayTest
Оригинальный ответ - актуально только для OS X Yosemite
Я нашел способ сделать это путем переопределения частного метода NSWindow: - (NSImage *)_cornerMask
, Просто верните изображение, созданное путем рисования NSBezierPath с закругленным прямоугольником, чтобы получить вид, аналогичный окну громкости OS X.
В моем тестировании я обнаружил, что вам нужно использовать изображение маски для NSVisualEffectView и NSWindow. В вашем коде вы используете слой представления cornerRadius
свойство, чтобы получить закругленные углы, но вы можете добиться того же, используя маску изображения. В моем коде я генерирую NSImage, который используется как NSVisualEffectView, так и NSWindow:
func maskImage(#cornerRadius: CGFloat) -> NSImage {
let edgeLength = 2.0 * cornerRadius + 1.0
let maskImage = NSImage(size: NSSize(width: edgeLength, height: edgeLength), flipped: false) { rect in
let bezierPath = NSBezierPath(roundedRect: rect, xRadius: cornerRadius, yRadius: cornerRadius)
NSColor.blackColor().set()
bezierPath.fill()
return true
}
maskImage.capInsets = NSEdgeInsets(top: cornerRadius, left: cornerRadius, bottom: cornerRadius, right: cornerRadius)
maskImage.resizingMode = .Stretch
return maskImage
}
Затем я создал подкласс NSWindow с установщиком для маскирующего изображения:
class MaskedWindow : NSWindow {
/// Just in case Apple decides to make `_cornerMask` public and remove the underscore prefix,
/// we name the property `cornerMask`.
@objc dynamic var cornerMask: NSImage?
/// This private method is called by AppKit and should return a mask image that is used to
/// specify which parts of the window are transparent. This works much better than letting
/// the window figure it out by itself using the content view's shape because the latter
/// method makes rounded corners appear jagged while using `_cornerMask` respects any
/// anti-aliasing in the mask image.
@objc dynamic func _cornerMask() -> NSImage? {
return cornerMask
}
}
Затем в моем подклассе NSWindowController я установил изображение маски для вида и окна:
class OverlayWindowController : NSWindowController {
@IBOutlet weak var visualEffectView: NSVisualEffectView!
override func windowDidLoad() {
super.windowDidLoad()
let maskImage = maskImage(cornerRadius: 18.0)
visualEffectView.maskImage = maskImage
if let window = window as? MaskedWindow {
window.cornerMask = maskImage
}
}
}
Я не знаю, что Apple сделает, если вы отправите приложение с этим кодом в App Store. Вы на самом деле не вызываете какой-либо частный API, вы просто переопределяете метод, имя которого совпадает с именем частного метода в AppKit. Как вы должны знать, что существует конфликт имен?
Кроме того, это безуспешно, без необходимости что-либо делать. Если Apple изменит способ, которым это работает внутри, и метод просто не будет вызван, ваше окно не получит красивые закругленные углы, но все по-прежнему работает и выглядит почти так же.
Если вам интересно, как я узнал об этом методе:
Я знал, что индикатор громкости OS X сделал то, что я хочу, и я надеялся, что изменение громкости, как сумасшедший, привело к заметному использованию процессора процессом, который выводит эту индикацию громкости на экран. Поэтому я открыл Activity Monitor, отсортированный по загрузке процессора, активировал фильтр, отображающий только "Мои процессы", и нажал на клавиши увеличения / уменьшения громкости.
Стало ясно, что coreaudiod
и что-то называется BezelUIServer
в /System/Library/LoginPlugins/BezelServices.loginPlugin/Contents/Resources/BezelUI/BezelUIServer
сделал что-то Из рассмотрения ресурсов пакета для последнего стало ясно, что он отвечает за составление индикации объема. (Примечание: этот процесс выполняется только в течение короткого времени после того, как он что-то отображает.)
Затем я использовал Xcode, чтобы присоединиться к этому процессу, как только он запустится (Debug > Attach to Process > By Process Identifier (PID) или Name..., затем введите "BezelUIServer") и снова изменил громкость. После того, как отладчик был присоединен, отладчик представления позволил мне взглянуть на иерархию представления и увидеть, что окно было экземпляром подкласса NSWindow с именем BSUIRoundWindow
,
С помощью class-dump
в двоичном коде показано, что этот класс является прямым потомком NSWindow и реализует только три метода, тогда как один - (id)_cornerMask
, что звучало многообещающе.
Вернувшись в XCode, я использовал инспектор объектов (правая сторона, третья вкладка), чтобы получить адрес для оконного объекта. Используя этот указатель, я проверил, что это _cornerMask
фактически возвращается, печатая его описание в lldb:
(lldb) po [0x108500110 _cornerMask]
<NSImage 0x608000070300 Size={37, 37} Reps=(
"NSCustomImageRep 0x608000082d50 Size={37, 37} ColorSpace=NSCalibratedRGBColorSpace BPS=0 Pixels=0x0 Alpha=NO"
)>
Это показывает, что возвращаемое значение на самом деле представляет собой NSImage, то есть информацию, необходимую для реализации _cornerMask
,
Если вы хотите взглянуть на это изображение, вы можете записать его в файл:
(lldb) e (BOOL)[[[0x108500110 _cornerMask] TIFFRepresentation] writeToFile:(id)[@"~/Desktop/maskImage.tiff" stringByExpandingTildeInPath] atomically:YES]
Чтобы копнуть немного глубже, вы можете использовать Hopper Disassembler, чтобы разобрать BezelUIServer
а также AppKit
и сгенерировать псевдокод, чтобы увидеть, как _cornerMask
реализуется и используется, чтобы получить более четкое представление о том, как работают внутренние органы. К сожалению, все, что касается этого механизма, является частным API.
Я помню, что делал такие вещи задолго до CALayer
был рядом. Ты используешь NSBezierPath
проложить путь.
Я не верю, что вам на самом деле нужно подкласс NSWindow
, Важным моментом в окне является инициализация окна с помощью NSBorderlessWindowMask
и примените следующие настройки:
[window setAlphaValue:0.5]; // whatever your desired opacity is
[window setOpaque:NO];
[window setHasShadow:NO];
Затем вы устанавливаете contentView
вашего окна на заказ NSView
подкласс с drawRect:
Переопределенный метод похож на этот:
// "erase" the window background
[[NSColor clearColor] set];
NSRectFill(self.frame);
// make a rounded rect and fill it with whatever color you like
NSBezierPath* clipPath = [NSBezierPath bezierPathWithRoundedRect:self.frame xRadius:14.0 yRadius:14.0];
[[NSColor blackColor] set]; // your bg color
[clipPath fill];
результат (игнорируйте ползунок):
Изменить: Если этот метод по какой-либо причине нежелателен, вы не можете просто назначить CAShapeLayer
как твой contentView
"s layer
затем либо преобразуйте вышеуказанное NSBezierPath
в CGPath
или просто построить как CGPath
и назначить путь к слоям path
?
"Эффект сглаживания", на который вы ссылаетесь, называется "Сглаживание". Я немного погуглил, и я думаю, что вы можете быть первым человеком, который попытался закруглить углы NSVisualEffectView. Вы сказали CALayer иметь радиус границы, который будет закруглять углы, но вы не установили никаких других параметров. Я бы попробовал это:
layer.shouldRasterize = YES;
layer.edgeAntialiasingMask = kCALayerLeftEdge | kCALayerRightEdge | kCALayerBottomEdge | kCALayerTopEdge;
Несмотря на ограничения NSVisualEffectView, не сглаживающие края, на данный момент есть хитрый обходной путь, который должен работать для этого приложения с плавающим окном без размера заголовка без тени без тени - иметь дочернее окно, которое рисует только края.
Я смог заставить мою выглядеть так:
сделав следующее:
В контроллере держит все:
- (void) create {
NSRect windowRect = NSMakeRect(100.0, 100.0, 200.0, 200.0);
NSRect behindWindowRect = NSMakeRect(99.0, 99.0, 202.0, 202.0);
NSRect behindViewRect = NSMakeRect(0.0, 0.0, 202.0, 202.0);
NSRect viewRect = NSMakeRect(0.0, 0.0, 200.0, 200.0);
window = [FloatingWindow createWindow:windowRect];
behindAntialiasWindow = [FloatingWindow createWindow:behindWindowRect];
roundedHollowView = [[RoundedHollowView alloc] initWithFrame:behindViewRect];
[behindAntialiasWindow setContentView:roundedHollowView];
[window addChildWindow:behindAntialiasWindow ordered:NSWindowBelow];
backingView = [[NSView alloc] initWithFrame:viewRect];
contentView = [[NSVisualEffectView alloc] initWithFrame:viewRect];
[contentView setWantsLayer:NO];
[contentView setState:NSVisualEffectStateActive];
[contentView setAppearance:
[NSAppearance appearanceNamed:NSAppearanceNameVibrantLight]];
[contentView setMaskImage:[AppDelegate maskImageWithBounds:contentView.bounds]];
[backingView addSubview:contentView];
[window setContentView:backingView];
[window setLevel:NSFloatingWindowLevel];
[window orderFront:self];
}
+ (NSImage *) maskImageWithBounds: (NSRect) bounds
{
return [NSImage imageWithSize:bounds.size flipped:YES drawingHandler:^BOOL(NSRect dstRect) {
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:bounds xRadius:20.0 yRadius:20.0];
[path setLineJoinStyle:NSRoundLineJoinStyle];
[path fill];
return YES;
}];
}
Drawrect RoundedHollowView выглядит следующим образом:
- (void)drawRect:(NSRect)dirtyRect {
[super drawRect:dirtyRect];
// "erase" the window background
[[NSColor clearColor] set];
NSRectFill(self.frame);
[[NSColor colorWithDeviceWhite:1.0 alpha:0.7] set];
NSBezierPath *path = [NSBezierPath bezierPathWithRoundedRect:self.bounds xRadius:20.0 yRadius:20.0];
path.lineWidth = 2.0;
[path stroke];
}
Опять же, это хак, и вам, возможно, придется поиграть со значениями lineWidth / alpha в зависимости от используемого базового цвета - в моем примере, если вы посмотрите очень внимательно или под более светлым фоном, вы немного разметите границу, но для в моем собственном использовании это менее неприятно, чем отсутствие сглаживания.
Имейте в виду, что режим наложения не будет таким же, как и у всплывающих окон osx yosemite, например, регулятора громкости - в них, как представляется, используется другой недокументированный внешний вид за окном, который демонстрирует больше эффекта цветового прожигания.
Ни одно из этих решений не помогло мне в Мохаве. Однако после часа исследования я нашел это удивительное репо, в котором демонстрируются различные конструкции окон. Одно из решений похоже на желаемый вид OP. Я попробовал, и он работал с красиво сглаженными закругленными углами и без артефакта заголовка. Вот рабочий код:
let visualEffect = NSVisualEffectView()
visualEffect.translatesAutoresizingMaskIntoConstraints = false
visualEffect.material = .dark
visualEffect.state = .active
visualEffect.wantsLayer = true
visualEffect.layer?.cornerRadius = 16.0
window?.titleVisibility = .hidden
window?.styleMask.remove(.titled)
window?.backgroundColor = .clear
window?.isMovableByWindowBackground = true
window?.contentView?.addSubview(visualEffect)
Обратите внимание, что в конце contentView.addSubview(visualEffect)
вместо того contentView = visualEffect
. Это один из ключей к работе.
Все благодарности Марко Массеру за самое аккуратное решение, есть два полезных момента:
- Для работы с гладкими закругленными углами
NSVisualEffectView
должен быть корневым представлением в контроллере представления. При использовании темного материала все еще есть забавные светлые обрезанные края, которые становятся очень заметными на темном фоне. Сделайте свой фон окна прозрачным, чтобы избежать этого,
window.backgroundColor = NSColor.clearColor()
,