美文网首页iOS 开发iOS 技术iOS开发实用技术
动态计算NSAttributedString的size大小

动态计算NSAttributedString的size大小

作者: lele8446 | 来源:发表于2016-04-20 14:58 被阅读9187次

NSAttributedString(富文本),作为NSString的子类,是一种带有属性的字符串,通过它可以轻松的在一个字符串中表现出多种字体、字号、背景色、下划线等各不相同的风格,还可以对段落进行格式化。下面就来探讨一下动态计算NSAttributedString的size大小实现:


  • 首先提供一个对NSAttributedString进行封装的函数

    该方法会为NSAttributedString添加默认段落属性以及字体属性(如果不存在的话)

    /**
     *  return 返回封装后的NSMutableAttributedString,添加了默认NSParagraphStyleAttributeName与NSFontAttributeName属性
     *
     *  @param labelStr  NSString
     *  @param labelDic  属性字典
        @{
           NSFontAttributeName://(字体)
           NSBackgroundColorAttributeName://(字体背景色)
           NSForegroundColorAttributeName://(字体颜色)
           NSParagraphStyleAttributeName://(段落)
           NSLigatureAttributeName://(连字符)
           NSKernAttributeName://(字间距)
           NSStrikethroughStyleAttributeName://NSUnderlinePatternSolid(实线) | 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;//头部缩进,相当于左padding
           paragraphStyle.tailIndent = 0;//相当于右padding
           paragraphStyle.lineHeightMultiple = 0;//行间距是多少倍
           paragraphStyle.alignment = NSTextAlignmentLeft;//对齐方式
           paragraphStyle.firstLineHeadIndent = 0;//首行头缩进
           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计算

系统提供了- boundingRectWithSize:options:attributes:context:方法来计算NSAttributedString的size大小,- sizeWithFont:constrainedToSize:lineBreakMode:已经被废弃了。

  /**
   *  return 动态返回字符串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);

     //获取指定位置上的属性信息,并返回与指定位置属性相同并且连续的字符串的范围信息。
     NSDictionary* dic = [atrString attributesAtIndex:0 effectiveRange:&range];
     //不存在段落属性,则存入默认值
     NSMutableParagraphStyle *paragraphStyle = dic[NSParagraphStyleAttributeName];
     if (!paragraphStyle || nil == paragraphStyle) {
          paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
          paragraphStyle.lineSpacing = 0.0;//增加行高
          paragraphStyle.headIndent = 0;//头部缩进,相当于左padding
          paragraphStyle.tailIndent = 0;//相当于右padding
          paragraphStyle.lineHeightMultiple = 0;//行间距是多少倍
          paragraphStyle.alignment = NSTextAlignmentLeft;//对齐方式
          paragraphStyle.firstLineHeadIndent = 0;//首行头缩进
          paragraphStyle.paragraphSpacing = 0;//段落后面的间距
          paragraphStyle.paragraphSpacingBefore = 0;//段落之前的间距
          [atrString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range];
      }

     //设置默认字体属性
     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;
  }

需要注意的是调用时,要选择NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading选项,不然计算出来的高度不准确

通过sizeToFit计算
  /**
   *  返回UILabel自适应后的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;
  }

其实就是通过新建一个临时的UILabel,然后通过sizeToFit方法计算出合适的CGSize。

通过CTFramesetter进行计算
  • CTFramesetter
    首先来了解一下CTFramesetter与NSAttributedString的关系。CTFramesetter是CTFrame的创建工厂,NSAttributedString需要通过CTFrame绘制到界面上,得到CTFramesetter后,创建path(绘制路径),然后得到CTFrame,最后通过CTFrameDraw方法绘制到界面上。如图:


    image

    CTFramesetter关联NSAttributedString,此时CTTypesetter实例将自动创建,它管理了字体。然后使用CTFramesetter 创建您要用于渲染文本的一个或多个帧。当创建帧时,指定一个用于此帧矩形内的子文本范围。Core Text 为每行文本自动创建一个CTLine ,并在CTLine内创建多个 CTRun文本分段,每个CTRun内的文本有着同样的格式。同时每个 CTRun 对象可以采用不同的属性,所以你可以精确的控制字距,连字,宽度,高度等更多属性。

  • 字符(Character)和字形(Glyphs)
    看一下字形图:


    image
  1. Bounding Box(边界框 bbox),这是一个假想的框子,它尽可能紧密的装入字形。
  2. Baseline(基线),一条假想的线,一行上的字形都以此线作为上下位置的参考,在这条线的左侧存在一个点叫做基线的原点。
  3. Ascent(上行高度)从原点到字体中最高(这里的高深都是以基线为参照线的)的字形的顶部的距离,Ascent是一个正值。
  4. Descent(下行高度)从原点到字体中最深的字形底部的距离,Descent是一个负值(比如一个字体原点到最深的字形的底部的距离为2,那么Descent就为-2)。
  5. Linegap(行距),Linegap也可以称作leading(其实准确点讲应该叫做External leading),行高LineHeight则可以通过 Ascent + |Descent| + Linegap 来计算。
  6. Origin(每一行的原点),Origin是在图中的baseLine处的。
  • 计算行高
    了解了以上知识点我们就来看一下通过CTFramesetter进行计算行高的实现
    方法一,将每一行CTLine的行高相加得到最终高度:

    CGFloat heightValue = 0;
    //string 为要计算高的NSAttributedString
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string);
      
    //这里的高要设置足够大
    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);
    
    /******************
     * 逐行lineHeight累加
     ******************/
    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);
    

    方法二,最后一行原点y坐标加最后一行高度:

    CGFloat heightValue = 0;
    //string 为要计算高的NSAttributedString
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string);
      
    //这里的高要设置足够大
    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);
    /******************
     * 最后一行原点y坐标加最后一行下行行高跟行距
     ******************/
    heightValue = 0;
    CGFloat line_y = (CGFloat)lineOrigins[CFArrayGetCount(lines)-1].y;  //最后一行line的原点y坐标
    CGFloat lastAscent = 0;//上行行高
    CGFloat lastDescent = 0;//下行行高
    CGFloat lastLeading = 0;//行距
    CTLineRef lastLine = CFArrayGetValueAtIndex(lines, CFArrayGetCount(lines)-1);
    CTLineGetTypographicBounds(lastLine, &lastAscent, &lastDescent, &lastLeading);
    //height - line_y为除去最后一行的字符原点以下的高度,descent + leading为最后一行不包括上行行高的字符高度
    heightValue = height - line_y + (CGFloat)(fabs(lastDescent) + lastLeading);
    heightValue = CGFloat_ceil(heightValue);
    

    方法三,使用CTFramesetterSuggestFrameSizeWithConstraints计算:

    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));
    }
    

    调用方法:

    //string 为要计算高的NSAttributedString
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((__bridge CFAttributedStringRef)string);
    //预设size
    CGSize size = CGSizeMake(width, 10000);
    CGSize suggestedSize= CTFramesetterSuggestFrameSizeForAttributedStringWithConstraints(framesetter,string,size,1000);
    
  • 写在最后
    最后说一下,经测试发现,以上说的三种通过CTFramesetter来计算高度的方法,都会存在误差,表现为UILabel显示时上下会有空白行,且留白范围与所显示内容呈递增关系,具体原因未知,如果有理解的欢迎指正!

