NSAttributedString背景颜色和圆角

时间:2013-05-03 15:23:43

标签: ios objective-c uiview quartz-graphics nsattributedstring

我对自定义UIView的圆角和文字背景颜色有疑问。

基本上,我需要在自定义UIView中实现这样的效果(图像附加 - 注意一侧的圆角): Background highlight

我认为使用的方法是:

  • 使用核心文本获取字形运行。
  • 检查高光范围。
  • 如果当前运行在高亮范围内,请在绘制字形运行之前绘制带圆角和所需填充颜色的背景矩形。
  • 绘制字形运行。

但是,我不确定这是否是唯一的解决方案(或者就此而言,这是否是最有效的解决方案)。

使用UIWebView不是一个选项,因此我必须在自定义UIView中执行此操作。

我的问题是,这是最好的使用方法,我是否走在正确的轨道上?或者我错过了一些重要的事情或以错误的方式解决问题?

4 个答案:

答案 0 :(得分:53)

我设法达到了上述效果,所以我想发布一个相同的答案。

如果有人有任何关于提高效率的建议,请随时提供帮助。我一定会把你的答案标记为正确答案。 :)

为此,您需要向NSAttributedString添加“自定义属性”。

基本上,这意味着您可以添加任何键值对,只要它是您可以添加到NSDictionary实例的内容即可。如果系统无法识别该属性,则不执行任何操作。作为开发人员,您可以为该属性提供自定义实现和行为。

就本回答而言,我们假设我添加了一个名为@"MyRoundedBackgroundColor"的自定义属性,其值为[UIColor greenColor]

对于接下来的步骤,您需要基本了解CoreText如何完成工作。查看Apple's Core Text Programming Guide以了解帧/行/字形运行/字形等等。

所以,以下是步骤:

  1. 创建自定义UIView子类。
  2. 拥有接受NSAttributedString
  3. 的属性
  4. 使用CTFramesetter实例创建NSAttributedString
  5. 覆盖drawRect:方法
  6. CTFrame创建CTFramesetter个实例。
    1. 您需要提供CGPathRef来创建CTFrame。使CGPath与您希望绘制文本的框架相同。
  7. 获取当前图形上下文并翻转文本坐标系。
  8. 使用CTFrameGetLines(...),获取您刚刚创建的CTFrame中的所有行。
  9. 使用CTFrameGetLineOrigins(...),获取CTFrame
  10. 的所有行原点
  11. for loop ...
  12. 数组中的每一行开始CTLine
  13. 使用CTLine将文本位置设置为CGContextSetTextPosition(...)的开头。
  14. 使用CTLineGetGlyphRuns(...)CTRunRef获取所有字形运行(CTLine)。
  15. for loop ...
  16. 数组中的每个glyphRun开始另一个CTRun
  17. 使用CTRunGetStringRange(...)获取运行范围。
  18. 使用CTRunGetTypographicBounds(...)获取印刷范围。
  19. 使用CTLineGetOffsetForStringIndex(...)获取运行的x偏移量。
  20. 使用上述函数返回的值计算边界矩形(让我们称之为runBounds)。
    1. 请记住 - CTRunGetTypographicBounds(...)需要指向变量的指针来存储文本的“上升”和“下降”。您需要添加这些以获得运行高度。
  21. 使用CTRunGetAttributes(...)获取运行的属性。
  22. 检查属性字典是否包含您的属性。
  23. 如果您的属性存在,请计算需要绘制的矩形的边界。
  24. 核心文本的行起源位于基线。我们需要从文本的最低点到最高点。因此,我们需要调整下降。
  25. 因此,从我们在步骤16中计算的边界矩减去下降(runBounds)。
  26. 现在我们有runBounds,我们知道要绘制哪个区域 - 现在我们可以使用任何CoreGraphis / UIBezierPath方法来绘制和填充特定的矩形圆角。
    1. UIBezierPath有一个名为bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:的便捷类方法,让您绕过特定的角落。您可以在第二个参数中使用位掩码指定角点。
  27. 现在您已经填充了矩形,只需使用CTRunDraw(...)绘制字形运行。
  28. 为创建自定义属性庆祝胜利 - 喝啤酒什么的! :d
  29. 关于检测属性范围是否超过多次运行,您可以在第一次运行遇到该属性时获得自定义属性的整个有效范围。如果您发现属性的最大有效范围的长度大于运行的长度,则需要在右侧绘制尖角(对于从左到右的脚本)。更多的数学运算将让您检测下一行的高光转角样式。 :)

    附件是效果的屏幕截图。顶部的框是标准UITextView,我已为其设置了attributionText。底部的框是使用上述步骤实现的框。已为textViews设置了相同的属性字符串。 custom attribute with rounded corners

    同样,如果有比我使用的方法更好的方法,请告诉我! :d

    希望这有助于社区。 :)

    干杯!

