开发者

Discrepancy between sizeWithFont:constrainedToSize:lineBreakMode: and textView.contentSize.height

I am using the method [string sizeWithFont:constrainedToSize:lineBreakMode:] to estimate the height of a textView that I am resizing. However, it seems to consistently return the incorrect size. To debug, I wrote the following code:

self.textView.font = [UIFont systemFontOfSize:14.0]; // It was this before, anyways
NSLog(@"Real width: %lf %lf", self.textView.contentSize.width, self.textView.frame.size.width);
NSLog(@"Real height: %lf", self.textView.contentSize.height);
NSLog(@"Estimated width: %lf", kOTMessageCellTextWidth);
NSLog(@"Estimated height: %lf", ([message.message sizeWithFont:[UIFont systemFontOfSize:14.0]
                                             constrainedToSize:CGSizeMake(kOTMessageCellTextWidth, CGFLOAT_MAX)
                                                 lineBreakMode:UILineBreakModeWordWrap].height));

However, the above code reveals that I am getting inconsistent results:

Real width: 223.000000 223.000000

Real height: 52.000000

Estimated width: 223.000000

Estimated height: 36.000000

Real width: 223.000000 223.000000

Real height: 1开发者_开发问答42.000000

Estimated width: 223.000000

Estimated height: 126.000000

Real width: 223.000000 223.000000

Real height: 142.000000

Estimated width: 223.000000

Estimated height: 126.000000

I noticed in this similar question that (apparently) textView has some padding that constrains its actual width. The recommendation there was the decrease the width passed to sizeWithFont: by some number. The recommended number was 11.

My question: is there any way to actually retrieve this value programmatically, or is there some documentation that I missed specifying this number? It seems like this number should be available and shouldn't have to be guess-and-checked, but I can't find a reliable way to identify it.

Thanks in advance.


OK, after a bit of (re)search, I've come to this.

UITextView has 8px of padding on each side. But also line-height is not the same with UILabel (and sizeWithFont methods' result)

in my case the font was Arial and the size was 16pt and line heights of textview is 4.5points more than uilabel's line heights. So;

  • I get the result (cSize) from sizeWithFont method, with giving width reduced by 16 pixels
  • I calculated the lineCount by cSize.height / font.lineHeight
  • and used cSize.height + (lineCount * 4.5) + 16.0 as the final height for textview

code (not the exact code):

CGSize constraintSize = CGSizeMake(250.0 - 16.0, MAXFLOAT);
CGSize labelSize = [message.text sizeWithFont:[UIFont fontWithName:@"Arial" size:16.0] constrainedToSize:constraintSize lineBreakMode:UILineBreakModeWordWrap];
CGFloat lineCount = labelSize.height / [UIFont fontWithName:@"Arial" size:16.0].lineHeight;
CGFloat additional = lineCount * 4.5;       // for 16.0 pt Arial for now
return labelSize.height + additional + 16.0;    // TextView fix

I'm still having a little problems and do not like the way i solved it. But seems ok for this problem. And It would be great if someone comes with a reasonable solution for this.


The fix is simple, since UITextView has 8px of padding on each side, so when you are calculating text's true size, you should minus this 16px padding (8px on double sides) first, then the result is right. Please see following code snippet:

// self.descView is an UITextView
// UITEXTVIEW_TEXT_PADDING is 8.0
CGSize constrainedSize = CGSizeMake(self.descView.contentSize.width-UITEXTVIEW_TEXT_PADDING*2, MAXFLOAT);
CGSize trueSize = [self.descView.text sizeWithFont:self.descView.font 
                                 constrainedToSize:constrainedSize 
                                     lineBreakMode:UILineBreakModeWordWrap];

CGSize contentSize = self.descView.contentSize;
contentSize.height = trueSize.height;
self.descView.contentSize = contentSize;

frame = self.descView.frame;
frame.size.height = contentSize.height ;
self.descView.frame = frame;

The result should be right.


NSString sizeWithFont is computed using Core Text. UITextView uses a Webview (internally) to render its contents.

This discrepancy leads to all sorts of headaches. Such as choosing to break lines at different locations — you'll notice comma delimited lists, and certain mathematical expression strings will break on different symbols between Core Text and UITextView. This often leads to out-by-one-line errors when measuring spaces for your strings to fit.

And if you want international character support — well, you can forget about Core Text line heights matching UITextView (no matter how many combinations of paragraph style and line height you try!).

I have found that the only way to reliably estimate the size for a UITextView given an NSString is to... actually construct the UITextView and read its fitted size (minus the 8px padding).

Here's a UIKitLineMetrics class that does exactly that.

It keeps two "dummy" text views. You update the instance with your desired font and layout width, and you can read the single and multi-line sizes out of it.

UIKitLineMetrics.h

#import <UIKit/UIKit.h>

@interface UIKitLineMetrics : NSObject

@property (nonatomic, strong) NSMutableAttributedString *attributedString;

- (CGSize) UIKitLineSizeForText:(NSString*)text;
- (CGSize) UIKitSingleLineSizeForText:(NSString*)text;

- (void) updateWithFont:(UIFont*)font andWidth:(CGFloat)width;

@end

UIKitLineMetrics.m

#import "UIKitLineMetrics.h"

@interface UIKitLineMetrics ()

@property (nonatomic, strong) UITextView *measuringTextView;
@property (nonatomic, strong) UITextView *measuringSingleTextView;

@end

@implementation UIKitLineMetrics

@synthesize measuringTextView;
@synthesize measuringSingleTextView;
@synthesize attributedString;

- (id) init
{
    self = [super init];

    if( self )
    {
        attributedString = [[NSMutableAttributedString alloc] init];
        measuringTextView = [[UITextView alloc] initWithFrame:CGRectZero];
        measuringSingleTextView = [[UITextView alloc] initWithFrame:CGRectZero];
    }

    return self;
}

- (CGSize) UIKitLineSizeForText:(NSString*)text
{
    measuringTextView.text = [text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];

    CGSize sz = [measuringTextView sizeThatFits:CGSizeMake(measuringTextView.frame.size.width, CGFLOAT_MAX)];

    return CGSizeMake(sz.width - 16, sz.height - 16);
}

- (CGSize) UIKitSingleLineSizeForText:(NSString*)text
{
    measuringSingleTextView.text = [text stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];

    CGSize sz = [measuringSingleTextView sizeThatFits:CGSizeMake(measuringSingleTextView.frame.size.width, CGFLOAT_MAX)];

    return CGSizeMake(sz.width - 16, sz.height - 16);
}

- (void) updateWithFont:(UIFont*)font andWidth:(CGFloat)width
{
    measuringTextView.font = font;    
    measuringSingleTextView.font = font;

    measuringTextView.frame = CGRectMake(0, 0, width, 500);
    measuringSingleTextView.frame = CGRectMake(0, 0, 50000, 500);
}

@end


You could try checking the contentInset value (a property of UIScrollView).


I've also struggled with the problem. I've usually gotten by by passing the sizeWithFont method a size that's smaller than the actual size of the textView. Even when adding the textview's content insets to the size, it doesn't work. The size returned is always smaller.

  • charlie


I use following function without much problem:

+(float) calculateHeightOfTextFromWidth:(NSString*) text: (UIFont*)withFont: (float)width :(UILineBreakMode)lineBreakMode

{
    [text retain];
    [withFont retain];
    CGSize suggestedSize = [text sizeWithFont:withFont constrainedToSize:CGSizeMake(width, FLT_MAX) lineBreakMode:lineBreakMode];

[text release];
[withFont release];

return suggestedSize.height;
}
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