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

至于这到底是因为什么,我暂时不知道为什么?有知道的铜须请私信我,谢谢啦。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,636评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,890评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,680评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,766评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,665评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,045评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,515评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,182评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,334评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,274评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,319评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,002评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,599评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,675评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,917评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,309评论 2 345
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,885评论 2 341

推荐阅读更多精彩内容

  • 项目中的要求总是多种多样的.最近公司项目有新的要求.举个例子来说,UIlabel在5s上的字体是16号,正常情况下...
    iOS_小绅士阅读 1,363评论 0 5
  • 以前总是很烦设计师非要说,让『把行距调大一点点』,因为在 iOS 这个对文字处理各种不友好的系统里,改行距并不像改...
    戴仓薯阅读 24,481评论 20 188
  • 一、 你觉得这里有某种东西,有某种东西值得去寻找。其实,在这个世界上,你很快就会明白。你同样因为失败而与世隔绝;你...
    stoner_lq阅读 563评论 0 0
  • 期盼着星星和夜幕到来的时候 待在湖边的自己望着水波的尽头 我拾起一块石子往湖里投去 和星光一起沉入水底需要多久 你...
    二明滴滴阅读 218评论 0 1
  • 凌晨三点的女人 我的意识,徘徊在酒店房间人为制造的黑暗之中,如同一条不幸被钓到的鱼一般,自我因那安眠药而沉重不堪、...
    咔辣辣阅读 254评论 0 0