NSAttributedString的boundingRectWithSize返回错误的尺寸。

163

我试图获取一个带属性的字符串的矩形范围,但是boundingRectWithSize调用不尊重我传入的大小,并返回一个单行高度的矩形,而不是一个很大的高度(它是一个长字符串)。我已经尝试通过传入一个非常大的高度值和零来实验,如下面的代码所示,但是返回的矩形始终相同。

CGRect paragraphRect = [attributedText boundingRectWithSize:CGSizeMake(300,0.0)
  options:NSStringDrawingUsesDeviceMetrics
  context:nil];

这是损坏了,还是我需要做其他事情才能返回一个包含文本的矩形?


5
你是否有一种段落样式可以截断/裁剪lineBreakMode - danyowdee
1
如果您正在阅读本文,是因为UILabel测量/包装到不正确的宽度,请查看https://dev59.com/ylYO5IYBdhLWcg3wMOxy。具体来说,在应用程序启动时将NSAllowsDefaultLineBreakStrategy设置为false。 - eric
25个回答

328

看起来你没有提供正确的选项。对于标签换行,请至少提供:

CGRect paragraphRect =
  [attributedText boundingRectWithSize:CGSizeMake(300.f, CGFLOAT_MAX)
  options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading)
  context:nil];

注意:如果原始文本的宽度小于300.f,则不会进行换行,因此请确保边界尺寸正确,否则仍会得到错误的结果。


25
每个人都需要查看这个答案,因为它有效。也许需要更清晰的文档来说明这种方法和 attributed strings 的一般性;并不明显你需要在哪里查找。 - macserv
32
注意!它并不总是可行的!有时返回的宽度比size参数指定的宽度更大。即使苹果的方法文档也指出:“您在size参数中指定的约束条件是渲染器如何调整字符串大小的指导方针。但是,如果需要额外的空间来呈现整个字符串,此方法返回的实际边界矩形可以大于约束条件。” - Klaas
80
NSAttributedString和boundingRectWithSize的API非常令人震惊。 - malhal
29
另一个需要注意的问题是,paragraphRect 返回的高度(以及可能的宽度)几乎总是带有小数。因此,它可能会显示高度为 141.3。您需要使用 ceilf(paragraphRect.size.height) 的结果来进行上取整处理。我经常忘记这一点,所以我会想为什么我的标签仍然被裁剪。 - jamone
42
我总是在我的 boundingRectWithSize: 计算中使用 CGRectIntegral() 函数,它将矩形的起点向下舍入并将其大小向上舍入到最接近的整数,以确保高度和宽度不会被截断,尤其是当它们为小数时。 - runmad
显示剩余10条评论

54

由于某些原因,boundingRectWithSize总是返回错误的大小。 我找到了一个解决方案。 UITextView有一个方法-sizeThatFits,可以返回文本所需的正确大小。 因此,不要使用boundingRectWithSize,而是创建一个UITextView,带有随机框架,并调用其sizeThatFits方法,传入相应的宽度和CGFLOAT_MAX高度。 它将返回具有正确高度的大小。

   UITextView *view=[[UITextView alloc] initWithFrame:CGRectMake(0, 0, width, 10)];   
   view.text=text;
   CGSize size=[view sizeThatFits:CGSizeMake(width, CGFLOAT_MAX)];
   height=size.height; 

如果你正在一个while循环中计算大小,请勿忘记将其添加到自动释放池中,因为将创建n个UITextView,如果不使用autoreleasepool,则应用程序的运行时内存将增加。


2
这个方法可行。我尝试过其他方法,但从未成功。我认为我的问题源于我分配的属性字符串的复杂性。它来自一个RTF文件,并使用各种字体、行间距,甚至阴影。尽管使用了UsesLineFragmentOrigin和UsesFontLeading,但我始终无法获得足够高的结果,而且随着属性字符串变得越长,问题变得越严重。我猜对于相对简单的字符串,boundingRectWithSize方法可能有效。在我看来,他们真的需要一种更好的方法来处理这个问题。 - John Bushnell
你不觉得每次调用heightForRowAtIndexPath方法都创建一个UITextView有些过度吗?这会花费时间。 - János
1
我同意你的观点,@János,但这是唯一对我有效的解决方案。 - shoan
顺便说一下,我要求一个正确的解决方案:https://dev59.com/347ea4cB1Zd3GeqPIPY5 - János
1
这是唯一有效的解决方案。这个问题已经存在了9年,苹果显然不想解决它,或者他们的工程师太弱无法找到解决方案。我们在谈论iOS 11,仍然存在着同样的错误。 - Duck
显示剩余5条评论

32

埃德·麦克马纳斯确实提供了一个关键来使这个工作。我找到了一个不起作用的案例。

UIFont *font = ...
UIColor *color = ...
NSDictionary *attributesDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
                                     font, NSFontAttributeName,
                                     color, NSForegroundColorAttributeName,
                                     nil];

