如何抚摸 NSAttributedString 的_outside_?

发布于 2024-10-08 12:45:47 字数 268 浏览 0 评论 0原文

我一直在 NSAttributedString 对象上使用 NSStrokeWidthAttributeName 来在绘制文本时在文本周围放置轮廓。问题在于笔划位于文本的填充区域内。当文本较小(例如 1 像素厚)时,描边会使文本难以阅读。我真正想要的是外面的一击。有什么办法可以做到这一点吗?

我尝试过没有偏移且模糊的 NSShadow ,但它太模糊并且很难看清。如果有一种方法可以增加阴影的大小而不产生任何模糊,那也是可行的。

I've been using NSStrokeWidthAttributeName on NSAttributedString objects to put an outline around text as it's drawn. The problem is that the stroke is inside the fill area of the text. When the text is small (e.g. 1 pixel thick), the stroking makes the text hard to read. What I really want is a stroke on the outside. Is there any way to do that?

I've tried an NSShadow with no offset and a blur, but it's too blurry and hard to see. If there was a way to increase the size of the shadow without any blur, that would work too.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

南巷近海 2024-10-15 12:45:47

虽然可能还有其他方法,但实现此目的的一种方法是首先仅使用笔画绘制字符串,然后仅使用填充绘制字符串,直接在先前绘制的内容之上。 (Adobe InDesign 实际上有这个内置功能,它看起来只将笔划应用于字母的外部,这有助于提高可读性)。

