iOS之UILabel行高行距知多少

設計圖的Label和iOS中的Lable

skech中打開設計師畫的Lable,按照標注寫好后,對比總是發現和設計稿有差異,每次設計師說,讓『把行距調大一點點』加xx幾個像素等等之類,都特么頭疼,因為在 iOS 這個對文字處理各種不友好的系統里,改行距并不像改字號那么簡單,只調『一點點』未必像我們想的那樣,改一個你想當然數字。

文字在iOS中應用最廣的就是UILabel。UILabel里文字的高度并不是UILabel本身的高度。例如一個 UILabel 字號為14,有些程序員可能就會把這個 Label 高度定為 14 像素了。而經驗豐富的人就會知道不能這樣,否則『g』之類的字母都可能會被切掉一些。在 xib 里,選中 label 之后按『Command + =』會發現字號為 14 的 label 合適的高度應該是 比14大。那我們怎么友好的將設計圖上的標注準確的應用到iOS的系統中呢?

字體的高度(lineHeight)

一個字形由很多參數構成。字形的各個參數,如下面的兩張圖



邊框(Bounding Box):一個假想的邊框,盡可能地容納整個字形。
基線(Baseline):一條假想的參照線,以此為基礎進行字形的渲染。一般來說是一條橫線。
基礎原點(Origin):基線上最左側的點。
行間距(Leading):行與行之間的間距。
字間距(Kerning):字與字之間的距離,為了排版的美觀,并不是所有的字形之間的距離都是一致的,但是這個基本步影響到我們的文字排版。
上行高度(Ascent)和下行高度(Decent):一個字形最高點和最低點到基線的距離,前者為正數,而后者為負數。當同一行內有不同字體的文字時,就取最大值作為相應的值。如下圖:

紅框高度既為當前行的行高,綠線為baseline,綠色到紅框上部分為當前行的最大Ascent,綠線到黃線為當前行的最大Desent,而黃框的高即為行間距。

由此可以得出:lineHeight = Ascent + |Decent| + Leading。lineHeight就是我們字體的真是高度,不同的字體庫lineHeight會有差異。
pointSize就是我們字體的字號。顯而易見個 lineHeight > pointSize。所以一個單行的14號字體的高度并不是pointSize的14,而是lineHeight。如果我們直接設置空間label高度14,有些字就會被截斷。

這些字形參數iOS系統的UIFont都有對應的屬性,如下:

@property(nonatomic,readonly,strong) NSString *familyName;
@property(nonatomic,readonly,strong) NSString *fontName;
@property(nonatomic,readonly)        CGFloat   pointSize;
@property(nonatomic,readonly)        CGFloat   ascender;
@property(nonatomic,readonly)        CGFloat   descender;
@property(nonatomic,readonly)        CGFloat   capHeight;
@property(nonatomic,readonly)        CGFloat   xHeight;
@property(nonatomic,readonly)        CGFloat   lineHeight NS_AVAILABLE_IOS(4_0);
@property(nonatomic,readonly)        CGFloat   leading;

這里的lineHeight就是這個字體單行的高度,我們可以用這個值來判斷文本是單行還是多行。這里提供一個計算文本高度的方法:

@implementation NSString (Size)
- (CGSize)sizeWithFont:(UIFont *)font boundSize:(CGSize)size lineSpacing:(CGFloat)lineSpacing{
    NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc]init];
    paragraphStyle.lineSpacing = lineSpacing;
    //設置換行模式為單詞模式
    paragraphStyle.lineBreakMode = NSLineBreakByWordWrapping;
    NSDictionary *attributes = @{NSFontAttributeName:font, NSParagraphStyleAttributeName:paragraphStyle};
    CGSize resultSize = [self boundingRectWithSize:size options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine attributes:attributes context:nil].size;
    return CGSizeMake(ceil(resultSize.width), ceil(resultSize.height));
}
@end

字體行間距

當字體多行的時候,為了美觀,設計師會寫上行間距,使用attributedString的時候我們可以設置行間距。那么行間距在視圖上到底是那一塊呢?如下圖所示:


行間距示意圖

由圖所示,視覺上的行距其實由那 3 部分組成:上面一行的默認空白 + 行距 + 下面一行的默認空白。綠色高度是我們寫的 lineSpacing,而兩段紅色加起來正好是一倍font.lineHeight - font.pointSize的值。