NSMutableAttributedString *string = [[NSMutableAttributedString alloc] initWithString: someString attributes:attributesDictionary];

[string appendAttributedString: [[NSAttributedString alloc] initWithString: anotherString];

CGRect rect = [string boundingRectWithSize:constraint options:(NSStringDrawingUsesLineFragmentOrigin|NSStringDrawingUsesFontLeading) context:nil];

rect 的高度将不正确。请注意,string 中追加的 anotherString 是没有属性字典初始化的。这是anotherString 的合法初始化程序,但在这种情况下 boundingRectWithSize: 不会给出准确的大小。


34
谢谢!事实证明,如果您希望boundingRectWithSize实际起作用,则NSAttributedString的每个部分都必须设置一个字典,其中至少设置了NSFontAttributeName和NSForegroundColorAttributeName。我没有看到文档中有这方面的说明。 - Ben Wheeler
4
请注意,似乎不同的NSAttributedStrings合并成一个字符串时必须全部使用相同的字典(因此使用相同的字体),以便boundingRectWithSize正确工作! - Ben Wheeler
1
这个解决方案与我用于“缩小以适应”我的UILabel类别非常相似。对我来说,让它起作用的关键是使用FLT_MAX高度创建边界矩形。如果没有这个,我似乎只会得到一个线条的矩形。 - dre1138
全员+1s。这真的让我很感动,所以感谢你们解决了它。 - Wex
我曾经遇到过这个问题。在NSMutableAttributedString中使用了空格和换行符,但没有属性,导致尺寸计算错误。 - vbezhenar
@Ben Wheeler,你需要将你的评论提交为答案,这样我们才能给它点赞并让它得到应有的关注! - Simo

30

经过长时间的调查研究,我的最终决定是:
- boundingRectWithSize函数仅对连续字符序列返回正确的大小!如果字符串包含空格或其他内容(由Apple称为“一些字形”),则无法获取显示文本所需的实际矩形大小!
我用字母替换了字符串中的空格,立刻得到了正确的结果。

苹果在这里说: https://developer.apple.com/documentation/foundation/nsstring/1524729-boundingrectwithsize

“此方法返回字符串中字形的实际边界。允许一些字形(例如空格)重叠尺寸传递的布局约束,因此在某些情况下,返回的CGRect的尺寸组件的宽度值可能超过大小参数的宽度值。”

因此,需要找到其他计算实际矩形的方法...


经过长时间的调查研究,方案终于找到了!!! 我不能确定它是否适用于与UITextView相关的所有情况,但是最主要和重要的事情已经被发现了!

boundingRectWithSize函数,以及CTFramesetterSuggestFrameSizeWithConstraints(和许多其他方法)将在正确的矩形被使用时计算大小和文本部分的正确值。 例如- UITextView具有textView.bounds.size.width -而此值不是系统在绘制UITextView上的文本时实际使用的矩形。

我找到了一个非常有趣的参数,并在代码中执行了简单的计算:

CGFloat padding = textView.textContainer.lineFragmentPadding;  
CGFloat  actualPageWidth = textView.bounds.size.width - padding * 2;

魔法起作用了 - 现在我的所有文本都正确计算了!享受吧!


1
是的,我也注意到了,它没有保持宽度的限制,总是变得更大。这真的不好,他们废弃了工作方法,现在我们不得不处理这个糟糕的问题。 - Boris Gafurov
1
您,先生,是我的新英雄!这让我疯狂了。我正在设置textContainers插图,所以必须使用textView.textContainer.size.width而不是textView.bounds.size.witdh。 - GCBenson
我无法投足足够的赞。奇怪的是,空格在边界计算中不被考虑。 - Chase Holland
1
将空格替换为一些虚拟字符,如“3”,可以返回准确的高度。 - Abuzar Amin
如果您正在阅读此文是因为UILabel测量/换行宽度不正确,请查看https://dev59.com/ylYO5IYBdhLWcg3wMOxy。具体来说,在应用程序启动时将NSAllowsDefaultLineBreakStrategy设置为false。 - eric
1
这拯救了我的职业生涯,也挽救了我与质量保证团队的关系。我欠你一个啤酒,兄弟!! - lucaslt89

24

Swift 4 版本

let string = "A great test string."
let font = UIFont.systemFont(ofSize: 14)
let attributes: [NSAttributedStringKey: Any] = [.font: font]
let attributedString = NSAttributedString(string: string, attributes: attributes)
let largestSize = CGSize(width: bounds.width, height: .greatestFiniteMagnitude)

//Option one (best option)
let framesetter = CTFramesetterCreateWithAttributedString(attributedString)
let textSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, CFRange(), nil, largestSize, nil)

//Option two
let textSize = (string as NSString).boundingRect(with: largestSize, options: [.usesLineFragmentOrigin , .usesFontLeading], attributes: attributes, context: nil).size