这只是一个示例视图,展示了如何完成此操作(灵感来自 http ://developer.apple.com/library/mac/#qa/qa2008/qa1531.html):

首先设置属性:

@implementation MDInDesignTextView

static NSMutableDictionary *regularAttributes = nil;
static NSMutableDictionary *indesignBackgroundAttributes = nil;
static NSMutableDictionary *indesignForegroundAttributes = nil;

- (void)drawRect:(NSRect)frame {
    NSString *string = @"Got stroke?";
    if (regularAttributes == nil) {
        regularAttributes = [[NSMutableDictionary
    dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSColor whiteColor],NSForegroundColorAttributeName,
        [NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
        [NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
    }

    if (indesignBackgroundAttributes == nil) {
        indesignBackgroundAttributes = [[NSMutableDictionary
        dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
        [NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
    }

    if (indesignForegroundAttributes == nil) {
        indesignForegroundAttributes = [[NSMutableDictionary
        dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSColor whiteColor],NSForegroundColorAttributeName, nil] retain];
    }

    [[NSColor grayColor] set];
    [NSBezierPath fillRect:frame];

    // draw top string
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 200.0)
        withAttributes:regularAttributes];

    // draw bottom string in two passes
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
        withAttributes:indesignBackgroundAttributes];
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
        withAttributes:indesignForegroundAttributes];
}

@end

这会产生以下输出:

alt text

alt text

现在,它并不完美,因为字形有时会落在分数边界上,但是,它看起来肯定比默认值更好。

如果性能是一个问题,您始终可以考虑降低到稍低的级别,例如 CoreGraphics 或 CoreText。

While there may be other ways, one way to accomplish this is to first draw the string with only a stroke, then draw the string with only a fill, directly overtop of what was previously drawn. (Adobe InDesign actually has this built-in, where it will appear to only apply the stroke to the outside of letter, which helps with readability).

This is just an example view that shows how to accomplish this (inspired by http://developer.apple.com/library/mac/#qa/qa2008/qa1531.html):

First set up the attributes:

@implementation MDInDesignTextView

static NSMutableDictionary *regularAttributes = nil;
static NSMutableDictionary *indesignBackgroundAttributes = nil;
static NSMutableDictionary *indesignForegroundAttributes = nil;

- (void)drawRect:(NSRect)frame {
    NSString *string = @"Got stroke?";
    if (regularAttributes == nil) {
        regularAttributes = [[NSMutableDictionary
    dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSColor whiteColor],NSForegroundColorAttributeName,
        [NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
        [NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
    }

    if (indesignBackgroundAttributes == nil) {
        indesignBackgroundAttributes = [[NSMutableDictionary
        dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSNumber numberWithFloat:-5.0],NSStrokeWidthAttributeName,
        [NSColor blackColor],NSStrokeColorAttributeName, nil] retain];
    }

    if (indesignForegroundAttributes == nil) {
        indesignForegroundAttributes = [[NSMutableDictionary
        dictionaryWithObjectsAndKeys:
        [NSFont systemFontOfSize:64.0],NSFontAttributeName,
        [NSColor whiteColor],NSForegroundColorAttributeName, nil] retain];
    }

    [[NSColor grayColor] set];
    [NSBezierPath fillRect:frame];

    // draw top string
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 200.0)
        withAttributes:regularAttributes];

    // draw bottom string in two passes
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
        withAttributes:indesignBackgroundAttributes];
    [string drawAtPoint:
        NSMakePoint(frame.origin.x + 200.0, frame.origin.y + 140.0)
        withAttributes:indesignForegroundAttributes];
}

@end

This produces the following output:

alt text

alt text

Now, it's not perfect, since the glyphs will sometimes fall on fractional boundaries, but, it certainly looks better than the default.

If performance is an issue, you could always look into dropping down to a slightly lower level, such as CoreGraphics or CoreText.

韶华倾负 2024-10-15 12:45:47

只需将我的解决方案基于 @NSGod 的答案留在这里,结果就非常相同,只需在 UILabel 内进行适当的定位。

当使用默认系统字体抚摸字母时,在 iOS 14 上出现错误时也很有用(另请参阅这个问题

错误:

输入图片此处描述

@interface StrokedTextLabel : UILabel
@end

/**
 * https://stackoverflow.com/a/4468880/3004003
 */
@implementation StrokedTextLabel

- (void)drawTextInRect:(CGRect)rect
{
    if (!self.attributedText) {
        [super drawTextInRect:rect];
        return;
    }

    NSMutableAttributedString *attributedText = self.attributedText.mutableCopy;
    [attributedText enumerateAttributesInRange:NSMakeRange(0, attributedText.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop) {
        if (attrs[NSStrokeWidthAttributeName]) {
            // 1. draw underlying stroked string
            // use doubled stroke width to simulate outer border, because border is being stroked
            // in both outer & inner directions with half width
            CGFloat strokeWidth = [attrs[NSStrokeWidthAttributeName] floatValue] * 2;
            [attributedText addAttributes:@{NSStrokeWidthAttributeName : @(strokeWidth)} range:range];
            self.attributedText = attributedText;
            // perform default drawing
            [super drawTextInRect:rect];

            // 2. draw unstroked string above
            NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
            style.alignment = self.textAlignment;

            [attributedText addAttributes:@{
                NSStrokeWidthAttributeName : @(0),
                NSForegroundColorAttributeName : self.textColor,
                NSFontAttributeName : self.font,
                NSParagraphStyleAttributeName : style
            } range:range];

            // we use here custom bounding rect detection method instead of
            // [attributedText boundingRectWithSize:...] because the latter gives incorrect result
            // in this case
            CGRect textRect = [self boundingRectWithAttributedString:attributedText forCharacterRange:NSMakeRange(0, attributedText.length)];
            [attributedText boundingRectWithSize:rect.size options:NSStringDrawingUsesLineFragmentOrigin
                                         context:nil];
            // adjust vertical position because returned bounding rect has zero origin
            textRect.origin.y = (rect.size.height - textRect.size.height) / 2;
            [attributedText drawInRect:textRect];
        }
    }];
}

/**
 * https://stackoverflow.com/a/20633388/3004003
 */
- (CGRect)boundingRectWithAttributedString:(NSAttributedString *)attributedString
                         forCharacterRange:(NSRange)range
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
    textContainer.lineFragmentPadding = 0;
    [layoutManager addTextContainer:textContainer];

    NSRange glyphRange;

    // Convert the range for glyphs.
    [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];

    return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}

@end

Swift 版本

import Foundation
import UIKit

/// https://stackoverflow.com/a/4468880/3004003
@objc(MUIStrokedTextLabel)
public class StrokedTextLabel : UILabel {

    override public func drawText(in rect: CGRect) {

        guard let attributedText = attributedText?.mutableCopy() as? NSMutableAttributedString else {
            super.drawText(in: rect)
            return
        }

        attributedText.enumerateAttributes(in: NSRange(location: 0, length: attributedText.length), options: [], using: { attrs, range, stop in
            guard let strokeWidth = attrs[NSAttributedString.Key.strokeWidth] as? CGFloat else {
                return
            }

            // 1. draw underlying stroked string
            // use doubled stroke width to simulate outer border, because border is being stroked
            // in both outer & inner directions with half width
            attributedText.addAttributes([
                NSAttributedString.Key.strokeWidth: strokeWidth * 2
            ], range: range)
            self.attributedText = attributedText
            // perform default drawing
            super.drawText(in: rect)

            // 2. draw unstroked string above
            let style = NSMutableParagraphStyle()
            style.alignment = textAlignment

            let attributes = [
                NSAttributedString.Key.strokeWidth: NSNumber(value: 0),
                NSAttributedString.Key.foregroundColor: textColor ?? UIColor.black,
                NSAttributedString.Key.font: font ?? UIFont.systemFont(ofSize: 17),
                NSAttributedString.Key.paragraphStyle: style
            ]

            attributedText.addAttributes(attributes, range: range)

            // we use here custom bounding rect detection method instead of
            // [attributedText boundingRectWithSize:...] because the latter gives incorrect result
            // in this case
            var textRect = boundingRect(with: attributedText, forCharacterRange: NSRange(location: 0, length: attributedText.length))
            attributedText.boundingRect(
                    with: rect.size,
                    options: .usesLineFragmentOrigin,
                    context: nil)
            // adjust vertical position because returned bounding rect has zero origin
            textRect.origin.y = (rect.size.height - textRect.size.height) / 2
            attributedText.draw(in: textRect)
        })
    }

    /// https://stackoverflow.com/a/20633388/3004003
    private func boundingRect(
            with attributedString: NSAttributedString?,
            forCharacterRange range: NSRange
    ) -> CGRect {
        guard let attributedString = attributedString else {
            return .zero
        }
        let textStorage = NSTextStorage(attributedString: attributedString)
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }

}

Just leave here my solution based on answer of @NSGod, result is pretty the same just having proper positioning inside UILabel

It is also useful when having bugs on iOS 14 when stroking letters with default system font (refer also this question)

Bug:

enter image description here

@interface StrokedTextLabel : UILabel
@end

/**
 * https://stackoverflow.com/a/4468880/3004003
 */
@implementation StrokedTextLabel

- (void)drawTextInRect:(CGRect)rect
{
    if (!self.attributedText) {
        [super drawTextInRect:rect];
        return;
    }

    NSMutableAttributedString *attributedText = self.attributedText.mutableCopy;
    [attributedText enumerateAttributesInRange:NSMakeRange(0, attributedText.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey, id> *attrs, NSRange range, BOOL *stop) {
        if (attrs[NSStrokeWidthAttributeName]) {
            // 1. draw underlying stroked string
            // use doubled stroke width to simulate outer border, because border is being stroked
            // in both outer & inner directions with half width
            CGFloat strokeWidth = [attrs[NSStrokeWidthAttributeName] floatValue] * 2;
            [attributedText addAttributes:@{NSStrokeWidthAttributeName : @(strokeWidth)} range:range];
            self.attributedText = attributedText;
            // perform default drawing
            [super drawTextInRect:rect];

            // 2. draw unstroked string above
            NSMutableParagraphStyle *style = [NSMutableParagraphStyle new];
            style.alignment = self.textAlignment;

            [attributedText addAttributes:@{
                NSStrokeWidthAttributeName : @(0),
                NSForegroundColorAttributeName : self.textColor,
                NSFontAttributeName : self.font,
                NSParagraphStyleAttributeName : style
            } range:range];

            // we use here custom bounding rect detection method instead of
            // [attributedText boundingRectWithSize:...] because the latter gives incorrect result
            // in this case
            CGRect textRect = [self boundingRectWithAttributedString:attributedText forCharacterRange:NSMakeRange(0, attributedText.length)];
            [attributedText boundingRectWithSize:rect.size options:NSStringDrawingUsesLineFragmentOrigin
                                         context:nil];
            // adjust vertical position because returned bounding rect has zero origin
            textRect.origin.y = (rect.size.height - textRect.size.height) / 2;
            [attributedText drawInRect:textRect];
        }
    }];
}

/**
 * https://stackoverflow.com/a/20633388/3004003
 */
- (CGRect)boundingRectWithAttributedString:(NSAttributedString *)attributedString
                         forCharacterRange:(NSRange)range
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:attributedString];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [textStorage addLayoutManager:layoutManager];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size];
    textContainer.lineFragmentPadding = 0;
    [layoutManager addTextContainer:textContainer];

    NSRange glyphRange;

    // Convert the range for glyphs.
    [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange];

    return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer];
}

