【问题标题】:NSLayoutManager: How to fill background colors where there are renderable glyphs onlyNSLayoutManager:如何在只有可渲染字形的地方填充背景颜色
【发布时间】:2019-04-15 05:56:34
【问题描述】:

默认布局管理器填充没有文本(最后一行除外)的背景颜色(通过 NSAttributedString .backgroundColor 属性指定)。

我已经通过子类化 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
}

但是,这不能处理属性字符串中由范围指定的多种颜色的可能性。我怎样才能做到这一点?我看过fillBackgroundRectArray,但收效甚微。

【问题讨论】:

  • 看看这个:github.com/kimikaza/TagFieldDemoPlayground/blob/master/… 这样你就可以为每种颜色设置不同的属性键。
  • Koen,谢谢你,但是它不能在多行上工作,但是
  • 是的 - 最后,我不再使用属性,而是自己跟踪选定的范围,并根据需要对它们进行着色。但是我没有像你显示的那些空白(在单词 when 之后),所以它也可能不适用于你的情况。

标签: ios swift core-text textkit nslayoutmanager


【解决方案1】:

或者,您可以完全绕过使用属性,如下所示:

所以我首先定义了这个结构:

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

然后在我的 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)
}

然后从您的 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()
}

最后,您需要在 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
}

希望这也适用于您的情况。

【讨论】:

    【解决方案2】:

    我怎样才能做到这一点?

    这是我如何实现您的目标,在著名的 Lorem ipsum... 中以不同颜色突出显示 “sit” 术语,该术语足够大,可以在多行上进行测试。

    所有支持以下代码的基础(Swift 5.1,iOS 13) 都在this answer 中提供,为了清楚起见,这里不会复制 ⟹ 他们导致了结果 1 以后。

    在您的情况下,您想突出显示字符串的某些特定部分,这意味着这些元素由于其内容而应具有专用的关键属性 ⟹ 在我看来,这取决于 textStorage 来处理它。

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

    如果你从这里构建并运行,你会得到结果 2 显示文本中 "sit" 的每个彩色背景都有问题 ⟹ ​​有一个偏移量在lineFragment 和彩色背景矩形之间。 ?

    我去看看你提到的fillBackgroundRectArray 方法,Apple 声明它'用颜色填充背景矩形' '是drawBackground 使用的原始方法':在这里纠正布局问题似乎是完美的。 ?

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

    需要深入探索参数调整以获得通用公式,但对于示例,它可以正常工作。

    最后,我们得到3的结果,当正则表达式的条件得到调整后,可以在属性字符串中通过范围指定多种颜色。 ???

    【讨论】:

      猜你喜欢
      • 2011-01-19
      • 2013-01-15
      • 2021-12-10
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-09-19
      相关资源
      最近更新 更多