ios NSAttributedString 背景色和圆角

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/16362407/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-08-30 23:27:09  来源:igfitidea点击:

NSAttributedString background color and rounded corners

iosobjective-cuiviewquartz-graphicsnsattributedstring

提问by codeBearer

I have a question regarding rounded corners and text background color for a custom UIView.

我有一个关于自定义UIView.

Basically, I need to achieve an effect like this (image attached - notice the rounded corners on one side) in a custom UIView: Background highlight

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

I'm thinking the approach to use is:

我认为使用的方法是:

  • Use Core Text to get glyph runs.
  • Check highlight range.
  • If the current run is within the highlight range, draw a background rectangle with rounded corners and desired fill color before drawing the glyph run.
  • Draw the glyph run.
  • 使用 Core Text 获取字形运行。
  • 检查高光范围。
  • 如果当前运行在高亮范围内,请在绘制字形运行之前绘制带圆角和所需填充颜色的背景矩形。
  • 绘制字形运行。

However, I'm not sure whether this is the only solution (or for that matter, whether this is the most efficient solution).

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

Using a UIWebViewis not an option, so I have to do it in a custom UIView.

使用 aUIWebView不是一种选择,所以我必须在自定义UIView.

My question being, is this the best approach to use, and am I on the right track? Or am I missing out something important or going about it the wrong way?

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

回答by codeBearer

I managed to achieve the above effect, so thought I'd post an answer for the same.

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

If anyone has any suggestions about making this more effective, please feel free to contribute. I'll be sure to mark your answer as the correct one. :)

如果有人对使其更有效有任何建议,请随时贡献。我一定会把你的答案标记为正确的。:)

For doing this, you'll need to add a "custom attribute" to NSAttributedString.

为此,您需要将“自定义属性”添加到NSAttributedString.

Basically, what that means is that you can add any key-value pair, as long as it is something that you can add to an NSDictionaryinstance. If the system does not recognize that attribute, it does nothing. It is up to you, as the developer, to provide a custom implementation and behavior for that attribute.

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

For the purposes of this answer, let us assume I've added a custom attribute called: @"MyRoundedBackgroundColor"with a value of [UIColor greenColor].

出于此答案的目的,让我们假设我添加了一个名为:的自定义属性@"MyRoundedBackgroundColor",其值为[UIColor greenColor].

For the steps that follow, you'll need to have a basic understanding of how CoreTextgets stuff done. Check out Apple's Core Text Programming Guidefor understanding what's a frame/line/glyph run/glyph, etc.

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

So, here are the steps:

