【问题标题】:How do you stroke the _outside_ of an NSAttributedString?你如何抚摸 NSAttributedString 的 _outside_ ?
【发布时间】:2010-12-17 04:06:20
【问题描述】:

我一直在 NSAttributedString 对象上使用 NSStrokeWidthAttributeName 在绘制文本时为其添加轮廓。问题是笔画在文本的填充区域内。当文本很小(例如 1 像素厚)时,笔划会使文本难以阅读。我真正想要的是外面的中风。有什么办法吗?

我试过NSShadow 没有偏移和模糊,但它太模糊且难以看到。如果有一种方法可以在不产生任何模糊的情况下增加阴影的大小,那也可以。

【问题讨论】:

    标签: objective-c cocoa nsattributedstring


    【解决方案1】:

    虽然可能有其他方法,但实现此目的的一种方法是首先仅使用笔画绘制字符串,然后仅使用填充绘制字符串,直接覆盖先前绘制的内容。 (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
    

    这会产生以下输出:

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

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

    【讨论】:

    • 很好,详细的答案。感谢您花时间!
    • 简单。杰出的。 ...并且应该完全没有必要...(对此应该已经存在支持)
    【解决方案2】:

    根据@NSGod 的回答将我的解决方案留在这里,结果与UILabel 中的正确定位几乎相同

    在 iOS 14 上使用默认系统字体抚摸字母时出现错误时也很有用(另请参阅 this question

    错误:

    @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)
        }
    
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2011-03-29
      • 2011-09-11
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-22
      相关资源
      最近更新 更多