如何在固定宽度下获取NSAttributedString的高度

52
我想在固定宽度的框中绘制NSAttributedStrings,但计算其绘制时所占高度却十分困难。 我已经尝试过以下方法:
  1. 调用- (NSSize) size,但结果无用(对于此目的),因为它们将提供字符串需要的任何宽度。

  2. 使用一个形状为我想要的宽度和NSStringDrawingUsesLineFragmentOrigin选项的矩形调用- (void)drawWithRect:(NSRect)rect options:(NSStringDrawingOptions)options,与我的绘图完全相同。 结果很难理解; 绝对不是我要寻找的(正如在许多地方指出的那样,包括这个 Cocoa-Dev线程)。

  3. 创建一个临时NSTextView并执行:
    [[tmpView textStorage] setAttributedString:aString];
    [tmpView setHorizontallyResizable:NO];
    [tmpView sizeToFit];

    当我查询tmpView的框架时,宽度仍然符合要求,而高度通常是正确的...直到我遇到较长的字符串时,它通常只有所需大小的一半。 (似乎没有达到最大尺寸:一个框架将高273.0(大约缺少了300),另一个框架将高478.0(仅缺少60左右))。

如果其他人曾成功执行此操作,我希望能得到任何指针。
12个回答

31
-[NSAttributedString boundingRectWithSize:options:]

你可以使用NSStringDrawingUsesDeviceMetrics指定参数,以获取所有字形边界的联合

-[NSAttributedString size]不同,返回的NSRect表示当字符串绘制时会更改的区域的尺寸。

如@Bryan所述,在OS X 10.11及更高版本中,建议不再使用boundingRectWithSize:options:。这是因为字符串样式现在根据上下文动态变化。

对于OS X 10.11及更高版本,请参阅苹果公司的计算文本高度开发人员文档。


一個沒有可見內容的字符串返回零大小矩形是有道理的。您是否在iOS 8上看到了與iOS 7不同的行為? - Graham Miln
我的意思是,如果你有一个只有文本(没有字形)的 NSAttributedString,并且使用 NSStringDrawingUsesDeviceMetrics,那么大小就是 CGSizeZero。因此,在决定如何获取大小之前,似乎必须知道是否有字形... - zekel
有没有可能出现没有字形的文本?这是常见情况吗? - Graham Miln
请注意:为了使此方法返回合理的结果,您必须传递NSStringDrawingUsesLineFragmentOrigin选项。有关更多信息,请参见下面的我的答案。 - Bryan
3
坏消息:这种方法在操作系统10.11及以上版本中被弃用,而且它不再返回正确的高度,太低了。但是,在10.10及以下版本中可以很好地工作。 - Bryan
显示剩余4条评论

14

答案是使用
- (void)drawWithRect:(NSRect)rect options:(NSStringDrawingOptions)options
但你传入的rect在你希望无限制的维度上应该为0.0(这很合理)。示例在此处


2
这个答案已经过时了。请参考下面Graham的回答,这是2014年后的正确方法。 - dwlz
这已经过时了。请参考 https://dev59.com/aHE95IYBdhLWcg3whOHK#54980862 获取最新的答案。 - Andrew Koster

12

我有一个带有多种字体的复杂属性字符串,在尝试了一些之前的答案后,得到了不正确的结果。使用UITextView可以给出正确的高度,但对于我的使用情况(调整集合单元格大小)太慢了。我编写了Swift代码,使用了与此前参考的苹果文档和Erik描述的相同的一般方法。这使我得到了正确的结果,并且执行速度比让UITextView进行计算要快得多。

private func heightForString(_ str : NSAttributedString, width : CGFloat) -> CGFloat {
    let ts = NSTextStorage(attributedString: str)

    let size = CGSize(width:width, height:CGFloat.greatestFiniteMagnitude)

    let tc = NSTextContainer(size: size)
    tc.lineFragmentPadding = 0.0

    let lm = NSLayoutManager()
    lm.addTextContainer(tc)

    ts.addLayoutManager(lm)
    lm.glyphRange(forBoundingRect: CGRect(origin: .zero, size: size), in: tc)

    let rect = lm.usedRect(for: tc)

    return rect.integral.size.height
}

这解决了我的问题。我有一个带有许多不同范围的属性的属性字符串,而典型的测量API并没有完全起作用。感谢您的答案。 - naturaln0va
这已经过时了。请参考 https://dev59.com/aHE95IYBdhLWcg3whOHK#54980862 获取最新的答案。 - Andrew Koster

5

您可能会对Jerry Krinock的优秀(仅限于OS X)NS(Attributed)String+Geometrics类别感兴趣,该类别旨在进行各种字符串测量,包括您所需的内容。


1
那段代码似乎是针对Mac的,它对iOS开发没有帮助。 - Brennan
3
没错,这个问题涉及到的是NSTextView,这是一个Mac类。当我回答这个问题的时候,我甚至不确定iOS是否已经拥有NSAttributedString - Rob Keniger
1
晚了点,但是提醒一下:我发现NS(Attributed)String+Geometrics在计算长字符串所需高度时经常低估。直到现在,我都是通过增加额外的25%来修正结果。现在我正在寻找一个更好的解决方案。我认为NS(Attributed)String+Geometrics在10.9、10.10和10.11上表现不佳。 - Bryan
1
这已经过时了。请参考 https://dev59.com/aHE95IYBdhLWcg3whOHK#54980862 获取最新的答案。 - Andrew Koster