所以,这里是步骤:

  1. Create a custom UIView subclass.
  2. Have a property for accepting an NSAttributedString.
  3. Create a CTFramesetterusing that NSAttributedStringinstance.
  4. Override the drawRect:method
  5. Create a CTFrameinstance from the CTFramesetter.
    1. You will need to give a CGPathRefto create the CTFrame. Make that CGPathto be the same as the frame in which you wish to draw the text.
  6. Get the current graphics context and flip the text coordinate system.
  7. Using CTFrameGetLines(...), get all the lines in the CTFrameyou just created.
  8. Using CTFrameGetLineOrigins(...), get all the line origins for the CTFrame.
  9. Start a for loop- for each line in the array of CTLine...
  10. Set the text position to the start of the CTLineusing CGContextSetTextPosition(...).
  11. Using CTLineGetGlyphRuns(...)get all the Glyph Runs (CTRunRef) from the CTLine.
  12. Start another for loop- for each glyphRun in the array of CTRun...
  13. Get the range of the run using CTRunGetStringRange(...).
  14. Get typographic bounds using CTRunGetTypographicBounds(...).
  15. Get the x offset for the run using CTLineGetOffsetForStringIndex(...).
  16. Calculate the bounding rect (let's call it runBounds) using the values returned from the aforementioned functions.
    1. Remember - CTRunGetTypographicBounds(...)requires pointers to variables to store the "ascent" and "descent" of the text. You need to add those to get the run height.
  17. Get the attributes for the run using CTRunGetAttributes(...).
  18. Check if the attribute dictionary contains your attribute.
  19. If your attribute exists, calculate the bounds of the rectangle that needs to be painted.
  20. Core text has the line origins at the baseline. We need to draw from the lowermost point of the text to the topmost point. Thus, we need to adjust for descent.
  21. So, subtract the descent from the bounding rect that we calculated in step 16 (runBounds).
  22. Now that we have the runBounds, we know what area we want to paint - now we can use any of the CoreGraphis/UIBezierPathmethods to draw and fill a rect with specific rounded corners.
    1. UIBezierPathhas a convenience class method called bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:that let's you round specific corners. You specify the corners using bit masks in the 2nd parameter.
  23. Now that you've filled the rect, simply draw the glyph run using CTRunDraw(...).
  24. Celebrate victory for having created your custom attribute - drink a beer or something! :D
  1. 创建自定义 UIView 子类。
  2. 具有接受NSAttributedString.
  3. 创建一个CTFramesetter使用该NSAttributedString实例。
  4. 覆盖drawRect:方法
  5. 创建一个CTFrame从实例CTFramesetter
    1. 您将需要提供 aCGPathRef来创建CTFrame. 使其CGPath与您希望绘制文本的框架相同。
  6. 获取当前图形上下文并翻转文本坐标系。
  7. 使用CTFrameGetLines(...),获取CTFrame您刚刚创建的所有行。
  8. 使用CTFrameGetLineOrigins(...),获取CTFrame.
  9. for loopCTLine...数组中的每一行开始一个-
  10. 将文本位置设置为CTLineusing的开头CGContextSetTextPosition(...)
  11. 使用CTLineGetGlyphRuns(...)得到所有的雕文奔跑(CTRunRef)从CTLine
  12. 开始另一个for loop- 对于CTRun...数组中的每个 glyphRun
  13. 使用 获取运行范围CTRunGetStringRange(...)
  14. 使用CTRunGetTypographicBounds(...).
  15. 使用 获取运行的 x 偏移量CTLineGetOffsetForStringIndex(...)
  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

Regarding detecting that the attribute range extends over multiple runs, you can get the entire effective range of your custom attribute when the 1st run encounters the attribute. If you find that the length of the maximum effective range of your attribute is greater than the length of your run, you need to paint sharp corners on the right side (for a left to right script). More math will let you detect the highlight corner style for the next line as well. :)

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

Attached is a screenshot of the effect. The box on the top is a standard UITextView, for which I've set the attributedText. The box on the bottom is the one that has been implemented using the above steps. The same attributed string has been set for both the textViews. custom attribute with rounded corners

附上效果截图。顶部的框是一个标准的UITextView,我已经为其设置了属性文本。底部的框是使用上述步骤实现的框。为两个 textViews 设置了相同的属性字符串。 带圆角的自定义属性

Again, if there is a better approach than the one that I've used, please do let me know! :D

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

Hope this helps the community. :)

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

Cheers!

干杯!

回答by Nikita Koltsov

I did it by checking frames of text fragments. In my project I needed to highlight hashtags while a user is typing text.

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

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: 

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 { 
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()
}
.rect } } else { return nil } } }
) }.reduce([], +) if cachedFrames != frames { cachedFrames = frames backgrounds.forEach {
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
)
.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 { ##代码##.range } return ranges } }

There is also a helper extension to determine frames of ranges:

还有一个辅助扩展来确定范围的帧:

##代码##

Result text view: text view example

结果文本视图: 文本视图示例

回答by shiwei93

Just customize NSLayoutManagerand override drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:)Apple API Document

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

In this method, you can draw underline by yourself, Swift code,

这个方法可以自己画下划线,Swift代码,

##代码##

Then construct your NSAttributedStringand add attributes .underlineStyleand .underlineColor.

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

##代码##

That's it!

就是这样!

result

结果