動(dòng)態(tài)計(jì)算NSAttributedString的寬高的方法

動(dòng)態(tài)計(jì)算NSAttributedString的寬高的方法

最近在復(fù)盤之前項(xiàng)目中關(guān)于文本寬高計(jì)算的實(shí)現(xiàn), 這里簡(jiǎn)單歸納總結(jié)一下.

1. boundingRectWithSize 方法

文本的寬高計(jì)算的API主要有如下方法:

// NOTE: All of the following methods will default to drawing on a baseline, limiting drawing to a single line.
// To correctly draw and size multi-line text, pass NSStringDrawingUsesLineFragmentOrigin in the options parameter.
@interface NSString (NSExtendedStringDrawing)
- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(7.0));
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary<NSAttributedStringKey, id> *)attributes context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(7.0));
@end

@interface NSAttributedString (NSExtendedStringDrawing)
- (void)drawWithRect:(CGRect)rect options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(6.0));
- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context API_AVAILABLE(macos(10.11), ios(6.0));
@end

另外關(guān)于配置options:

options表示計(jì)算的類型

NSStringDrawingUsesLineFragmentOrigin:繪制文本時(shí)使用 line fragement origin 而不是 baseline origin圃阳。一般使用這項(xiàng)限佩。
NSStringDrawingUsesFontLeading:根據(jù)字體計(jì)算高度
NSStringDrawingUsesDeviceMetrics:使用象形文字計(jì)算高度
NSStringDrawingTruncatesLastVisibleLine:如果NSStringDrawingUsesLineFragmentOrigin設(shè)置祟同,這個(gè)選項(xiàng)沒(méi)有用

官方文檔中有部分注釋:

This method draws as much of the string as it can inside the specified rectangle, wrapping the string text as needed to make it fit. If the string is too big to fit completely inside the rectangle, the method scales the font or adjusts the letter spacing to make the string fit within the given bounds.
If newline characters are present in the string, those characters are honored and cause subsequent text to be placed on the next line underneath the starting point. To correctly draw and size multi-line text, pass NSStringDrawingUsesLineFragmentOrigin in the options parameter.

因此, 是計(jì)算多行文本信息在NSStringDrawingOptions選項(xiàng), 一般需要添加如下的配置, 不然計(jì)算出來(lái)的高度不準(zhǔn)確:

NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingUsesFontLeading

另外在計(jì)算出CGSize以后, 一般得到的寬高是浮點(diǎn)數(shù), 如果需要用這個(gè)Size配置給某個(gè)View作為View的Size, 還需要通過(guò)ceil 方法向上取整得到更加準(zhǔn)確的CGSize, 也就是 ceilf(size.height)

另外計(jì)算string size 時(shí), 使用的屬性, 一定需要與View中設(shè)置文本時(shí), 使用的屬性一致!

特殊情況: 使用該方法計(jì)算完文本中中有\n或者\r\n , 會(huì)導(dǎo)致計(jì)算寬高不準(zhǔn)確, 這里貼一個(gè)網(wǎng)上針對(duì)這個(gè)問(wèn)題的解決方法(但是本人并不建議這樣做):

由于這個(gè)方法計(jì)算字符串的大小的通過(guò)取得字符串的size來(lái)計(jì)算, 如果你計(jì)算的字符串中包含`\n\r `這樣的字符, 也只會(huì)把它當(dāng)成字符來(lái)計(jì)算泞坦。但是在顯示的時(shí)候就是`\n`是轉(zhuǎn)義字符贰锁,那么顯示的計(jì)算的高度就不一樣了, 所以可以采用:

計(jì)算的高度 = boundingRectWithSize計(jì)算出來(lái)的高度 + \n\r轉(zhuǎn)義字符出現(xiàn)的個(gè)數(shù) * 單行文本的高度滤蝠。

2. 使用UILabel的sizeThatFits:方法

