動態(tài)計(jì)算NSAttributedString的size大小

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
  1. Bounding Box(邊界框 bbox),這是一個假想的框子,它盡可能緊密的裝入字形。
  2. Baseline(基線),一條假想的線,一行上的字形都以此線作為上下位置的參考,在這條線的左側(cè)存在一個點(diǎn)叫做基線的原點(diǎn)。
  3. Ascent(上行高度)從原點(diǎn)到字體中最高(這里的高深都是以基線為參照線的)的字形的頂部的距離,Ascent是一個正值。
  4. Descent(下行高度)從原點(diǎn)到字體中最深的字形底部的距離,Descent是一個負(fù)值(比如一個字體原點(diǎn)到最深的字形的底部的距離為2,那么Descent就為-2)。
  5. Linegap(行距),Linegap也可以稱作leading(其實(shí)準(zhǔn)確點(diǎn)講應(yīng)該叫做External leading),行高LineHeight則可以通過 Ascent + |Descent| + Linegap 來計(jì)算。
  6. 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)系,具體原因未知,如果有理解的歡迎指正!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1.iOS中的round、ceil、floor函數(shù)略解 round如果參數(shù)是小數(shù),則求本身的四舍五入.ceil如果...
    K_Gopher閱讀 1,264評論 1 0
  • CoreText是iOS/OSX中文本顯示的一個底層框架,它是用C語言寫成的,有快速簡單的優(yōu)勢。iOS中的Text...
    小貓仔閱讀 5,119評論 2 9
  • 本文所涉及的代碼你可以在這里下載到https://github.com/kejinlu/CTTest,包含兩個項(xiàng)目...
    eb99d15a673d閱讀 1,331評論 0 6
  • 最近在做實(shí)時聊天,出現(xiàn)了滾動tableview卡頓問題,經(jīng)過研究發(fā)現(xiàn)是因?yàn)閳D片太多造城的,于是試著用coretex...
    南楓小謹(jǐn)閱讀 1,648評論 3 11
  • 1獲取系統(tǒng)語言設(shè)置 NSUserDefaults *userDefault = [NSUserDefaults s...
    仉隳閱讀 846評論 0 0

友情鏈接更多精彩內(nèi)容