cod*_*rer 50 objective-c quartz-graphics uiview nsattributedstring ios
我有一个关于自定义圆角和文本背景颜色的问题UIView.
基本上,我需要在自定义UIView中实现这样的效果(图像附加 - 注意一侧的圆角):

我在考虑使用的方法是:
但是,我不确定这是否是唯一的解决方案(或者就此而言,这是否是最有效的解决方案).
使用a UIWebView不是一个选项,所以我必须在自定义中执行UIView.
我的问题是,这是最好的使用方法,我是否走在正确的轨道上?或者我错过了重要的事情或错误的方式?
cod*_*rer 56
我设法达到了上述效果,所以我想发布一个相同的答案.
如果有人有任何关于使这个更有效的建议,请随时贡献.我一定会把你的答案标记为正确答案.:)
为此,您需要添加"自定义属性" NSAttributedString.
基本上,这意味着您可以添加任何键值对,只要它是您可以添加到NSDictionary实例的东西.如果系统无法识别该属性,则不执行任何操作.作为开发人员,您可以为该属性提供自定义实现和行为.
出于本答案的目的,我们假设我添加了一个名为的自定义属性:@"MyRoundedBackgroundColor"值为[UIColor greenColor].
对于接下来的步骤,您需要基本了解如何CoreText完成任务.查看Apple的核心文本编程指南,了解帧/行/字形运行/字形等.
那么,以下是步骤:
NSAttributedString.CTFramesetter使用该NSAttributedString实例创建一个.drawRect:方法CTFrame从中创建一个实例CTFramesetter.
CGPathRef创建CTFrame.使其CGPath与您希望绘制文本的框架相同.CTFrameGetLines(...),获取CTFrame刚刚创建的所有行.CTFrameGetLineOrigins(...),得到所有的起源CTFrame.for loop- 为数组中的每一行CTLine...CTLine使用的开始CGContextSetTextPosition(...).CTLineGetGlyphRuns(...)get中的所有Glyph Runs(CTRunRef)CTLine.for loop- 对于数组中的每个glyphRun CTRun...CTRunGetStringRange(...).CTRunGetTypographicBounds(...).CTLineGetOffsetForStringIndex(...).runBounds使用从上述函数返回的值计算边界矩形(让我们调用它).
CTRunGetTypographicBounds(...)需要指向变量的指针来存储文本的"上升"和"下降".您需要添加它们才能获得运行高度.CTRunGetAttributes(...).runBounds)中计算的边界矩减去下降.runBounds知道了,我们知道要绘制哪个区域 - 现在我们可以使用任何CoreGraphis/ UIBezierPath方法来绘制和填充具有特定圆角的矩形.
UIBezierPath有一个方便类方法bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:,让你绕过特定的角落.您可以在第二个参数中使用位掩码指定角点.CTRunDraw(...).关于检测属性范围是否超过多次运行,您可以在第一次运行遇到该属性时获得自定义属性的整个有效范围.如果您发现属性的最大有效范围的长度大于运行的长度,则需要在右侧绘制尖角(对于从左到右的脚本).更多的数学运算将让您检测下一行的高光转角样式.:)
附件是效果的屏幕截图.顶部的框是标准UITextView,我为其设置了belongsText.底部的框是使用上述步骤实现的框.已为textViews设置了相同的属性字符串.

再说一次,如果有比我使用过的方法更好的方法,请告诉我!:d
希望这有助于社区.:)
干杯!
只需自定义NSLayoutManager和覆盖drawUnderline(forGlyphRange:underlineType:baselineOffset:lineFragmentRect:lineFragmentGlyphRange:containerOrigin:) Apple API 文档
这个方法可以自己画下划线,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()
}
Run Code Online (Sandbox Code Playgroud)
然后构造您的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
)
Run Code Online (Sandbox Code Playgroud)
就是这样!
我按照 @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
}
}
Run Code Online (Sandbox Code Playgroud)
我通过检查文本片段的帧来做到这一点。在我的项目中,我需要在用户输入文本时突出显示主题标签。
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
}
}
Run Code Online (Sandbox Code Playgroud)
还有一个辅助扩展来确定范围的帧:
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
}
}
}
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
18439 次 |
| 最近记录: |