目前市面上的非UI線(xiàn)程文本算高方法或多或少都有一些問(wèn)題。本文通過(guò)逆向和分析UILabel的sizeThatFits方法實(shí)現(xiàn)來(lái)得到一個(gè)最佳的文本算高的精簡(jiǎn)方法。方法可以運(yùn)行在任意線(xiàn)程,因此可以有效的應(yīng)用在那些異步算高或者要求尺寸進(jìn)行提前計(jì)算的場(chǎng)景中。
從iOS官方的實(shí)現(xiàn)中可以看出文本算高會(huì)考慮簡(jiǎn)單文本字符串、屬性字符串、字體大小、最大顯示行數(shù)numberOfLines、段落信息、 段落的對(duì)齊方式、斷字方式、段落的首行縮進(jìn)、陰影偏移等等因素。下面就是具體的實(shí)現(xiàn)代碼:
/// 使用此方法時(shí)請(qǐng)標(biāo)明源作者:歐陽(yáng)大哥2013。本方法符合MIT協(xié)議規(guī)范。
/// github地址:https://github.com/youngsoft
/// 計(jì)算簡(jiǎn)單文本或者屬性字符串的自適應(yīng)尺寸
/// @param fitsSize 指定限制的尺寸,參考UILabel中的sizeThatFits中的參數(shù)的意義。
/// @param text 要計(jì)算的簡(jiǎn)單文本NSString或者屬性字符串NSAttributedString對(duì)象
/// @param numberOfLines 指定最大顯示的行數(shù),如果為0則表示不限制最大行數(shù)
/// @param font 指定計(jì)算時(shí)文本的字體,可以為nil表示使用UILabel控件的默認(rèn)17號(hào)字體
/// @param textAlignment 指定文本對(duì)齊方式默認(rèn)是NSTextAlignmentNatural
/// @param lineBreakMode 指定多行時(shí)斷字模式,默認(rèn)可以用UILabel的默認(rèn)斷字模式NSLineBreakByTruncatingTail
/// @param minimumScaleFactor 指定文本的最小縮放因子,默認(rèn)填寫(xiě)0。這個(gè)參數(shù)用于那些定寬時(shí)可以自動(dòng)縮小文字字體來(lái)自適應(yīng)顯示的場(chǎng)景。
/// @param shadowOffset 指定陰影的偏移位置,需要注意的是這個(gè)偏移位置是同時(shí)指定了陰影顏色和偏移位置才有效。如果不考慮陰影則請(qǐng)傳遞CGSizeZero,否則陰影會(huì)參與尺寸計(jì)算。
/// @return 返回自適應(yīng)的最合適尺寸
CGSize calcTextSize(CGSize fitsSize, id text, NSInteger numberOfLines, UIFont *font, NSTextAlignment textAlignment, NSLineBreakMode lineBreakMode, CGFloat minimumScaleFactor, CGSize shadowOffset) {
if (text == nil || [text length] <= 0) {
return CGSizeZero;
}
NSAttributedString *calcAttributedString = nil;
//如果不指定字體則用默認(rèn)的字體。
if (font == nil) {
font = [UIFont systemFontOfSize:17];
}
CGFloat systemVersion = [UIDevice currentDevice].systemVersion.floatValue;
NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
paragraphStyle.alignment = textAlignment;
paragraphStyle.lineBreakMode = lineBreakMode;
//系統(tǒng)大于等于11才設(shè)置行斷字策略。
if (systemVersion >= 11.0) {
@try {
[paragraphStyle setValue:@(1) forKey:@"lineBreakStrategy"];
} @catch (NSException *exception) {}
}
if ([text isKindOfClass:NSString.class]) {
calcAttributedString = [[NSAttributedString alloc] initWithString:(NSString *)text attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
} else {
NSAttributedString *originAttributedString = (NSAttributedString *)text;
//對(duì)于屬性字符串總是加上默認(rèn)的字體和段落信息。
NSMutableAttributedString *mutableCalcAttributedString = [[NSMutableAttributedString alloc] initWithString:originAttributedString.string attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
//再附加上原來(lái)的屬性。
[originAttributedString enumerateAttributesInRange:NSMakeRange(0, originAttributedString.string.length) options:0 usingBlock:^(NSDictionary<NSAttributedStringKey,id> * _Nonnull attrs, NSRange range, BOOL * _Nonnull stop) {
[mutableCalcAttributedString addAttributes:attrs range:range];
}];
//這里再次取段落信息,因?yàn)橛锌赡軐傩宰址芯鸵呀?jīng)包含了段落信息。
if (systemVersion >= 11.0) {
NSParagraphStyle *alternativeParagraphStyle = [mutableCalcAttributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
if (alternativeParagraphStyle != nil) {
paragraphStyle = (NSMutableParagraphStyle*)alternativeParagraphStyle;
}
}
calcAttributedString = mutableCalcAttributedString;
}
//調(diào)整fitsSize的值, 這里的寬度調(diào)整為只要寬度小于等于0或者顯示一行都不限制寬度,而高度則總是改為不限制高度。
fitsSize.height = FLT_MAX;
if (fitsSize.width <= 0 || numberOfLines == 1) {
fitsSize.width = FLT_MAX;
}
//構(gòu)造出一個(gè)NSStringDrawContext
NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init];
context.minimumScaleFactor = minimumScaleFactor;
@try {
//因?yàn)橄旅鎺讉€(gè)屬性都是未公開(kāi)的屬性,所以我們用KVC的方式來(lái)實(shí)現(xiàn)。
[context setValue:@(numberOfLines) forKey:@"maximumNumberOfLines"];
if (numberOfLines != 1) {
[context setValue:@(YES) forKey:@"wrapsForTruncationMode"];
}
[context setValue:@(YES) forKey:@"wantsNumberOfLineFragments"];
} @catch (NSException *exception) {}
//計(jì)算屬性字符串的bounds值。
CGRect rect = [calcAttributedString boundingRectWithSize:fitsSize options:NSStringDrawingUsesLineFragmentOrigin context:context];
//需要對(duì)段落的首行縮進(jìn)進(jìn)行特殊處理!
//如果只有一行則直接添加首行縮進(jìn)的值,否則進(jìn)行特殊處理。。
CGFloat firstLineHeadIndent = paragraphStyle.firstLineHeadIndent;
if (firstLineHeadIndent != 0.0 && systemVersion >= 11.0) {
//得到繪制出來(lái)的行數(shù)
NSInteger numberOfDrawingLines = [[context valueForKey:@"numberOfLineFragments"] integerValue];
if (numberOfDrawingLines == 1) {
rect.size.width += firstLineHeadIndent;
} else {
//取內(nèi)容的行數(shù)。
NSString *string = calcAttributedString.string;
NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet];
NSArray *lines = [string componentsSeparatedByCharactersInSet:charset]; //得到文本內(nèi)容的行數(shù)
NSString *lastLine = lines.lastObject;
NSInteger numberOfContentLines = lines.count - (NSInteger)(lastLine.length == 0); //有效的內(nèi)容行數(shù)要減去最后一行為空行的情況。
if (numberOfLines == 0) {
numberOfLines = NSIntegerMax;
}
if (numberOfLines > numberOfContentLines)
numberOfLines = numberOfContentLines;
//只有繪制的行數(shù)和指定的行數(shù)相等時(shí)才添加上首行縮進(jìn)!這段代碼根據(jù)反匯編來(lái)實(shí)現(xiàn),但是不理解為什么相等才設(shè)置?
if (numberOfDrawingLines == numberOfLines) {
rect.size.width += firstLineHeadIndent;
}
}
}
//取fitsSize和rect中的最小寬度值。
if (rect.size.width > fitsSize.width) {
rect.size.width = fitsSize.width;
}
//加上陰影的偏移
rect.size.width += fabs(shadowOffset.width);
rect.size.height += fabs(shadowOffset.height);
//轉(zhuǎn)化為可以有效顯示的邏輯點(diǎn), 這里將原始邏輯點(diǎn)乘以縮放比例得到物理像素點(diǎn),然后再取整,然后再除以縮放比例得到可以有效顯示的邏輯點(diǎn)。
CGFloat scale = [UIScreen mainScreen].scale;
rect.size.width = ceil(rect.size.width * scale) / scale;
rect.size.height = ceil(rect.size.height *scale) / scale;
return rect.size;
}
//上述方法的精簡(jiǎn)版本
NS_INLINE CGSize calcTextSizeV2(CGSize fitsSize, id text, NSInteger numberOfLines, UIFont *font) {
return calcTextSize(fitsSize, text, numberOfLines, font, NSTextAlignmentNatural, NSLineBreakByTruncatingTail,0.0, CGSizeZero);
}
下面是具體的驗(yàn)證測(cè)試用例(用例在iOS9到iOS13上運(yùn)行通過(guò)):
CFTimeInterval simpleTextUILabelInterval = 0;
CFTimeInterval simpleTextNOUILabelInterval = 0;
CFTimeInterval attributedTextUILabelInterval = 0;
CFTimeInterval attributedTextNOUILabelInterval = 0;
NSArray *testStringArray = @[@"您",@"好",@"中",@"國(guó)",@"w",@"i",@"d",@"t",@"h",@",",@"。",@"a",@"b",@"c",@"\n", @"1",@"5",@"2",@"j",@"A",@"J",@"0",@"??",@"??",@" "];
srand(time(NULL));
for (int i = 0; i < 5000; i++) {
//隨機(jī)生成0到100個(gè)字符。
int textLength = rand() % 100;
NSMutableString *text = [NSMutableString new];
for (int j = 0; j < textLength; j++) {
[text appendString:testStringArray[rand()%testStringArray.count]];
}
if (text.length == 0)
continue;
CGSize fitSize = CGSizeMake(rand()%1000, rand()%1000);
//測(cè)試簡(jiǎn)單文本。
UILabel *label = [UILabel new];
label.text = text;
label.numberOfLines = rand() % 100;
label.textAlignment = rand() % 5;
label.lineBreakMode = rand() % 7;
label.font = [UIFont systemFontOfSize:rand()%30 + 5.0];
CFTimeInterval start = CACurrentMediaTime();
CGSize sz1 = [label sizeThatFits:fitSize];
simpleTextUILabelInterval += CACurrentMediaTime() - start;
start = CACurrentMediaTime();
CGSize sz2 = calcTextSize(fitSize, label.text, label.numberOfLines, label.font, label.textAlignment, label.lineBreakMode, label.minimumScaleFactor, CGSizeZero);
simpleTextNOUILabelInterval += CACurrentMediaTime() - start;
NSAssert(CGSizeEqualToSize(sz1, sz2), @"");
//測(cè)試富文本
NSRange range1 = NSMakeRange(0, rand()%text.length);
NSMutableParagraphStyle *paragraphStyle1 = [[NSMutableParagraphStyle alloc] init];
paragraphStyle1.lineSpacing = rand() % 20;
paragraphStyle1.firstLineHeadIndent = rand() %10;
paragraphStyle1.paragraphSpacing = rand() % 30;
paragraphStyle1.headIndent = rand() % 10;
paragraphStyle1.tailIndent = rand() % 10;
UIFont *font1 = [UIFont systemFontOfSize:rand() % 20 + 3.0];
NSRange range2 = NSMakeRange(range1.length, text.length - range1.length);
NSMutableParagraphStyle *paragraphStyle2 = [[NSMutableParagraphStyle alloc] init];
paragraphStyle2.lineSpacing = rand() % 20;
paragraphStyle2.firstLineHeadIndent = rand() %10;
paragraphStyle2.paragraphSpacing = rand() % 30;
paragraphStyle2.headIndent = rand() % 10;
paragraphStyle2.tailIndent = rand() % 10;
UIFont *font2 = [UIFont systemFontOfSize:rand() % 20 + 3.0];
NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:text];
[attributedText addAttributes:@{NSParagraphStyleAttributeName:paragraphStyle1,NSFontAttributeName:font1} range:range1];
[attributedText addAttributes:@{NSParagraphStyleAttributeName:paragraphStyle2,NSFontAttributeName:font2} range:range2];
label = [UILabel new];
label.numberOfLines = rand() % 100;
label.textAlignment = rand() % 5;
label.lineBreakMode = rand() % 7;
label.font = [UIFont systemFontOfSize:rand()%30 + 5.0];
label.attributedText = attributedText;
start = CACurrentMediaTime();
CGSize sz3 = [label sizeThatFits:fitSize];
attributedTextUILabelInterval += CACurrentMediaTime() - start;
start = CACurrentMediaTime();
CGSize sz4 = calcTextSize(fitSize, label.attributedText, label.numberOfLines, label.font, label.textAlignment, label.lineBreakMode, 0.0, CGSizeZero);
attributedTextNOUILabelInterval += CACurrentMediaTime() - start;
NSAssert(CGSizeEqualToSize(sz3, sz4), @"");
}
simpleTextUILabelInterval *= 1000;
simpleTextNOUILabelInterval *= 1000;
attributedTextUILabelInterval *= 1000;
attributedTextNOUILabelInterval *= 1000;
NSLog(@"簡(jiǎn)單文本計(jì)算UILabel總耗時(shí)(毫秒):%.3f, 平均耗時(shí):%.3f",simpleTextUILabelInterval, simpleTextUILabelInterval / 5000);
NSLog(@"簡(jiǎn)單文本計(jì)算非UILabel總耗時(shí)(毫秒):%.3f, 平均耗時(shí):%.3f",simpleTextNOUILabelInterval, simpleTextNOUILabelInterval / 5000);
NSLog(@"富文本計(jì)算UILabel總耗時(shí)(毫秒):%.3f, 平均耗時(shí):%.3f",attributedTextUILabelInterval, attributedTextUILabelInterval / 5000);
NSLog(@"富文本計(jì)算非UILabel總耗時(shí)(毫秒):%.3f, 平均耗時(shí):%.3f",attributedTextNOUILabelInterval, attributedTextNOUILabelInterval / 5000);
關(guān)注: 歐陽(yáng)大哥2013簡(jiǎn)書(shū)|歐陽(yáng)大哥2013掘金|歐陽(yáng)大哥2013Github