相关文章

网友评论

  • 爱的播放:推荐一款写富文本非常好用的控件:NudeIn,你完全不用去学习这些蛋疼的原生写法,可以像masonry那样写富文本控件
    github地址:https://github.com/hon-key/Nudeln
  • 田小北北:CGFloat_ceil ? 请问是哪个框架里边的
    田小北北:@lele8446 好的,谢啦
    lele8446:这是自定义函数,我漏写了。完整定义如下:
    static inline CGFLOAT_TYPE CGFloat_ceil(CGFLOAT_TYPE cgfloat) {
    #if CGFLOAT_IS_DOUBLE
    return ceil(cgfloat);
    #else
    return ceilf(cgfloat);
    #endif
    }
  • 就是一个春天的花朵:经过测试,前两个方法算的高度不准确,如果富文本字符串里面含有图片的话,是不会算进去的,还不如用NSAttributedString自带的计算高度的方法,很准确
    就是很随意哦:@春风沉醉的晚上_ :+1:
    就是一个春天的花朵:@就是很随意哦 - (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context NS_AVAILABLE(10_11, 6_0);

    这个方法,直接用NSAttributedString对象调用
    就是很随意哦:你好 富文本带图片的话,该怎么算高度呢?
  • 2018火:mark

本文标题:动态计算NSAttributedString的size大小

本文链接:https://www.haomeiwen.com/subject/vgxzlttx.html