Подтвердить что ты не робот

Как сделать гладкое, округленное, объемное окно OS X с помощью NSVisualEffectView?

В настоящее время я пытаюсь создать окно, похожее на окно Volume OS X:
enter image description here


Чтобы сделать это, у меня есть собственный NSWindow (с использованием пользовательского подкласса), который является прозрачным /titlebar -less/shadow-less, который имеет 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 - более гладко):
enter image description here
Любые идеи о том, как сделать углы более гладкими? Благодаря

4b9b3361

Ответ 1

Обновление для 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 Xs.


В моем тестировании я обнаружил, что вам нужно использовать изображение маски для 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 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, я использовал Object Inspector (правая сторона, третья вкладка), чтобы получить адрес для объекта window. Используя этот указатель, я проверил, что это действительно возвращает _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.

Ответ 2

Я помню, как это делалось задолго до того, как 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];

result (игнорировать слайдер):

enter image description here

Изменить. Если этот метод по какой-либо причине нежелателен, не можете ли вы просто назначить CAShapeLayer в качестве contentView layer, либо преобразовать вышеуказанный NSBezierPath в CGPath или просто построить как CGPath и назначить путь к слоям path?

Ответ 3

"Сглаженный эффект", который вы имеете в виду, называется "Сглаживание". Я немного поработал в поисковых системах, и я думаю, что вы, возможно, первый человек, который попытался обойти углы 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#//apple_ref/occ/instp/CALayer/edgeAntialiasingMask

Ответ 4

Несмотря на то, что ограничения NSVisualEffectView не сглаживаются, теперь обходное решение kludgey, которое должно работать для этого приложения плавающего без заголовка окна без тени, имеет дочернее окно под ним, которое вытягивает только края.

Мне удалось заставить мой выглядеть так:

enter image description here

выполнив следующее:

В контроллере, содержащем все:

- (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;
}];
}

RoundedHollowView drawrect выглядит так:

- (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 в зависимости от базового цвета, который вы используете. В моем примере, если вы посмотрите очень близко или на более светлом фоне, вы немного разглядите границу, но для моего собственного использования он чувствует себя менее резким, чем отсутствие сглаживания.

Имейте в виду, что режим смешивания не будет таким же, как и всплывающие всплывающие окна oosx yosemite, такие как регулятор громкости - кажется, что они используют другой недокументированный внешний вид позади, который показывает больше эффекта цветного ожога.

Ответ 5

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

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

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