這里以指定的UILabel來(lái)作為富文本展示示例

    //    useBound label size: (0.0, 0.0, 88.0078125, 76.375)
    func useBound() -> UILabel {
        let label = UILabel()
        let attr: [NSAttributedString.Key : Any] = [
            .font: UIFont.systemFont(ofSize: 16),
            .foregroundColor : UIColor.red.cgColor,
        ]
        let attrStr = NSAttributedString(string: "hello world1\n\n\nhello world2", attributes: attr)
        let rect = attrStr.boundingRect(with: CGSize(width: 100, height: Int.max), options: [.usesLineFragmentOrigin, .usesFontLeading], context: nil)
        print("useBound label size: \(rect.size)")
        label.frame = CGRect(x: 100, y: 100, width: ceil(rect.width), height: ceil(rect.height));
        label.attributedText = attrStr
        label.numberOfLines = 0
        label.backgroundColor = .green
        return label
    }
    
    //   useLabel label size: (88.33333333333333, 76.66666666666667)
    func useLabel() -> UILabel {
        let label = UILabel()
        let attrStr = NSAttributedString(string: "hello world1\n\n\nhello world2", attributes: [
            .font: UIFont.systemFont(ofSize: 16),
            .foregroundColor : UIColor.blue.cgColor,
        ])
        label.attributedText = attrStr
        label.numberOfLines = 0
        let size = label.sizeThatFits(CGSize.init(width: 100, height: Int.max))
        print("useLabel label size: \(size)")
        label.frame = CGRect(x: 0, y: 100, width: size.width, height: size.height);
        label.backgroundColor = .green
        return label
    }

最后展示出來(lái)兩者大小差不多的, 但是注意第一條中使用ceil向上取整操作!

3. 使用CoreText中的CTFrame計(jì)算

網(wǎng)上關(guān)于CoreText相關(guān)信息非常多, 簡(jiǎn)單來(lái)說(shuō)就是使用iOS系統(tǒng)的排版, 然后通過(guò)排版以后結(jié)果信息來(lái)獲取系統(tǒng)排版渲染以后的結(jié)果.

CTFramesetter是CTFrame的創(chuàng)建工廠, NSAttributedString需要通過(guò)CTFrame繪制到界面上锣险,得到CTFrameSetter后芯肤,創(chuàng)建path(繪制路徑), 然后得到CTFrame, 最后通過(guò)CTFrameDraw方法繪制到界面上

CTFramesetter關(guān)聯(lián)NSAttributedString, 此時(shí)CTTypesetter實(shí)例將自動(dòng)創(chuàng)建, 它管理了字體压鉴。然后使用CTFramesetter 創(chuàng)建您要用于渲染文本的一個(gè)或多個(gè)幀油吭。當(dāng)創(chuàng)建幀時(shí), 指定一個(gè)用于此幀矩形內(nèi)的子文本范圍

每行文本會(huì)自動(dòng)創(chuàng)建成CTLine , 并在CTLine內(nèi)創(chuàng)建多個(gè) CTRun文本分段, 每個(gè)CTRun內(nèi)的文本有著同樣的格式

同時(shí)每個(gè) CTRun 對(duì)象可以采用不同的屬性上鞠,所以你可以精確的控制字距,連字世曾,寬度谴咸,高度等更多屬性

常見(jiàn)使用CTFrame計(jì)算文本高度有三種方法, 其中建議使用CTFramesetterSuggestFrameSizeWithConstraints進(jìn)行計(jì)算

3.1 獲取每條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);

3.2 最后一行原點(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);

3.3 直接使用CTFramesetterSuggestFrameSizeWithConstraints

- (CGSize)sizeThatFits:(CGSize)size {
    NSAttributedString *drawString = self.data.attributeStringToDraw;
    if (drawString == nil) {
        return CGSizeZero;
    }

    // 通過(guò)CTFrame 獲取計(jì)算的結(jié)果
    CFAttributedStringRef attributedStringRef = (__bridge CFAttributedStringRef)drawString;
    // 使用 attr Sttr 創(chuàng)建 FramesetterRef 內(nèi)容
    // 在 main thread 中運(yùn)行
    CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString(attributedStringRef);
    CFRange range = CFRangeMake(0, 0);
    // 渲染問(wèn)題
    if (_numberOfLines > 0 && framesetter) {
        CGMutablePathRef path = CGPathCreateMutable();
        CGPathAddRect(path, NULL, CGRectMake(0, 0, size.width, size.height));
        CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);
        
        // 獲取全局的 lines
        CFArrayRef lines = CTFrameGetLines(frame);
        
        // 計(jì)算行
        if (nil != lines && CFArrayGetCount(lines) > 0) {
            // 最小的展示行
            NSInteger lastVisibleLineIndex = MIN(_numberOfLines, CFArrayGetCount(lines)) - 1;
            CTLineRef lastVisibleLine = CFArrayGetValueAtIndex(lines, lastVisibleLineIndex);
            
            // 獲取最后一行可見(jiàn)行的 rangeToLayout
            CFRange rangeToLayout = CTLineGetStringRange(lastVisibleLine);
            // 搞定range ->  0 到最后一行
            range = CFRangeMake(0, rangeToLayout.location + rangeToLayout.length);
        }
        CFRelease(frame);
        CFRelease(path);
    }
    
    CFRange fitCFRange = CFRangeMake(0, 0);
    // 針對(duì) CTFramesetter 構(gòu)造一個(gè)constriants 信息
    CGSize newSize = CTFramesetterSuggestFrameSizeWithConstraints(framesetter, range, NULL, size, &fitCFRange);
    if (framesetter) {
        CFRelease(framesetter);
    }
    
    // 計(jì)算出來(lái)的 newSize
    return newSize;
}

