NSAttributedString(富文本),是一種帶有屬性的字符串,通過它可以輕松的在一個字符串中表現(xiàn)出多種字體、字號、背景色、下劃線等各不相同的風(fēng)格,還可以對段落進(jìn)行格式化。下面就來探討一下動態(tài)計(jì)算NSAttributedString的size大小實(shí)現(xiàn):
-
首先提供一個對NSAttributedString進(jìn)行封裝的函數(shù)
該方法會為NSAttributedString添加默認(rèn)段落屬性以及字體屬性(如果不存在的話)/** * return 返回封裝后的NSMutableAttributedString,添加了默認(rèn)NSParagraphStyleAttributeName與NSFontAttributeName屬性 * * @param labelStr NSString * @param labelDic 屬性字典 @{ NSFontAttributeName://(字體) NSBackgroundColorAttributeName://(字體背景色) NSForegroundColorAttributeName://(字體顏色) NSParagraphStyleAttributeName://(段落) NSLigatureAttributeName://(連字符) NSKernAttributeName://(字間距) NSStrikethroughStyleAttributeName://NSUnderlinePatternSolid(實(shí)線) | NSUnderlineStyleSingle(刪除線) NSUnderlineStyleAttributeName://(下劃線) NSStrokeColorAttributeName://(邊線顏色) NSStrokeWidthAttributeName://(邊線寬度) NSShadowAttributeName://(陰影) NSVerticalGlyphFormAttributeName://(橫豎排版) } * * @return NSMutableAttributedString */ + (NSMutableAttributedString *)getNSAttributedString:(NSString *)labelStr labelDict:(NSDictionary *)labelDic { NSMutableAttributedString *atrString = [[NSMutableAttributedString alloc] initWithString:labelStr]; NSRange range = NSMakeRange(0, atrString.length); if (labelDic && labelDic.count > 0) { NSEnumerator *enumerator = [labelDic keyEnumerator]; id key; while ((key = [enumerator nextObject])) { [atrString addAttribute:key value:labelDic[key] range:range]; } } //段落屬性 NSMutableParagraphStyle *paragraphStyle = labelDic[NSParagraphStyleAttributeName]; if (!paragraphStyle || nil == paragraphStyle) { paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; paragraphStyle.lineSpacing = 0.0;//增加行高 paragraphStyle.headIndent = 0;//頭部縮進(jìn),相當(dāng)于左padding paragraphStyle.tailIndent = 0;//相當(dāng)于右padding paragraphStyle.lineHeightMultiple = 0;//行間距是多少倍 paragraphStyle.alignment = NSTextAlignmentLeft;//對齊方式 paragraphStyle.firstLineHeadIndent = 0;//首行頭縮進(jìn) paragraphStyle.paragraphSpacing = 0;//段落后面的間距 paragraphStyle.paragraphSpacingBefore = 0;//段落之前的間距 [atrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; } //字體 UIFont *font = labelDic[NSFontAttributeName]; if (!font || nil == font) { font = [UIFont fontWithName:@"HelveticaNeue" size:12.0]; [atrString addAttribute:NSFontAttributeName value:font range:range]; } return atrString; }
使用boundingRectWithSize:options:attributes:context計(jì)算
系統(tǒng)提供了- boundingRectWithSize:options:attributes:context:方法來計(jì)算NSAttributedString的size大小,- sizeWithFont:constrainedToSize:lineBreakMode:已經(jīng)被廢棄了。
/**
* return 動態(tài)返回字符串size大小
*
* @param aString 字符串
* @param width 指定寬度
* @param height 指定寬度
*
* @return CGSize
*/
+ (CGSize)getStringRect:(NSAttributedString *)aString width:(CGFloat)width height:(CGFloat)height
{
CGSize size = CGSizeZero;
NSMutableAttributedString *atrString = [[NSMutableAttributedString alloc] initWithAttributedString:aString];
NSRange range = NSMakeRange(0, atrString.length);
//獲取指定位置上的屬性信息,并返回與指定位置屬性相同并且連續(xù)的字符串的范圍信息。
NSDictionary* dic = [atrString attributesAtIndex:0 effectiveRange:&range];
//不存在段落屬性,則存入默認(rèn)值
NSMutableParagraphStyle *paragraphStyle = dic[NSParagraphStyleAttributeName];
if (!paragraphStyle || nil == paragraphStyle) {
paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
paragraphStyle.lineSpacing = 0.0;//增加行高
paragraphStyle.headIndent = 0;//頭部縮進(jìn),相當(dāng)于左padding
paragraphStyle.tailIndent = 0;//相當(dāng)于右padding
paragraphStyle.lineHeightMultiple = 0;//行間距是多少倍
paragraphStyle.alignment = NSTextAlignmentLeft;//對齊方式
paragraphStyle.firstLineHeadIndent = 0;//首行頭縮進(jìn)
paragraphStyle.paragraphSpacing = 0;//段落后面的間距
paragraphStyle.paragraphSpacingBefore = 0;//段落之前的間距
[atrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
}
//設(shè)置默認(rèn)字體屬性
UIFont *font = dic[NSFontAttributeName];
if (!font || nil == font) {
font = [UIFont fontWithName:@"HelveticaNeue" size:12.0];
[atrString addAttribute:NSFontAttributeName value:font range:range];
}
NSMutableDictionary *attDic = [NSMutableDictionary dictionaryWithDictionary:dic];
[attDic setObject:font forKey:NSFontAttributeName];
[attDic setObject:paragraphStyle forKey:NSParagraphStyleAttributeName];
CGSize strSize = [[aString string] boundingRectWithSize:CGSizeMake(width, height)
options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading
attributes:attDic
context:nil].size;
size = CGSizeMake(CGFloat_ceil(strSize.width), CGFloat_ceil(strSize.height));
return size;
}
需要注意的是調(diào)用時,要選擇NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading選項(xiàng),不然計(jì)算出來的高度不準(zhǔn)確
通過sizeToFit計(jì)算
/**
* 返回UILabel自適應(yīng)后的size
*
* @param aString 字符串
* @param width 指定寬度
* @param height 指定高度
*
* @return CGSize
*/
+ (CGSize)sizeLabelToFit:(NSAttributedString *)aString width:(CGFloat)width height:(CGFloat)height {
UILabel *tempLabel = [[UILabel alloc]initWithFrame:CGRectMake(0, 0, width, height)];
tempLabel.attributedText = aString;
tempLabel.numberOfLines = 0;
[tempLabel sizeToFit];
CGSize size = tempLabel.frame.size;
size = CGSizeMake(CGFloat_ceil(size.width), CGFloat_ceil(size.height));
return size;
}
其實(shí)就是通過新建一個臨時的UILabel,然后通過sizeToFit方法計(jì)算出合適的CGSize。
通過CTFramesetter進(jìn)行計(jì)算
-
CTFramesetter
首先來了解一下CTFramesetter與NSAttributedString的關(guān)系。CTFramesetter是CTFrame的創(chuàng)建工廠,NSAttributedString需要通過CTFrame繪制到界面上,得到CTFramesetter后,創(chuàng)建path(繪制路徑),然后得到CTFrame,最后通過CTFrameDraw方法繪制到界面上。如圖:
image
CTFramesetter關(guān)聯(lián)NSAttributedString,此時CTTypesetter實(shí)例將自動創(chuàng)建,它管理了字體。然后使用CTFramesetter 創(chuàng)建您要用于渲染文本的一個或多個幀。當(dāng)創(chuàng)建幀時,指定一個用于此幀矩形內(nèi)的子文本范圍。Core Text 為每行文本自動創(chuàng)建一個CTLine ,并在CTLine內(nèi)創(chuàng)建多個 CTRun文本分段,每個CTRun內(nèi)的文本有著同樣的格式。同時每個 CTRun 對象可以采用不同的屬性,所以你可以精確的控制字距,連字,寬度,高度等更多屬性。
-
字符(Character)和字形(Glyphs)
看一下字形圖:
image
- Bounding Box(邊界框 bbox),這是一個假想的框子,它盡可能緊密的裝入字形。
- Baseline(基線),一條假想的線,一行上的字形都以此線作為上下位置的參考,在這條線的左側(cè)存在一個點(diǎn)叫做基線的原點(diǎn)。
- Ascent(上行高度)從原點(diǎn)到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離,Ascent是一個正值。
- Descent(下行高度)從原點(diǎn)到字體中最深的字形底部的距離,Descent是一個負(fù)值(比如一個字體原點(diǎn)到最深的字形的底部的距離為2,那么Descent就為-2)。
- Linegap(行距),Linegap也可以稱作leading(其實(shí)準(zhǔn)確點(diǎn)講應(yīng)該叫做External leading),行高LineHeight則可以通過 Ascent + |Descent| + Linegap 來計(jì)算。
- Origin(每一行的原點(diǎn)),Origin是在圖中的baseLine處的。
-
計(jì)算行高
了解了以上知識點(diǎn)我們就來看一下通過CTFramesetter進(jìn)行計(jì)算行高的實(shí)現(xiàn)
方法一,將每一行CTLine的行高相加得到最終高度:CGFloat heightValue = 0; //string 為要計(jì)算高的NSAttributedString CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string); //這里的高要設(shè)置足夠大 CGFloat height = 10000; CGRect drawingRect = CGRectMake(0, 0, width, height); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, drawingRect); CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0), path, NULL); CGPathRelease(path); CFRelease(framesetter); CFArrayRef lines = CTFrameGetLines(textFrame); CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), lineOrigins); /****************** * 逐行l(wèi)ineHeight累加 ******************/ heightValue = 0; for (int i = 0; i < CFArrayGetCount(lines); i++) { CTLineRef line = CFArrayGetValueAtIndex(lines, i); CGFloat lineAscent;//上行行高 CGFloat lineDescent;//下行行高 CGFloat lineLeading;//行距 CGFloat lineHeight;//行高 //獲取每行的高度 CTLineGetTypographicBounds(line, &lineAscent, &lineDescent, &lineLeading); lineHeight = lineAscent + fabs(lineDescent) + lineLeading; heightValue = heightValue + lineHeight; } heightValue = CGFloat_ceil(heightValue);方法二,最后一行原點(diǎn)y坐標(biāo)加最后一行高度:
CGFloat heightValue = 0; //string 為要計(jì)算高的NSAttributedString CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string); //這里的高要設(shè)置足夠大 CGFloat height = 10000; CGRect drawingRect = CGRectMake(0, 0, width, height); CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, drawingRect); CTFrameRef textFrame = CTFramesetterCreateFrame(framesetter,CFRangeMake(0,0), path, NULL); CGPathRelease(path); CFRelease(framesetter); CFArrayRef lines = CTFrameGetLines(textFrame); CGPoint lineOrigins[CFArrayGetCount(lines)]; CTFrameGetLineOrigins(textFrame, CFRangeMake(0, 0), lineOrigins); /****************** * 最后一行原點(diǎn)y坐標(biāo)加最后一行下行行高跟行距 ******************/ heightValue = 0; CGFloat line_y = (CGFloat)lineOrigins[CFArrayGetCount(lines)-1].y; //最后一行l(wèi)ine的原點(diǎn)y坐標(biāo) CGFloat lastAscent = 0;//上行行高 CGFloat lastDescent = 0;//下行行高 CGFloat lastLeading = 0;//行距 CTLineRef lastLine = CFArrayGetValueAtIndex(lines, CFArrayGetCount(lines)-1); CTLineGetTypographicBounds(lastLine, &lastAscent, &lastDescent, &lastLeading); //height - line_y為除去最后一行的字符原點(diǎn)以下的高度,descent + leading為最后一行不包括上行行高的字符高度 heightValue = height - line_y + (CGFloat)(fabs(lastDescent) + lastLeading); heightValue = CGFloat_ceil(heightValue);方法三,使用CTFramesetterSuggestFrameSizeWithConstraints計(jì)算:
static inline CGSize CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(CTFramesetterRef framesetter, NSAttributedString *attributedString, CGSize size, NSUInteger numberOfLines) { CFRange rangeToSize = CFRangeMake(0, (CFIndex)[attributedString length]); CGSize constraints = CGSizeMake(size.width, 10000); if (numberOfLines == 1) { // If there is one line, the size that fits is the full width of the line constraints = CGSizeMake(10000, 10000); } else if (numberOfLines > 0) { // If the line count of the label more than 1, limit the range to size to the number of lines that have been set CGMutablePathRef path = CGPathCreateMutable(); CGPathAddRect(path, NULL, CGRectMake(0.0f, 0.0f, constraints.width, 10000)); CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL); CFArrayRef lines = CTFrameGetLines(frame); if (CFArrayGetCount(lines) > 0) { NSInteger lastVisibleLineIndex = MIN((CFIndex)numberOfLines, CFArrayGetCount(lines)) - 1; CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex); CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine); rangeToSize = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length); } CFRelease(frame); CGPathRelease(path); } CGSize suggestedSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, rangeToSize, NULL, constraints, NULL); return CGSizeMake(CGFloat_ceil(suggestedSize.width), CGFloat_ceil(suggestedSize.height)); }調(diào)用方法:
//string 為要計(jì)算高的NSAttributedString CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string); //預(yù)設(shè)size CGSize size = CGSizeMake(width, 10000); CGSize suggestedSize= CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter,string,size,1000); 寫在最后
最后說一下,經(jīng)測試發(fā)現(xiàn),以上說的三種通過CTFramesetter來計(jì)算高度的方法,都會存在誤差,表現(xiàn)為UILabel顯示時上下會有空白行,且留白范圍與所顯示內(nèi)容呈遞增關(guān)系,具體原因未知,如果有理解的歡迎指正!

