【问题标题】:NSAttributedString background color and rounded cornersNSAttributedString 背景颜色和圆角
【发布时间】:2013-04-28 01:31:24
【问题描述】:

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

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

我认为使用的方法是:

  • 使用 Core Text 运行字形。
  • 检查突出显示范围。
  • 如果当前运行在突出显示范围内,请在绘制字形运行之前绘制一个带圆角和所需填充颜色的背景矩形。
  • 绘制字形运行。

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

使用UIWebView 不是一个选项,所以我必须在自定义UIView 中这样做。

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

【问题讨论】:

  • 你好。谢谢你的评论。 :) 我认为 NSLayoutManager 在 iOS6 中不可用。 iOS6 有 CTFrameSetter,它会给我 CTFrame -> CTLine -> CTGlyph。如果我正确获得所需文本的范围,我可以绘制矩形,然后告诉 CTFrame 自己绘制。
  • 对不起,是的。也许试试 textView->selectedRange 和 -[UITextInput selectionRectsForRange:]
  • 可以吗? 1.使标签透明 2.获取子字符串文本范围的框架(stackoverflow.com/questions/19417776/…) 3.在标签后面添加圆角矩形子视图

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


【解决方案1】:

我在@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
   }
}

【讨论】:

    【解决方案2】:

    只需自定义 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
    )
    

    就是这样!

    【讨论】:

    • 请问,如何自定义?你能举个例子吗?
    • @GabrielCavalcante 我将我的演示发布到 Github TextKitDemo
    • 这个对uilabel有影响吗?
    【解决方案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
        }
      }
    }
    
    

    结果文本视图:

    【讨论】:

    • 嗨,有什么 uilabel 解决方案吗?
    【解决方案4】:

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

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

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

    基本上,这意味着您可以添加任何键值对,只要它是您可以添加到NSDictionary 实例的东西。如果系统不能识别该属性,它什么也不做。作为开发人员,您有责任为该属性提供自定义实现和行为。

    为了回答这个问题,假设我添加了一个名为:@"MyRoundedBackgroundColor" 的自定义属性,其值为 [UIColor greenColor]

    对于接下来的步骤,您需要对CoreText 如何完成工作有一个基本的了解。查看Apple's Core Text Programming Guide 了解什么是框架/线条/字形运行/字形等。

    所以,步骤如下:

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

    关于检测属性范围跨越多次运行,当第一次运行遇到该属性时,您可以获得自定义属性的整个有效范围。如果您发现属性的最大有效范围的长度大于运行的长度,则需要在右侧绘制尖角(对于从左到右的脚本)。更多的数学运算也可以让您检测下一行的高光角样式。 :)

    附上效果截图。顶部的框是标准的UITextView,为此我设置了属性文本。底部的框是使用上述步骤实现的框。为两个 textView 设置了相同的属性字符串。

    再一次,如果有比我用过的方法更好的方法,请告诉我! :D

    希望这对社区有所帮助。 :)

    干杯!

    【讨论】:

    • 你好。通常,我会粘贴用于任何给定实现的代码。不幸的是,对于这种特殊情况,我不能做同样的事情。 :( 很抱歉。
    • 我很感激。但是,如果您改变主意并想发布几个 sn-ps,那将很有帮助。我无法按照您的指示进行操作。
    • 查看此danielgorst.wordpress.com/2012/07/30/… 以获得非常相似的代码。
    • @ddiego 我写了一个 HighlightLabel (github.com/dineshrajas/HighlightLabel)。继续,如果你还需要它。
    • 我刚刚意识到培根 ipsum =]]] 非常好,OP。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2020-03-27
    • 1970-01-01
    • 1970-01-01
    • 2011-09-23
    • 2021-11-10
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多