4. 猜測(cè)一下使用UILabel的sizeThatFits的原理

網(wǎng)上也有使用intrinsicContentSize結(jié)合preferredMaxLayoutWidth計(jì)算自適應(yīng)高度的內(nèi)容

UIlabel擁有intrinsicContentSize方法調(diào)用邏輯可能如下:

- (CGSize)intrinsicContentSize {
    return [self sizeThatFits:CGSizeMake(self.bounds.size.width, MAXFLOAT)];
}

- (CGSize)sizeThatFits:(CGSize)size {
    // 通過(guò)計(jì)算attrString構(gòu)造CFTrame
    // 然后通過(guò) CTFrame相關(guān)排版服務(wù)計(jì)算寬高
    // 然后處理UILabel 的 UIRectEdge 信息
}

參考

http://www.reibang.com/p/6ed98368ceed

https://my.oschina.net/FEEDFACF/blog/1858685

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市岭佳,隨后出現(xiàn)的幾起案子血巍,更是在濱河造成了極大的恐慌,老刑警劉巖珊随,帶你破解...
    沈念sama閱讀 219,188評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件述寡,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡鲫凶,警方通過(guò)查閱死者的電腦和手機(jī)禀崖,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,464評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)螟炫,“玉大人波附,你說(shuō)我怎么就攤上這事≈缱辏” “怎么了掸屡?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,562評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)然评。 經(jīng)常有香客問(wèn)我仅财,道長(zhǎng),這世上最難降的妖魔是什么沾瓦? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,893評(píng)論 1 295
  • 正文 為了忘掉前任满着,我火速辦了婚禮,結(jié)果婚禮上贯莺,老公的妹妹穿的比我還像新娘风喇。我一直安慰自己,他們只是感情好缕探,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,917評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布魂莫。 她就那樣靜靜地躺著,像睡著了一般爹耗。 火紅的嫁衣襯著肌膚如雪耙考。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,708評(píng)論 1 305
  • 那天潭兽,我揣著相機(jī)與錄音倦始,去河邊找鬼。 笑死山卦,一個(gè)胖子當(dāng)著我的面吹牛鞋邑,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播账蓉,決...
    沈念sama閱讀 40,430評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼枚碗,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了铸本?” 一聲冷哼從身側(cè)響起肮雨,我...
    開(kāi)封第一講書(shū)人閱讀 39,342評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎箱玷,沒(méi)想到半個(gè)月后怨规,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體陌宿,經(jīng)...
    沈念sama閱讀 45,801評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,976評(píng)論 3 337
  • 正文 我和宋清朗相戀三年椅亚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了限番。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,115評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡呀舔,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出扩灯,到底是詐尸還是另有隱情媚赖,我是刑警寧澤,帶...
    沈念sama閱讀 35,804評(píng)論 5 346
  • 正文 年R本政府宣布珠插,位于F島的核電站惧磺,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏捻撑。R本人自食惡果不足惜磨隘,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,458評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望顾患。 院中可真熱鬧番捂,春花似錦、人聲如沸江解。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,008評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)犁河。三九已至鳖枕,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間桨螺,已是汗流浹背宾符。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,135評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灭翔,地道東北人魏烫。 一個(gè)月前我還...
    沈念sama閱讀 48,365評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像缠局,于是被迫代替她去往敵國(guó)和親则奥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,055評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容