答案 1 :(得分:6)

只需自定义NSLayoutManager并覆盖drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:) Apple API Document

通过这种方法,您可以自己绘制下划线,Swift代码

override func drawUnderline(forGlyphRange glyphRange: NSRange,
    underlineType underlineVal: NSUnderlineStyle,
    baselineOffset: CGFloat,
    lineFragmentRect lineRect: CGRect,
    lineFragmentGlyphRange lineGlyphRange: NSRange,
    containerOrigin: CGPoint
) {
    let firstPosition  = location(forGlyphAt: glyphRange.location).x

    let lastPosition: CGFloat

    if NSMaxRange(glyphRange) < NSMaxRange(lineGlyphRange) {
        lastPosition = location(forGlyphAt: NSMaxRange(glyphRange)).x
    } else {
        lastPosition = lineFragmentUsedRect(
            forGlyphAt: NSMaxRange(glyphRange) - 1,
            effectiveRange: nil).size.width
    }

    var lineRect = lineRect
    let height = lineRect.size.height * 3.5 / 4.0 // replace your under line height
    lineRect.origin.x += firstPosition
    lineRect.size.width = lastPosition - firstPosition
    lineRect.size.height = height

    lineRect.origin.x += containerOrigin.x
    lineRect.origin.y += containerOrigin.y

    lineRect = lineRect.integral.insetBy(dx: 0.5, dy: 0.5)

    let path = UIBezierPath(rect: lineRect)
    // let path = UIBezierPath(roundedRect: lineRect, cornerRadius: 3) 
    // set your cornerRadius
    path.fill()
}

然后构造您的NSAttributedString并添加属性.underlineStyle.underlineColor

addAttributes(
    [
        .foregroundColor: UIColor.white,
        .underlineStyle: NSUnderlineStyle.single.rawValue,
        .underlineColor: UIColor(red: 51 / 255.0, green: 154 / 255.0, blue: 1.0, alpha: 1.0)
    ],
    range: range
)

就是这样!

result

答案 2 :(得分:3)

我通过检查文本片段的框架来做到这一点。在我的项目中,我需要在用户键入文本时突出显示主题标签。

class HashtagTextView: UITextView {

  let hashtagRegex = "#[-_0-9A-Za-z]+"

  private var cachedFrames: [CGRect] = []

  private var backgrounds: [UIView] = []

  override init(frame: CGRect, textContainer: NSTextContainer?) {
    super.init(frame: frame, textContainer: textContainer)
    configureView()
  }

  required init?(coder: NSCoder) {
    super.init(coder: coder)
    configureView()
  }

  override func layoutSubviews() {
    super.layoutSubviews()

    // Redraw highlighted parts if frame is changed
    textUpdated()
  }

  deinit {
    NotificationCenter.default.removeObserver(self)
  }

  @objc private func textUpdated() {
    // You can provide whatever ranges needed to be highlighted 
    let ranges = resolveHighlightedRanges()

    let frames = ranges.compactMap { frame(ofRange: $0) }.reduce([], +)

    if cachedFrames != frames {
      cachedFrames = frames

      backgrounds.forEach { $0.removeFromSuperview() }
      backgrounds = cachedFrames.map { frame in
        let background = UIView()
        background.backgroundColor = UIColor.hashtagBackground
        background.frame = frame
        background.layer.cornerRadius = 5
        insertSubview(background, at: 0)
        return background
      }
    }
  }

