【问题标题】: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
    相关资源
    最近更新 更多