简体   繁体   English

NSLayoutManager:如何在只有可渲染字形的地方填充背景颜色

[英]NSLayoutManager: How to fill background colors where there are renderable glyphs only

The default layout manager fills in the background color (specified via NSAttributedString .backgroundColor attribute) where there's no text (except for the last line).默认布局管理器填充没有文本的背景颜色(通过 NSAttributedString .backgroundColor 属性指定)(最后一行除外)。

在此处输入图片说明

I've managed to achieve the effect I want by sublclassing NSLayoutManager and overriding func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) as follows:我设法通过子类化 NSLayoutManager 并覆盖func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint)来实现我想要的效果,如下所示:

override func drawBackground(forGlyphRange glyphsToShow: NSRange, at origin: CGPoint) {
    guard let textContainer = textContainers.first, let textStorage = textStorage else { fatalError() }

    // This just takes the color of the first character assuming the entire container has the same background color.
    // To support ranges of different colours, you'll need to draw each glyph separately, querying the attributed string for the
    // background color attribute for the range of each character.
    guard textStorage.length > 0, let backgroundColor = textStorage.attribute(.backgroundColor, at: 0, effectiveRange: nil) as? UIColor else { return }

    var lineRects = [CGRect]()

    // create an array of line rects to be drawn.
    enumerateLineFragments(forGlyphRange: glyphsToShow) { (_, usedRect, _, range, _) in
        var usedRect = usedRect
        let locationOfLastGlyphInLine = NSMaxRange(range)-1
        // Remove the space at the end of each line (except last).
        if self.isThereAWhitespace(at: locationOfLastGlyphInLine) {
            let lastGlyphInLineWidth = self.boundingRect(forGlyphRange: NSRange(location: locationOfLastGlyphInLine, length: 1), in: textContainer).width
            usedRect.size.width -= lastGlyphInLineWidth
        }
        lineRects.append(usedRect)
    }

    lineRects = adjustRectsToContainerHeight(rects: lineRects, containerHeight: textContainer.size.height)

    for (lineNumber, lineRect) in lineRects.enumerated() {
        guard let context = UIGraphicsGetCurrentContext() else { return }
        context.saveGState()
        context.setFillColor(backgroundColor.cgColor)
        context.fill(lineRect)
        context.restoreGState()
    }
}

private func isThereAWhitespace(at location: Int) -> Bool {
    return propertyForGlyph(at: location) == NSLayoutManager.GlyphProperty.elastic
}

在此处输入图片说明

However, this doesn't handle the possibility of having multiple colors specified by range in the attributed string.但是,这不能处理在属性字符串中具有由范围指定的多种颜色的可能性。 How might I achieve this?我怎么能做到这一点? I've looked at fillBackgroundRectArray with little success.我看过fillBackgroundRectArray ,但收效甚微。

How might I achieve this?我怎么能做到这一点?

Heres' how I reached your goal to highlight the " sit " term in different colors inside the famous Lorem ipsum... that is huge enough to test on multiple lines.下面是我如何达到您的目标,即在著名的Lorem ipsum...以不同颜色突出显示“坐”术语Lorem ipsum...这足以在多行上进行测试。

All the basics that support the following code (Swift 5.1, iOS 13) are provided in this answer and won't be copied here for clarity reason ⟹ they led to the result 1 hereafter.此答案中提供了支持以下代码(Swift 5.1,iOS 13)的所有基础知识,为清楚起见,不会在此处复制⟹它们导致了此后的结果1

在此处输入图片说明

In your case, you want to highlight some specific parts of a string which means that these elements should have dedicated key attributes due to their content ⟹ in my view, it's up to the textStorage to deal with it.在您的情况下,您想突出显示字符串的某些特定部分,这意味着这些元素应该具有专用的关键属性,因为它们的内容⟹在我看来,这取决于textStorage来处理它。

MyTextStorage.swift MyTextStorage.swift

// Sent when a modification appears via the 'replaceCharacters' method.
    override func processEditing() {

        var regEx: NSRegularExpression

        do {
            regEx = try NSRegularExpression.init(pattern: " sit ", options: .caseInsensitive)
            let stringLength = backingStorage.string.distance(from: backingStorage.string.startIndex,
                                                              to: backingStorage.string.endIndex)
            regEx.enumerateMatches(in: backingStorage.string,
                                   options: .reportCompletion,
                                   range: NSRange(location: 1, length: stringLength-1)) { (result, flags, stop) in

                                guard let result = result else { return }
                                self.setAttributes([NSAttributedString.Key.foregroundColor : UIColor.black, //To be seen above every colors.
                                                    NSAttributedString.Key.backgroundColor : UIColor.random()],
                                                   range: result.range)
            }
        } catch let error as NSError {
            print(error.description)
        }

        super.processEditing()
    }
}