3
在OS X 10.11+上,以下方法对我有效(来自Apple的计算文本高度文档)。
- (CGFloat)heightForString:(NSAttributedString *)myString atWidth:(float)myWidth
{
    NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:myString];
    NSTextContainer *textContainer = [[NSTextContainer alloc] initWithContainerSize:
        NSMakeSize(myWidth, FLT_MAX)];
    NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
    [layoutManager addTextContainer:textContainer];
    [textStorage addLayoutManager:layoutManager];
    [layoutManager glyphRangeForTextContainer:textContainer];
    return [layoutManager
        usedRectForTextContainer:textContainer].size.height;
}

这已经过时了。请查看https://dev59.com/aHE95IYBdhLWcg3whOHK#54980862以获取最新的答案。 - Andrew Koster
这是我经过大量使用boundingRectWithSize:后唯一有效的方法,谢谢! - Noah Nuebling
@AndrewKoster,你应该考虑删除你的评论。它让我感到困惑,并阻止我尝试一个最终对我有效的解决方案,而不是你所说的最新答案。 - Noah Nuebling
然而,当我为宽度提供了除FLT_MAX以外的值时,这似乎只有在这种情况下才有效。 - Noah Nuebling
@NoahNuebling,我的链接指向了@ZAFAR007的答案。很遗憾,Stack Overflow的CSS有些偏差。 @ZAFAR007的答案是使用Swift和boundingRect。相较于这篇答案,它更加简单,并且在我的测试中比这篇答案表现得更好。 - Andrew Koster
@AndrewKoster 我使用带有粗体文本和链接(包括下划线)的属性字符串。boundingRect 方法似乎运行得很好,但在某些边缘情况下会出现问题。它稍微低估了宽度(如果我没记错的话)。我尝试了很多方法来使其正常工作,但对我来说都没有用。上面的评论甚至提到 boundingRect 方法在 10.11+ 中已经被弃用。相比之下,layourManager 解决方案直接来自于苹果文档,据我所知并未被弃用。因此,我认为这个解决方案对大多数人来说有更高的成功率。 - Noah Nuebling

3

Swift 3:

let attributedStringToMeasure = NSAttributedString(string: textView.text, attributes: [
        NSFontAttributeName: UIFont(name: "GothamPro-Light", size: 15)!,
        NSForegroundColorAttributeName: ClickUpConstants.defaultBlackColor
])

let placeholderTextView = UITextView(frame: CGRect(x: 0, y: 0, width: widthOfActualTextView, height: 10))
placeholderTextView.attributedText = attributedStringToMeasure
let size: CGSize = placeholderTextView.sizeThatFits(CGSize(width: widthOfActualTextView, height: CGFloat.greatestFiniteMagnitude))

height = size.height

这个答案对我非常有效,与其他答案不同的是,其他答案在处理较长字符串时会给出错误的高度。
如果您想使用普通文本而不是属性文本来实现此目的,请按照以下步骤操作:
let placeholderTextView = UITextView(frame: CGRect(x: 0, y: 0, width: ClickUpConstants.screenWidth - 30.0, height: 10))
placeholderTextView.text = "Some text"
let size: CGSize = placeholderTextView.sizeThatFits(CGSize(width: widthOfActualTextView, height: CGFloat.greatestFiniteMagnitude))

height = size.height

这已经过时了。请参考 https://dev59.com/aHE95IYBdhLWcg3whOHK#54980862 获取最新的答案。 - Andrew Koster

3

Swift 4.2

let attributedString = self.textView.attributedText
let rect = attributedString?.boundingRect(with: CGSize(width: self.textView.frame.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
print("attributedString Height = ",rect?.height)

2

我刚刚在这个问题上浪费了很多时间,所以我提供了一个额外的答案来帮助未来的读者。Graham的答案正确率达到了90%,但是它缺少了一个关键点:

为了使用-boundingRectWithSize:options:获得准确的结果,你必须传递以下选项

NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesDeviceMetrics|NSStringDrawingUsesFontLeading

如果省略lineFragmentOrigin参数,你将得到无意义的结果;返回的矩形将只有一行高,并且不会考虑你传递给该方法的大小。
为什么这么复杂,文档又那么差,我也不清楚。但是你有了这些选项,它就可以完美地工作了(至少在OS X上)。

此外:这假设您已经使用给定字体创建了NSAttributedString,并且您正在寻找的NSParagraphStyle - 在我的情况下,这是一个左对齐,自动换行,无连字符的段落样式。不要忘记在创建attributedString时创建它。 - Bryan
此方法在10.11+上已被弃用,不再正确工作;在El Capitan上返回的高度太小。 - Bryan

1

这一页上没有一个答案对我起作用,甚至来自苹果的文档的古老Objective-C代码也不行。最终我解决了UITextView的问题,首先设置其textattributedText属性,然后像这样计算所需的大小:

let size = textView.sizeThatFits(CGSize(width: maxWidth, height: CGFloat.max))

完美运行。太棒了!


终于有东西是真正有效的了。 - hotdogsoup.nl

1
作为许多人之前提到的,基于我的测试结果。
我在iOS上使用open func boundingRect(with size: CGSize, options: NSStringDrawingOptions = [], context: NSStringDrawingContext?) -> CGRect,如下所示:
let rect = attributedTitle.boundingRect(with: CGSize(width:200, height:0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil)

这里的 200 是固定宽度,如您所期望的,高度我给了0,因为我认为最好明确告诉API高度是无限的
选项在这里并不重要,我尝试过 .usesLineFragmentOrigin.usesLineFragmentOrigin.union(.usesFontLeading).usesLineFragmentOrigin.union(.usesFontLeading).union(.usesDeviceMetrics),结果相同。
而且结果符合我的想法。
谢谢。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接