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
NSAttributedString background color and rounded corners
提问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:
基本上,我需要在自定义 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 UIWebView
is 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 NSDictionary
instance. 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 CoreText
gets 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:
所以,这里是步骤:
- Create a custom UIView subclass.
- Have a property for accepting an
NSAttributedString
. - Create a
CTFramesetter
using thatNSAttributedString
instance. - Override the
drawRect:
method - Create a
CTFrame
instance from theCTFramesetter
.- You will need to give a
CGPathRef
to create theCTFrame
. Make thatCGPath
to be the same as the frame in which you wish to draw the text.
- You will need to give a
- Get the current graphics context and flip the text coordinate system.
- Using
CTFrameGetLines(...)
, get all the lines in theCTFrame
you just created. - Using
CTFrameGetLineOrigins(...)
, get all the line origins for theCTFrame
. - Start a
for loop
- for each line in the array ofCTLine
... - Set the text position to the start of the
CTLine
usingCGContextSetTextPosition(...)
. - Using
CTLineGetGlyphRuns(...)
get all the Glyph Runs (CTRunRef
) from theCTLine
. - Start another
for loop
- for each glyphRun in the array ofCTRun
... - Get the range of the run using
CTRunGetStringRange(...)
. - Get typographic bounds using
CTRunGetTypographicBounds(...)
. - Get the x offset for the run using
CTLineGetOffsetForStringIndex(...)
. - Calculate the bounding rect (let's call it
runBounds
) using the values returned from the aforementioned functions.- 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.
- Remember -
- Get the attributes for the run using
CTRunGetAttributes(...)
. - Check if the attribute dictionary contains your attribute.
- If your attribute exists, calculate the bounds of the rectangle that needs to be painted.
- 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.
- So, subtract the descent from the bounding rect that we calculated in step 16 (
runBounds
). - Now that we have the
runBounds
, we know what area we want to paint - now we can use any of theCoreGraphis
/UIBezierPath
methods to draw and fill a rect with specific rounded corners.UIBezierPath
has a convenience class method calledbezierPathWithRoundedRect:byRoundingCorners:cornerRadii:
that let's you round specific corners. You specify the corners using bit masks in the 2nd parameter.
- Now that you've filled the rect, simply draw the glyph run using
CTRunDraw(...)
. - Celebrate victory for having created your custom attribute - drink a beer or something! :D
- 创建自定义 UIView 子类。
- 具有接受
NSAttributedString
. - 创建一个
CTFramesetter
使用该NSAttributedString
实例。 - 覆盖
drawRect:
方法 - 创建一个
CTFrame
从实例CTFramesetter
。- 您将需要提供 a
CGPathRef
来创建CTFrame
. 使其CGPath
与您希望绘制文本的框架相同。
- 您将需要提供 a
- 获取当前图形上下文并翻转文本坐标系。
- 使用
CTFrameGetLines(...)
,获取CTFrame
您刚刚创建的所有行。 - 使用
CTFrameGetLineOrigins(...)
,获取CTFrame
. for loop
为CTLine
...数组中的每一行开始一个-- 将文本位置设置为
CTLine
using的开头CGContextSetTextPosition(...)
。 - 使用
CTLineGetGlyphRuns(...)
得到所有的雕文奔跑(CTRunRef
)从CTLine
。 - 开始另一个
for loop
- 对于CTRun
...数组中的每个 glyphRun - 使用 获取运行范围
CTRunGetStringRange(...)
。 - 使用
CTRunGetTypographicBounds(...)
. - 使用 获取运行的 x 偏移量
CTLineGetOffsetForStringIndex(...)
。 runBounds
使用从上述函数返回的值计算边界矩形(我们称之为)。- 请记住 -
CTRunGetTypographicBounds(...)
需要指向变量的指针来存储文本的“上升”和“下降”。您需要添加这些以获得运行高度。
- 请记住 -
- 使用 获取运行的属性
CTRunGetAttributes(...)
。 - 检查属性字典是否包含您的属性。
- 如果您的属性存在,则计算需要绘制的矩形的边界。
- 核心文本的行起点位于基线处。我们需要从文本的最低点绘制到最高点。因此,我们需要调整下降。
- 因此,从我们在步骤 16 (
runBounds
) 中计算的边界矩形中减去下降。 - 现在我们有了
runBounds
,我们知道要绘制哪个区域 - 现在我们可以使用任何CoreGraphis
/UIBezierPath
方法来绘制和填充具有特定圆角的矩形。UIBezierPath
有一个方便的类方法bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:
,可以让你绕过特定的角落。您可以在第二个参数中使用位掩码指定角。
- 现在您已经填充了矩形,只需使用
CTRunDraw(...)
. - 庆祝您创建自定义属性的胜利 - 喝啤酒什么的!: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.
附上效果截图。顶部的框是一个标准的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:
还有一个辅助扩展来确定范围的帧:
##代码##回答by shiwei93
Just customize NSLayoutManager
and 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 NSAttributedString
and add attributes .underlineStyle
and .underlineColor
.
然后构造您的NSAttributedString
并添加属性.underlineStyle
和.underlineColor
。
That's it!
就是这样!