@end

Swift version

import Foundation
import UIKit

/// https://stackoverflow.com/a/4468880/3004003
@objc(MUIStrokedTextLabel)
public class StrokedTextLabel : UILabel {

    override public func drawText(in rect: CGRect) {

        guard let attributedText = attributedText?.mutableCopy() as? NSMutableAttributedString else {
            super.drawText(in: rect)
            return
        }

        attributedText.enumerateAttributes(in: NSRange(location: 0, length: attributedText.length), options: [], using: { attrs, range, stop in
            guard let strokeWidth = attrs[NSAttributedString.Key.strokeWidth] as? CGFloat else {
                return
            }

            // 1. draw underlying stroked string
            // use doubled stroke width to simulate outer border, because border is being stroked
            // in both outer & inner directions with half width
            attributedText.addAttributes([
                NSAttributedString.Key.strokeWidth: strokeWidth * 2
            ], range: range)
            self.attributedText = attributedText
            // perform default drawing
            super.drawText(in: rect)

            // 2. draw unstroked string above
            let style = NSMutableParagraphStyle()
            style.alignment = textAlignment

            let attributes = [
                NSAttributedString.Key.strokeWidth: NSNumber(value: 0),
                NSAttributedString.Key.foregroundColor: textColor ?? UIColor.black,
                NSAttributedString.Key.font: font ?? UIFont.systemFont(ofSize: 17),
                NSAttributedString.Key.paragraphStyle: style
            ]

            attributedText.addAttributes(attributes, range: range)

            // we use here custom bounding rect detection method instead of
            // [attributedText boundingRectWithSize:...] because the latter gives incorrect result
            // in this case
            var textRect = boundingRect(with: attributedText, forCharacterRange: NSRange(location: 0, length: attributedText.length))
            attributedText.boundingRect(
                    with: rect.size,
                    options: .usesLineFragmentOrigin,
                    context: nil)
            // adjust vertical position because returned bounding rect has zero origin
            textRect.origin.y = (rect.size.height - textRect.size.height) / 2
            attributedText.draw(in: textRect)
        })
    }

    /// https://stackoverflow.com/a/20633388/3004003
    private func boundingRect(
            with attributedString: NSAttributedString?,
            forCharacterRange range: NSRange
    ) -> CGRect {
        guard let attributedString = attributedString else {
            return .zero
        }
        let textStorage = NSTextStorage(attributedString: attributedString)
        let layoutManager = NSLayoutManager()
        textStorage.addLayoutManager(layoutManager)
        let textContainer = NSTextContainer(size: bounds.size)
        textContainer.lineFragmentPadding = 0
        layoutManager.addTextContainer(textContainer)

        var glyphRange = NSRange()

        // Convert the range for glyphs.
        layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange)

        return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    }

}
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文