如何在UILabel中findCGRect文本的子string?
对于一个给定的NSRange
,我想在UILabel
中find一个对应于该NSRange
的字形的NSRange
。 例如,我想在“快速的棕色狐狸跳过懒狗”一句中find包含“狗”一词的CGRect
。
诀窍是, UILabel
有多行,文本是真正的attributedText
文本,所以要findstring的确切位置有点困难。
我想写在我的UILabel
子类上的方法看起来像这样:
- (CGRect)rectForSubstringWithRange:(NSRange)range;
详情,对于那些有兴趣的人:
我的目标是能够创build一个具有UILabel的确切外观和位置的新UILabel,然后我可以进行animation制作。 其余的我已经知道了,但是特别是这一步让我暂时陷入困境。
到目前为止,我所做的努力来解决这个问题:
- 我希望在iOS 7中,会有一些文本工具包可以解决这个问题,但是大多数我见过的使用Text Kit的例子都集中在了
UITextView
和UITextField
,而不是UILabel
。 - 我看到另一个关于Stack Overflow的问题,可以解决这个问题,但是接受的答案已经超过两年了,而代码对于归因文本performance不佳。
我敢打赌,正确的答案涉及以下其中之一:
- 使用标准的文本工具包方法在一行代码中解决这个问题。 我敢打赌,它会涉及
NSLayoutManager
和textContainerForGlyphAtIndex:effectiveRange
- 编写一个复杂的方法,将UILabel分解成行,并且可能使用Core Text方法在行内find字形的矩形。 我现在最好的select是分开@ mattt的优秀TTTAttributedLabel ,它有一个方法可以在一个点find一个字形 – 如果我反转,find一个字形的点,可能工作。
更新:这里有一个Github的要点,我已经尝试到目前为止,以解决这个问题的三件事情: https : //gist.github.com/bryanjclark/7036101
按照约书亚在密码中的回答 ,我提出了以下似乎运作良好的问题:
- (CGRect)boundingRectForCharacterRange:(NSRange)range { NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:[self attributedText]]; NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init]; [textStorage addLayoutManager:layoutManager]; NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:[self bounds].size]; textContainer.lineFragmentPadding = 0; [layoutManager addTextContainer:textContainer]; NSRange glyphRange; // Convert the range for glyphs. [layoutManager characterRangeForGlyphRange:range actualGlyphRange:&glyphRange]; return [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; }
以卢克·罗杰斯的答案为基础,但写得很快:
extension UILabel { func boundingRectForCharacterRange(range: NSRange) -> CGRect? { guard let attributedText = attributedText else { return nil } let textStorage = NSTextStorage(attributedString: attributedText) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: bounds.size) textContainer.lineFragmentPadding = 0.0 layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // Convert the range for glyphs. layoutManager.characterRangeForGlyphRange(range, actualGlyphRange: &glyphRange) return layoutManager.boundingRectForGlyphRange(glyphRange, inTextContainer: textContainer) } }
Swift 3
extension UILabel { func boundingRectForCharacterRange(range: NSRange) -> CGRect? { guard let attributedText = attributedText else { return nil } let textStorage = NSTextStorage(attributedString: attributedText) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: bounds.size) textContainer.lineFragmentPadding = 0.0 layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() // Convert the range for glyphs. layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) } }
我的build议是使用文本工具包。 不幸的是,我们无法访问UILabel
使用的布局pipe理器,但是可能创build它的副本并使用它来获取范围的矩形。
我的build议是创build一个NSTextStorage
对象,其中包含与标签中相同的属性文本。 接下来创build一个NSLayoutManager
并将其添加到文本存储对象。 最后,创build一个与标签大小相同的NSTextContainer
,并将其添加到布局pipe理器中。
现在文本存储与标签有相同的文本,文本容器的大小与标签的大小相同,所以我们应该能够使用boundingRectForGlyphRange:inTextContainer:
来请求为我们的范围创build的布局pipe理器。 确保在布局pipe理器对象上使用glyphRangeForCharacterRange:actualCharacterRange:
首先将字符范围转换为字形范围。
一切顺利,应该给你一个你在标签内指定范围的CGRect
。
我还没有testing过这个,但是这将是我的方法,并且模仿UILabel
本身的工作应该有一个很好的成功的机会。
你可以而不是基于你的类在UITextView? 如果是这样,请查看UiTextInput协议方法。 特别参见几何形状和命中rest方法。
不幸的是,在UILabel中没有直接的API。 为了准确地支持这个function,你需要做大量的工作来保持不同angular色的大小。 但是,有人已经花时间来搞清楚了。 请看下面的一段代码,看看是否适合你:
UILabel获取CGRect作为文本的子string
翻译为Swift 3。
func boundingRectForCharacterRange(_ range: NSRange) -> CGRect { let textStorage = NSTextStorage(attributedString: self.attributedText!) let layoutManager = NSLayoutManager() textStorage.addLayoutManager(layoutManager) let textContainer = NSTextContainer(size: self.bounds.size) textContainer.lineFragmentPadding = 0 layoutManager.addTextContainer(textContainer) var glyphRange = NSRange() layoutManager.characterRange(forGlyphRange: range, actualGlyphRange: &glyphRange) return layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer) }
另一个这样做的方法,如果你有自动调整字体大小,将是这样的:
let stringLength: Int = countElements(self.attributedText!.string) let substring = (self.attributedText!.string as NSString).substringWithRange(substringRange) //First, confirm that the range is within the size of the attributed label if (substringRange.location + substringRange.length > stringLength) { return CGRectZero } //Second, get the rect of the label as a whole. let textRect: CGRect = self.textRectForBounds(self.bounds, limitedToNumberOfLines: self.numberOfLines) let path: CGMutablePathRef = CGPathCreateMutable() CGPathAddRect(path, nil, textRect) let framesetter = CTFramesetterCreateWithAttributedString(self.attributedText) let tempFrame: CTFrameRef = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, stringLength), path, nil) if (CFArrayGetCount(CTFrameGetLines(tempFrame)) == 0) { return CGRectZero } let lines: CFArrayRef = CTFrameGetLines(tempFrame) let numberOfLines: Int = self.numberOfLines > 0 ? min(self.numberOfLines, CFArrayGetCount(lines)) : CFArrayGetCount(lines) if (numberOfLines == 0) { return CGRectZero } var returnRect: CGRect = CGRectZero let nsLinesArray: NSArray = CTFrameGetLines(tempFrame) // Use NSArray to bridge to Array let ctLinesArray = nsLinesArray as Array var lineOriginsArray = [CGPoint](count:ctLinesArray.count, repeatedValue: CGPointZero) CTFrameGetLineOrigins(tempFrame, CFRangeMake(0, numberOfLines), &lineOriginsArray) for (var lineIndex: CFIndex = 0; lineIndex < numberOfLines; lineIndex++) { let lineOrigin: CGPoint = lineOriginsArray[lineIndex] let line: CTLineRef = unsafeBitCast(CFArrayGetValueAtIndex(lines, lineIndex), CTLineRef.self) //CFArrayGetValueAtIndex(lines, lineIndex) let lineRange: CFRange = CTLineGetStringRange(line) if ((lineRange.location <= substringRange.location) && (lineRange.location + lineRange.length >= substringRange.location + substringRange.length)) { var charIndex: CFIndex = substringRange.location - lineRange.location; // That's the relative location of the line var secondary: CGFloat = 0.0 let xOffset: CGFloat = CTLineGetOffsetForStringIndex(line, charIndex, &secondary); // Get bounding information of line var ascent: CGFloat = 0.0 var descent: CGFloat = 0.0 var leading: CGFloat = 0.0 let width: Double = CTLineGetTypographicBounds(line, &ascent, &descent, &leading) let yMin: CGFloat = floor(lineOrigin.y - descent); let yMax: CGFloat = ceil(lineOrigin.y + ascent); let yOffset: CGFloat = ((yMax - yMin) * CGFloat(lineIndex)) returnRect = CalculationHelper.sizeOfString(substring, font: self.font, width: DBL_MAX, height: DBL_MAX) returnRect.origin.x = xOffset + self.frame.origin.x returnRect.origin.y = yOffset + self.frame.origin.y + ((self.frame.size.height - textRect.size.height) / 2) break } } return returnRect