iOS文本尺寸自適應異步計算實現

目前市面上的非UI線程文本算高方法或多或少都有一些問題。本文經過逆向和分析UILabel的sizeThatFits方法實現來獲得一個最佳的文本算高的精簡方法。方法能夠運行在任意線程,所以能夠有效的應用在那些異步算高或者要求尺寸進行提早計算的場景中。git

從iOS官方的實現中能夠看出文本算高會考慮簡單文本字符串、屬性字符串、字體大小、最大顯示行數numberOfLines、段落信息、 段落的對齊方式、斷字方式、段落的首行縮進、陰影偏移等等因素。下面就是具體的實現代碼:github

/// 使用此方法時請標明源做者:歐陽大哥2013。本方法符合MIT協議規範。
/// github地址:https://github.com/youngsoft
/// 計算簡單文本或者屬性字符串的自適應尺寸
/// @param fitsSize 指定限制的尺寸,參考UILabel中的sizeThatFits中的參數的意義。
/// @param text 要計算的簡單文本NSString或者屬性字符串NSAttributedString對象
/// @param numberOfLines 指定最大顯示的行數,若是爲0則表示不限制最大行數
/// @param font 指定計算時文本的字體,能夠爲nil表示使用UILabel控件的默認17號字體
/// @param textAlignment 指定文本對齊方式默認是NSTextAlignmentNatural
/// @param lineBreakMode 指定多行時斷字模式,默承認以用UILabel的默認斷字模式NSLineBreakByTruncatingTail
/// @param minimumScaleFactor 指定文本的最小縮放因子,默認填寫0。這個參數用於那些定寬時能夠自動縮小文字字體來自適應顯示的場景。
/// @param shadowOffset 指定陰影的偏移位置,須要注意的是這個偏移位置是同時指定了陰影顏色和偏移位置纔有效。若是不考慮陰影則請傳遞CGSizeZero,不然陰影會參與尺寸計算。
/// @return 返回自適應的最合適尺寸
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;

    //若是不指定字體則用默認的字體。
    if (font == nil) {
        font = [UIFont systemFontOfSize:17];
    }
    
    CGFloat systemVersion = [UIDevice currentDevice].systemVersion.floatValue;
        
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
    paragraphStyle.alignment = textAlignment;
    paragraphStyle.lineBreakMode = lineBreakMode;
    //系統大於等於11才設置行斷字策略。
    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;
        //對於屬性字符串老是加上默認的字體和段落信息。
        NSMutableAttributedString *mutableCalcAttributedString = [[NSMutableAttributedString alloc] initWithString:originAttributedString.string attributes:@{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle}];
        
        //再附加上原來的屬性。
        [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];
        }];
        
        //這裏再次取段落信息,由於有可能屬性字符串中就已經包含了段落信息。
        if (systemVersion >= 11.0) {
            NSParagraphStyle *alternativeParagraphStyle = [mutableCalcAttributedString attribute:NSParagraphStyleAttributeName atIndex:0 effectiveRange:NULL];
            if (alternativeParagraphStyle != nil) {
                paragraphStyle = (NSMutableParagraphStyle*)alternativeParagraphStyle;
            }
        }
        
        calcAttributedString = mutableCalcAttributedString;
    }
    
    //調整fitsSize的值, 這裏的寬度調整爲只要寬度小於等於0或者顯示一行都不限制寬度,而高度則老是改成不限制高度。
    fitsSize.height = FLT_MAX;
    if (fitsSize.width <= 0 || numberOfLines == 1) {
        fitsSize.width = FLT_MAX;
    }
        
    //構造出一個NSStringDrawContext
    NSStringDrawingContext *context = [[NSStringDrawingContext alloc] init];
    context.minimumScaleFactor = minimumScaleFactor;
    @try {
        //由於下面幾個屬性都是未公開的屬性,因此咱們用KVC的方式來實現。
        [context setValue:@(numberOfLines) forKey:@"maximumNumberOfLines"];
        if (numberOfLines != 1) {
            [context setValue:@(YES) forKey:@"wrapsForTruncationMode"];
        }
        [context setValue:@(YES) forKey:@"wantsNumberOfLineFragments"];
    } @catch (NSException *exception) {}
       

    //計算屬性字符串的bounds值。
    CGRect rect = [calcAttributedString boundingRectWithSize:fitsSize options:NSStringDrawingUsesLineFragmentOrigin context:context];
    
    //須要對段落的首行縮進進行特殊處理!
    //若是隻有一行則直接添加首行縮進的值,不然進行特殊處理。。
    CGFloat firstLineHeadIndent = paragraphStyle.firstLineHeadIndent;
    if (firstLineHeadIndent != 0.0 && systemVersion >= 11.0) {
        //獲得繪製出來的行數
        NSInteger numberOfDrawingLines = [[context valueForKey:@"numberOfLineFragments"] integerValue];
        if (numberOfDrawingLines == 1) {
            rect.size.width += firstLineHeadIndent;
        } else {
            //取內容的行數。
            NSString *string = calcAttributedString.string;
            NSCharacterSet *charset = [NSCharacterSet newlineCharacterSet];
            NSArray *lines = [string componentsSeparatedByCharactersInSet:charset]; //獲得文本內容的行數
            NSString *lastLine = lines.lastObject;
            NSInteger numberOfContentLines = lines.count - (NSInteger)(lastLine.length == 0);  //有效的內容行數要減去最後一行爲空行的狀況。
            if (numberOfLines == 0) {
                numberOfLines = NSIntegerMax;
            }
            if (numberOfLines > numberOfContentLines)
                numberOfLines = numberOfContentLines;
            
            //只有繪製的行數和指定的行數相等時才添加上首行縮進!這段代碼根據反彙編來實現,可是不理解爲何相等才設置?
            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);
       
    //轉化爲能夠有效顯示的邏輯點, 這裏將原始邏輯點乘以縮放比例獲得物理像素點,而後再取整,而後再除以縮放比例獲得能夠有效顯示的邏輯點。
    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;
}