@implementation NSAttributedString (Size)
+ (NSMutableAttributedString *)attributedStringWithString:(NSString *)string  font:(UIFont *)font color:(UIColor *)color lineSpacing:(CGFloat)spacing {  
 if (!font) {
        NSAssert(0, @"請傳遞一個正常的字體參數");
    }
    
    if (!color) {
        NSAssert(0, @"請傳遞一個正常的字體參數");
    }
    
    if (![string isNonEmpty]) {
        return [[NSMutableAttributedString alloc] initWithString:@"" attributes:nil];;
    }
    NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
    style.lineSpacing = spacing;
    style.lineBreakMode = NSLineBreakByWordWrapping;
    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:@{NSFontAttributeName:font, NSForegroundColorAttributeName:color, NSParagraphStyleAttributeName:style}];
    return [attributedString mutableCopy];
}
@end

- (BOOL)isNonEmpty {
    
    NSMutableCharacterSet *emptyStringSet = [[NSMutableCharacterSet alloc] init];
    [emptyStringSet formUnionWithCharacterSet: [NSCharacterSet whitespaceAndNewlineCharacterSet]];
    [emptyStringSet formUnionWithCharacterSet: [NSCharacterSet characterSetWithCharactersInString: @" "]];
    if ([self length] == 0) {
        return NO;
    }
    NSString* str = [self stringByTrimmingCharactersInSet:emptyStringSet];
    return [str length] > 0;
}

同樣這里我提供了一個可以傳入行間距合成attributedString的方法,前面的nil檢查很有必要加。因為[[NSMutableAttributedString alloc] initWithString:text] 不接受 nil 參數,會直接 crash。isNonEmpty是一個用來檢測字符串是否為空的方法,具體實現如下。通過一個NSString得到NSMutableAttributedString后,直接賦值給UILable的attributedString屬性。這樣我們就可以方便的設置文本的間距了,是不是覺得很完美。

受蘋果歧視的中文行間距

我們先看一段的代碼,如下:

    NSString * text1 = @"hhh小書包!";//@"hello world!";
    UILabel *label1 = [[UILabel alloc]initWithFrame:CGRectMake(50, 100, 150, 0)];
    label1.text = text1;
    label1.font = font;
    label1.numberOfLines = 0;
    label1.backgroundColor = UIColor.redColor;
    [self.view addSubview:label1];
    [label1 sizeToFit];
    NSLog(@"label1.heihgt = %2f",label1.bounds.size.height);
    NSLog(@"label1.lineHeight = %2f",label1.font.lineHeight);
    
    
    NSString * text2 = @"hhh小書包!小書包!小書包!小書包!小書包!小書包!小書包!小書包!";//@"hello world!";
    UILabel *label2 = [[UILabel alloc]initWithFrame:CGRectMake(200, 100, 150, 0)];
    label2.text = text2;
    label2.font = font;
    label2.numberOfLines = 0;
    label2.backgroundColor = UIColor.redColor;
    [self.view addSubview:label2];
    [label2 sizeToFit];
    NSLog(@"label1.heihgt = %2f",label1.bounds.size.height);
    NSLog(@"label1.lineHeight = %2f",label1.font.lineHeight);
    
    NSString * text3 = @"hhh小書包!";//@"hello world!";
    UILabel *label3 = [[UILabel alloc]initWithFrame:CGRectMake(50, 250, 150, 0)];
    NSAttributedString * attriString3 = [NSAttributedString attributedStringWithString:text3 font:font color:UIColor.blackColor lineSpacing:5];
    label3.attributedText = attriString3;
    label3.numberOfLines = 0;
    label3.backgroundColor = UIColor.redColor;
    [self.view addSubview:label3];
    [label3 sizeToFit];
    NSLog(@"label2.heihgt = %2f",label2.bounds.size.height);
    NSLog(@"label2.lineHeight = %2f",label2.font.lineHeight);
    
    
    NSString * text4 = @"hhh小書包!小書包!小書包!";//@"hello world!";
    UILabel *label4 = [[UILabel alloc]initWithFrame:CGRectMake(200, 250, 150, 0)];
    NSAttributedString * attriString4 = [NSAttributedString attributedStringWithString:text4 font:font color:UIColor.blackColor lineSpacing:5];
    label4.attributedText = attriString4;
    label4.numberOfLines = 0;
    label4.backgroundColor = UIColor.redColor;
    [self.view addSubview:label4];
    [label4 sizeToFit];
    
    NSLog(@"label3.heihgt = %2f",label3.bounds.size.height);
    NSLog(@"label3.lineHeight = %2f",label3.font.lineHeight);

很簡單對不對,但是接下來看看我們屏幕上的UI顯示:
中英混合內容

中英文混合

上面label中的內容是中英文混合,那如果文本內容是純中文或者純英文呢(其他國家文字暫不考慮),我們改下文字內容看看最后效果,效果圖分別如下:

純英文內容

純英文

純中文內容

純中文

從圖中可以看出很明顯感覺到蘋果對中文深深的惡意!歸納下來就是這兩點:

  • 當內容是英文的時候,無論單行還是多行,使用attributedString行高都不會出現問題。
  • 當內容中包含的中文的時候,單行的attributedString會自動加上一個多余的行間距,多行則顯示正常。

所以我們在使用attributedString就得單獨處理一行的情況,去掉這個多余的行間距linespace。我們在NSAttributedString +Size 加一個類別方法

+ (NSMutableAttributedString *)attributedStringWithString:(NSString *)string  font:(UIFont *)font color:(UIColor *)color maxWidth:(CGFloat) maxWidth lineSpacing:(CGFloat)spacing {
    if (!font) {
        NSAssert(0, @"請傳遞一個正常的字體參數");
    }
    
    if (!color) {
        NSAssert(0, @"請傳遞一個正常的字體參數");
    }
    
    if (![string isNonEmpty]) {
        return [[NSMutableAttributedString alloc] initWithString:@"" attributes:nil];;
    }
    NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init];
    style.lineBreakMode = NSLineBreakByWordWrapping;
    NSDictionary * attributes = @{NSFontAttributeName:font, NSForegroundColorAttributeName:color, NSParagraphStyleAttributeName:style};
    CGFloat contentHeight = [string boundingRectWithSize:CGSizeMake(maxWidth, MAXFLOAT) options:NSStringDrawingUsesLineFragmentOrigin | NSStringDrawingTruncatesLastVisibleLine attributes:attributes context:nil].size.height;
    if (contentHeight > font.lineHeight){
        style.lineSpacing = spacing;
    } else {
        //單行的時候去掉行間距
        style.lineSpacing = 0;
    }
    NSAttributedString *attributedString = [[NSAttributedString alloc] initWithString:string attributes:attributes];
    return [attributedString mutableCopy];
}

思考:
我們定義UILable使用sizeToFit自動撐開的高度為labelHeight,
lableHeight 和 lineHeight ,lineSpace應該有如下關系:
lableHeight = lineHeight * numberOfLines + lineSpace * (numberOfLines - 1)
但是經過測試log打印結果發現
lableHeight > lineHeight * numberOfLines + lineSpace * (numberOfLines - 1)
舉個栗子:

    UIFont *font = [UIFont systemFontOfSize:24];
    NSString * text4 = @"hello world!hello world!hello world!";//@"hello world!";
    UILabel *label4 = [[UILabel alloc]initWithFrame:CGRectMake(200, 250, 150, 0)];
    NSAttributedString * attriString4 = [NSAttributedString attributedStringWithString:text4 font:font color:UIColor.blackColor maxWidth:150 lineSpacing:5];
    label4.attributedText = attriString4;
    label4.adjustsFontSizeToFitWidth = YES;
    label4.minimumScaleFactor = 0.5;
    label4.numberOfLines = 0;
    label4.backgroundColor = UIColor.redColor;
    [self.view addSubview:label4];
    [label4 sizeToFit];

    NSLog(@"label4.heihgt1 = %2f",label4.bounds.size.height);
    //文本有三行
    NSLog(@"label4.height2 = %2f",label4.font.lineHeight * 3 + 5 * 2);

iPhone8模擬器下log打印結果

2017-12-18 18:07:13.621879+0800 Color[18188:1273525] label4.heihgt1 = 96.000000
2017-12-18 18:07:13.621999+0800 Color[18188:1273525] label4.height2 = 95.921875

至于這到底是因為什么,我暫時不知道為什么?有知道的銅須請私信我,謝謝啦。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • 項目中的要求總是多種多樣的.最近公司項目有新的要求.舉個例子來說,UIlabel在5s上的字體是16號,正常情況下...
    iOS_小紳士閱讀 1,425評論 0 5
  • 以前總是很煩設計師非要說,讓『把行距調大一點點』,因為在 iOS 這個對文字處理各種不友好的系統里,改行距并不像改...
    戴倉薯閱讀 24,584評論 20 188
  • 一、 你覺得這里有某種東西,有某種東西值得去尋找。其實,在這個世界上,你很快就會明白。你同樣因為失敗而與世隔絕;你...
    stoner_lq閱讀 607評論 0 0
  • 期盼著星星和夜幕到來的時候 待在湖邊的自己望著水波的盡頭 我拾起一塊石子往湖里投去 和星光一起沉入水底需要多久 你...
    二明滴滴閱讀 235評論 0 1
  • 凌晨三點的女人 我的意識,徘徊在酒店房間人為制造的黑暗之中,如同一條不幸被釣到的魚一般,自我因那安眠藥而沉重不堪、...
    咔辣辣閱讀 278評論 0 0