  /// General setup
  private func configureView() {
    NotificationCenter.default.addObserver(self, selector: #selector(textUpdated), name: UITextView.textDidChangeNotification, object: self)
  }

  /// Looks for locations of the string to be highlighted.
  /// The current case - ranges of hashtags.
  private func resolveHighlightedRanges() -> [NSRange] {
    guard text != nil, let regex = try? NSRegularExpression(pattern: hashtagRegex, options: []) else { return [] }

    let matches = regex.matches(in: text, options: [], range: NSRange(text.startIndex..<text.endIndex, in: text))
    let ranges = matches.map { $0.range }
    return ranges
  }
}

还有一个辅助扩展程序,用于确定范围的帧:


extension UITextView {
  func convertRange(_ range: NSRange) -> UITextRange? {
    let beginning = beginningOfDocument
    if let start = position(from: beginning, offset: range.location), let end = position(from: start, offset: range.length) {
      let resultRange = textRange(from: start, to: end)
      return resultRange
    } else {
      return nil
    }
  }

  func frame(ofRange range: NSRange) -> [CGRect]? {
    if let textRange = convertRange(range) {
      let rects = selectionRects(for: textRange)
      return rects.map { $0.rect }
    } else {
      return nil
    }
  }
}

结果文本视图: text view example

答案 3 :(得分:3)

我在@codeBearer答案之后编写了以下代码。

import UIKit

class CustomAttributedTextView: UITextView {

   override func layoutSubviews() {
       super.layoutSubviews()
   }

   func clearForReuse() {
       setNeedsDisplay()
   }
   var lineCountUpdate: ((Bool) -> Void)?

   override func draw(_ rect: CGRect) {
       super.draw(rect)
       UIColor.clear.setFill()
       UIColor.clear.setFill()
       guard let context = UIGraphicsGetCurrentContext() else { return }
       context.textMatrix = .identity
       context.translateBy(x: 0, y: bounds.size.height)
       context.scaleBy(x: 1.0, y: -1.0)
       let path = CGMutablePath()
       let size = sizeThatFits(CGSize(width: self.frame.width, height: .greatestFiniteMagnitude))
       path.addRect(CGRect(x: 0, y: 0, width: size.width, height: size.height), transform: .identity)

       let framesetter = CTFramesetterCreateWithAttributedString(attributedText as CFAttributedString)
       let frame: CTFrame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, attributedText.length), path, nil)

       let lines: [CTLine] = frame.lines

       var origins = [CGPoint](repeating: .zero, count: lines.count)
       CTFrameGetLineOrigins(frame, CFRangeMake(0, 0), &origins)

       for lineIndex in 0..<lines.count {
           let line = lines[lineIndex]
           let runs: [CTRun] = line.ctruns
           var tagCountInOneLine = 0
           for run in runs {
               var cornerRadius: CGFloat = 3
               let attributes: NSDictionary = CTRunGetAttributes(run)
               var imgBounds: CGRect = .zero
               if let value: UIColor =  attributes.value(forKey: NSAttributedString.Key.customBackgroundColor.rawValue) as? UIColor {
                   var ascent: CGFloat = 0
                   imgBounds.size.width = CGFloat(CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, nil, nil) + 4)
                   imgBounds.size.height = ascent + 6

                   let xOffset = CTLineGetOffsetForStringIndex(line, CTRunGetStringRange(run).location, nil)
                   imgBounds.origin.x = origins[lineIndex].x + xOffset + 3
                   imgBounds.origin.y = origins[lineIndex].y - 13

                   if lineIndex != 0 {
                       imgBounds.origin.y = imgBounds.origin.y - 1
                   }

                   let path = UIBezierPath(roundedRect: imgBounds, cornerRadius: cornerRadius)
                   value.setFill()
                   path.fill()
                   value.setStroke()
               }
           }
       }
   }
}

extension CTFrame {

    var lines: [CTLine] {
        let linesAO: [AnyObject] = CTFrameGetLines(self) as [AnyObject]
        guard let lines = linesAO as? [CTLine] else {
           return []
        }

       return lines
   }
}

extension CTLine {
   var ctruns: [CTRun] {
       let linesAO: [AnyObject] = CTLineGetGlyphRuns(self) as [AnyObject]
       guard let lines = linesAO as? [CTRun] else {
           return []
       }

       return lines
   }
}