//A random color is provided for each " sit " element to highlight the possible different colors in a string.
extension UIColor {
    static func random () -> UIColor {
        return UIColor(red: CGFloat.random(in: 0...1),
                       green: CGFloat.random(in: 0...1),
                       blue: CGFloat.random(in: 0...1),
                       alpha: 1.0)
    }
}

If you build and run from here, you get the result 2 that shows a problem with each colored background of " sit " found in the text ⟹ there's an offset between the lineFragment and the colored background rectangles.如果你从这里构建并运行,你会得到结果2 ,它显示了文本中每个“坐”的彩色背景的问题lineFragment和彩色背景矩形之间有一个偏移量。 🤔 🤔

I went and see the fillBackgroundRectArray method you mentioned and about which Apple states that it 'fills background rectangles with a color' and 'is the primitive method used by drawBackground ' : seems to be perfect here to correct the layout problem.我去看了你提到的fillBackgroundRectArray方法,苹果公司说它“用颜色填充背景矩形”“是drawBackground使用的原始方法” :在这里似乎很适合纠正布局问题。 🤩 🤩

MyLayoutManager.swift MyLayoutManager.swift

override func fillBackgroundRectArray(_ rectArray: UnsafePointer<CGRect>,
                                      count rectCount: Int,
                                      forCharacterRange charRange: NSRange,
                                      color: UIColor) {

    self.enumerateLineFragments(forGlyphRange: charRange) { (rect, usedRect, textContainer, glyphRange, stop) in

        var newRect = rectArray[0]
        newRect.origin.y = usedRect.origin.y + (usedRect.size.height / 4.0)
        newRect.size.height = usedRect.size.height / 2.0

        let currentContext = UIGraphicsGetCurrentContext()
        currentContext?.saveGState()
        currentContext?.setFillColor(color.cgColor)
        currentContext?.fill(newRect)

        currentContext?.restoreGState()
    }
}

Parameters adjustments need to be explored in depth to have a generic formula but for the example it works fine as is.需要深入探索参数调整以获得通用公式,但对于示例,它按原样运行良好。

Finally, we get the result 3 that enables the possibility of having multiple colors specified by range in the attributed string once the conditions of the regular expression are adapted.最后,我们得到结果3 ,一旦调整了正则表达式的条件,就可以在属性字符串中使用范围指定的多种颜色 🎉🥳🎊 🎉🥳🎊

Alternatively, you could bypass using the attributes at all, like this:或者,您可以完全绕过使用属性,如下所示:

So first I defined this struct:所以首先我定义了这个结构:

struct HighlightBackground {
    let range: NSRange
    let color: NSColor
}

Then in my NSTextView subclass:然后在我的 NSTextView 子类中:

var highlightBackgrounds = [HighlightBackground]()

override func setSelectedRanges(_ ranges: [NSValue], affinity: NSSelectionAffinity, stillSelecting stillSelectingFlag: Bool) {
    if stillSelectingFlag == false {
        return
    }

 // remove old ranges first
    highlightBackgrounds = highlightBackgrounds.filter { $0.color != .green }

    for value in ranges {
        let range = value.rangeValue

        highlightBackgrounds.append(HighlightBackground(range: range, color: .green))
    }

    super.setSelectedRanges(ranges, affinity: affinity, stillSelecting: stillSelectingFlag)
}

And then call this from your draw(_ rect: NSRect) method:然后从你的draw(_ rect: NSRect)方法中调用它:

func showBackgrounds() {
    guard
        let context = NSGraphicsContext.current?.cgContext,
        let lm = self.layoutManager
    else { return }

    context.saveGState()
    //        context.translateBy(x: origin.x, y: origin.y)

    for bg in highlightBackgrounds {
        bg.color.setFill()

        let glRange = lm.glyphRange(forCharacterRange: bg.range, actualCharacterRange: nil)    
        for rect in lm.rectsForGlyphRange(glRange) {    
            let path = NSBezierPath(roundedRect: rect, xRadius: selectedTextCornerRadius, yRadius: selectedTextCornerRadius)
            path.fill()
        }
    }

    context.restoreGState()
}

Finally, you'll need this in your NSLayoutManager subclass, although you probably could also put it in the NSTextView subclass:最后,您将需要在您的 NSLayoutManager 子类中使用它,尽管您也可以将它放在 NSTextView 子类中:

func rectsForGlyphRange(_ glyphsToShow: NSRange) -> [NSRect] {

    var rects = [NSRect]()
    guard
        let tc = textContainer(forGlyphAt: glyphsToShow.location, effectiveRange: nil)
    else { return rects }

    enumerateLineFragments(forGlyphRange: glyphsToShow) { _, _, _, effectiveRange, _ in
        let rect = self.boundingRect(forGlyphRange: NSIntersectionRange(glyphsToShow, effectiveRange), in: tc)
        rects.append(rect)
    }

    return rects
}

Hopefully, this works also in your case.希望这也适用于您的情况。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM