Как сделать гладкое, округленное, похожее на объем окно 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;

Сглаживание диагональных ребер CALayer

https://developer.apple.com/library/mac/documentation/GraphicsImaging/Reference/CALayer_class/index.html

Несмотря на ограничения 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. Это один из ключей к работе.

Все благодарности Марко Массеру за самое аккуратное решение, есть два полезных момента:

  1. Для работы с гладкими закругленными углами NSVisualEffectView должен быть корневым представлением в контроллере представления.
  2. При использовании темного материала все еще есть забавные светлые обрезанные края, которые становятся очень заметными на темном фоне. Сделайте свой фон окна прозрачным, чтобы избежать этого, window.backgroundColor = NSColor.clearColor(),

    введите описание изображения здесь

Другие вопросы по тегам