ios计算文本高度的几种方法
本来想尝试写个通过字符串长度得到高度的算法,结果发现不可行,这里记录一下整个过程。
起因
UITableviewCell的高度计算往往依赖于其内部UILabel或者UITextView的高度。
触发高度计算的场景有以下几种:
1 页面第一次加载
2 用户上拉或者下拉操作
3 代码主动触发
4 烂代码,不做缓存
许多开发同学喜欢用sizetoFit自适应,殊不知sizetoFit是一个很耗时的操作,不恰当的使用可能造成页面卡顿,滑动不流畅等等。所以一直想找一个替代方法。思路是通过字符串长度反算高宽,一般在TableViewCell里的文本宽度都是固定的(设计给的),所以最终是要计算文本框的高度。
思路:
如果我们知道一行字符的个数,就可以通过长度/单行个数反算高度。一行字数可以本地通过预处理等到,然后打包进安装包。理论上时间复杂度是O(1)。
问题:
上述算法基于以下前提:
每个字符占的宽度一致
但这是不可能的,肉眼都能发现数字和中文的字符宽度是不一样的。何况还有表情、火星文等奇奇怪怪的文本。
那么退而求其次,能否通过归类来局部加速呢? 比如字符“abc123"和字符"a1b2c3"都由三个字符和三个字母构成的,
其高度应该一致,那么我们可以统计高频的文本长度,来加速大部分的用户场景。
当然,这里也有前提:
某分类下的字符,其每个字符的宽度是一样的。
比如数字和数字的宽度一致,中文字符和中文字符宽度一致,表情的宽度一致等等。
为了验证这个结论,专门写了demo来分析。
计算文本的常用方法如下:
1 用UILabel来计算
NSMutableParagraphStyle*paragraphStyle = [[NSParagraphStyledefaultParagraphStyle]mutableCopy];
paragraphStyle.lineBreakMode=NSLineBreakByWordWrapping;
paragraphStyle.alignment=NSTextAlignmentLeft;
paragraphStyle.lineSpacing=2;
NSMutableAttributedString*attibuteStr = [[NSMutableAttributedStringalloc]initWithString:calcedStr];
[attibuteStraddAttribute:NSFontAttributeNamevalue:[UIFontsystemFontOfSize:14]range:NSMakeRange(0, calcedStr.length)];
[attibuteStraddAttribute:NSParagraphStyleAttributeNamevalue:paragraphStylerange:NSMakeRange(0, calcedStr.length)];
UILabel*label = [UILabelnew];
[labelsetAttributedText:attibuteStr];
label.numberOfLines=0;
label.textColor= [UIColorblackColor];
CGSizeresultSize = [labelsizeThatFits:limitSize];
这里有个新发现,以前一直以为UIView只能在主线程create和add,测试时发现可以在后台创建,并且能计算高度,以后可以预加载数据然后后台计算高度缓存,完全不用卡主线程
2 用[NSString boundingRectWithSize]
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft;
paragraphStyle.lineSpacing = 2;
NSDictionary* attributes = @{NSFontAttributeName:[UIFont systemFontOfSize:14],
NSParagraphStyleAttributeName: paragraphStyle,
NSForegroundColorAttributeName: [UIColor blackColor]};
CGRect rect = [calcedStr boundingRectWithSize:limitSize
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:attributes
context:nil];
3 用CoreText计算
NSMutableParagraphStyle *paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
paragraphStyle.alignment = NSTextAlignmentLeft;
paragraphStyle.lineSpacing = 2;
NSMutableAttributedString *attibuteStr = [[NSMutableAttributedString alloc] initWithString:calcedString];
[attibuteStr addAttribute:NSFontAttributeName value:[UIFont systemFontOfSize:14] range:NSMakeRange(0, calcedString.length)];
[attibuteStr addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, calcedString.length)];
CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)attibuteStr;
CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);
CFRange range = CFRangeMake(0, calcedString.length);
CFRange fitCFRange = CFRangeMake(0, 0);
CGSize newSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, range, NULL, limitSize, &fitCFRange);
if (nil != framesetter) {
CFRelease(framesetter);
framesetter = nil;
}
return CGSizeMake(ceilf(newSize.width), ceilf(newSize.height));
结果:
IMG_2523.PNG
从真机实测的的结果:
时间: CoreText < NSString < UILabel
不过差异不大,毫秒级别,考虑到cpu时钟之类的干扰因素,可以认为是一致的。
差异最大的是计算结果CGSize,在代码中为了保持输入一致,设置相同的文本风格,包括字体大小,行距,对齐方式等:
屏幕快照 2017-09-05 09.23.18.png但最后计算的CGSize不完全相同,差异如下:
Size.width:
CoreText > UILabel > NSString
Size.height:
UILabel > NSString > CoreText
CoreText 得到的结果更接近纯文本本身的高度,在网上搜了下CoreText的渲染原理,找到这张图
image.png所以我猜CoreText比其他两种方式计算得到的Size更宽的原因是因为其他两种有默认的padding或者offset之类的东西,所以他们计算的时候要在原来的传入的Size上减去padding,因为文本展示的区间更小了,所以计算出来的CGSize要“瘦长”一点。查了一下UILable有没有padding之类的接口,没找到。UITextview倒是有,与我们的场景不相符,不纠结了。
从测试见过也可以得到一个结论:
NSString 和 UIlable 的计算结果也有差异,但是如果两个维度向上取整,那么就一致了,所以我们在实际写代码的时候一定要记得向上取整
另外还发现一个有用的方法,以前一直以为UIVidw不能在主线程之外的地方创建和调用,但是在实际调试中,发现可以异步后台线程创建UILabel,并且可以调用boundingRect方法得到计算结果。如果我们想做cell的高度预计算,可以用一个离屏的cell来实现,最简单
言归正传,我们初心还是想从字符串长度推测高度,不过从实际的测试结果来看,无法实现。
屏幕快照 2017-09-05 09.43.31.png上图是纯数字的测试结果,最右边表示触发换行最小字符长度,可以看到单个数字本身的宽度也是不同的,数字“4”最宽,数字“1”最窄。
再看看26个字母:
屏幕快照 2017-09-05 09.45.49.png字母本身的宽度也是不同的,字母“m”最宽,字母"i"和“j"最窄。
由于每个字符的宽度都不同,那么同样长度的字符会有不同的组合,所以想从长度推算宽度的方法失败。