//上述方法的精簡版本
NS_INLINE CGSize calcTextSizeV2(CGSize fitsSize, id text, NSInteger numberOfLines, UIFont *font) {
    return calcTextSize(fitsSize, text, numberOfLines, font, NSTextAlignmentNatural, NSLineBreakByTruncatingTail,0.0, CGSizeZero);
}
複製代碼

下面是具體的驗證測試用例(用例在iOS9到iOS13上運行經過):bash

CFTimeInterval simpleTextUILabelInterval = 0;
    CFTimeInterval simpleTextNOUILabelInterval = 0;
    CFTimeInterval attributedTextUILabelInterval = 0;
    CFTimeInterval attributedTextNOUILabelInterval = 0;
    NSArray *testStringArray = @[@"您",@"好",@"中",@"國",@"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++) {
        //隨機生成0到100個字符。
        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);
        
        //測試簡單文本。
        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), @"");
        
        //測試富文本
        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(@"簡單文本計算UILabel總耗時(毫秒):%.3f, 平均耗時:%.3f",simpleTextUILabelInterval, simpleTextUILabelInterval / 5000);
    NSLog(@"簡單文本計算非UILabel總耗時(毫秒):%.3f, 平均耗時:%.3f",simpleTextNOUILabelInterval, simpleTextNOUILabelInterval / 5000);
    NSLog(@"富文本計算UILabel總耗時(毫秒):%.3f, 平均耗時:%.3f",attributedTextUILabelInterval, attributedTextUILabelInterval / 5000);
    NSLog(@"富文本計算非UILabel總耗時(毫秒):%.3f, 平均耗時:%.3f",attributedTextNOUILabelInterval, attributedTextNOUILabelInterval / 5000);
        
複製代碼

關注: 歐陽大哥2013簡書|歐陽大哥2013掘金|歐陽大哥2013Githubapp

相關文章
相關標籤/搜索