//Option three
let textSize = attributedString.boundingRect(with: largestSize, options: [.usesLineFragmentOrigin , .usesFontLeading], context: nil).size

使用CTFramesetter测量文本效果最佳,因为它提供整数大小并能很好地处理表情符号和其他Unicode字符。


“最佳选项”似乎不再起作用了,但“第三个选项”可以。 - Andrew Koster
这里的选项2对我有用...也许转换为NSString?选项3是我的原始解决方案,但计算高度总是针对一行...过去做了10多次,从未遇到过这种情况... - Adama

12

我在这些建议中没有取得好的结果。我的字符串包含Unicode子弹点,我怀疑它们在计算过程中造成了问题。我注意到UITextView可以很好地处理绘制,所以我寻找了那个来利用其计算。我做了以下操作,可能不如NSString绘制方法优化,但至少是准确的。它也比初始化UITextView只调用 -sizeThatFits:稍微更有效一些。

NSTextContainer *textContainer = [[NSTextContainer alloc] initWithSize:CGSizeMake(width, CGFLOAT_MAX)];
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
[layoutManager addTextContainer:textContainer];

NSTextStorage *textStorage = [[NSTextStorage alloc] initWithAttributedString:formattedString];
[textStorage addLayoutManager:layoutManager];

const CGFloat formattedStringHeight = ceilf([layoutManager usedRectForTextContainer:textContainer].size.height);

1
可能不是Unicode点,而是您字符串末尾的空格。请参见上面的答案:https://dev59.com/i2Yr5IYBdhLWcg3wg6b9#25941139。 - SamB

10

事实证明,如果您希望boundingRectWithSize正常工作,NSAttributedString的每个部分都必须设置一个至少带有NSFontAttributeName和NSForegroundColorAttributeName的字典!

我没有在任何地方看到这个被记录下来。


1
谢谢,这对我来说是解决方案,除了我发现只有NSFontAttributeName是必要的,而不是NSForegroundColorAttributeName。 - Marmoy

9

我发现首选解决方案无法处理换行符。

我发现以下方法在所有情况下都有效:

UILabel* dummyLabel = [UILabel new];
[dummyLabel setFrame:CGRectMake(0, 0, desiredWidth, CGFLOAT_MAX)];
dummyLabel.numberOfLines = 0;
[dummyLabel setLineBreakMode:NSLineBreakByWordWrapping];
dummyLabel.attributedText = myString;
[dummyLabel sizeToFit];
CGSize requiredSize = dummyLabel.frame.size;

在我的情况下,我应该在后台线程上进行计算,不允许使用UI元素。 - Lubbo

9

如果您想通过截断尾部来获取边界框,这个问题可以帮助您解决问题。

CGFloat maxTitleWidth = 200;

NSMutableParagraphStyle *paragraph = [[NSMutableParagraphStyle alloc] init];
paragraph.lineBreakMode = NSLineBreakByTruncatingTail;

NSDictionary *attributes = @{NSFontAttributeName : self.textLabel.font,
                             NSParagraphStyleAttributeName: paragraph};

CGRect box = [self.textLabel.text
              boundingRectWithSize:CGSizeMake(maxTitleWidth, CGFLOAT_MAX)
              options:(NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading)
              attributes:attributes context:nil];

7

@warrenm 很抱歉告诉您,framesetter方法对我没有用。

我得到了这个结果。这个函数可以帮助我们确定iPhone/iPad SDK中NSAttributedString的字符串范围所需的框架大小,给定宽度:

它可用于动态高度的UITableView单元格。

- (CGSize)frameSizeForAttributedString:(NSAttributedString *)attributedString
{
    CTTypesetterRef typesetter = CTTypesetterCreateWithAttributedString((CFAttributedStringRef)attributedString);
    CGFloat width = YOUR_FIXED_WIDTH;

    CFIndex offset = 0, length;
    CGFloat y = 0;
    do {
        length = CTTypesetterSuggestLineBreak(typesetter, offset, width);
        CTLineRef line = CTTypesetterCreateLine(typesetter, CFRangeMake(offset, length));

        CGFloat ascent, descent, leading;
        CTLineGetTypographicBounds(line, &ascent, &descent, &leading);

        CFRelease(line);

        offset += length;
        y += ascent + descent + leading;
    } while (offset < [attributedString length]);

    CFRelease(typesetter);

    return CGSizeMake(width, ceil(y));
}

感谢HADDAD ISSA >>> http://haddadissa.blogspot.in/2010/09/compute-needed-heigh-for-fixed-width-of.html

这篇文章是关于如何计算固定宽度下所需的高度。

在任何设备上都无法运行(iPhone 4和5)。无论是在iOS7.1.2的设备上还是在带有iOS8.3的4s模拟器上都是如此:( - sanjana
需要导入任何内容吗? - jose920405
@KaranAlangat 是的 #import <CoreText/CoreText.h> - jose920405

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