iOS 行距全攻略

作者: 戴仓薯 | 来源:发表于2016-03-22 09:09 被阅读14057次

    以前总是很烦设计师非要说,让『把行距调大一点点』,因为在 iOS 这个对文字处理各种不友好的系统里,改行距并不像改字号那么简单,只调『一点点』也得多写好几行。
    不过自从我写了下面这些工具方法,调行距也就回归到它本来应该的样子:一行代码的事。

    设置行距

    UILabel+Utils.m
    - (void)setText:(NSString*)text lineSpacing:(CGFloat)lineSpacing {
        if (lineSpacing < 0.01 || !text) {
            self.text = text;
            return;
        }
        
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
        [attributedString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, [text length])];
        
        NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        [paragraphStyle setLineSpacing:lineSpacing];
        [paragraphStyle setLineBreakMode:self.lineBreakMode];
        [paragraphStyle setAlignment:self.textAlignment];
        [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [text length])];
    
        self.attributedText = attributedString;
    }
    
    使用
    [label setText:text lineSpacing:2.0f];
    
    1. 作为一个四处使用的工具方法,前面的nil检查很有必要加。因为[[NSMutableAttributedString alloc] initWithString:text] 不接受 nil 参数,会直接 crash。
    2. 生成的 paragraphStyle 除了配行距之外,还带上了 label 原有的一些常用属性。如果有其他需要,也可以加在这里。
    UITextView+Utils.m
    - (void)setText:(NSString*)text lineSpacing:(CGFloat)lineSpacing {
        if (lineSpacing < 0.01 || !text) {
            self.text = text;
            return;
        }
        
        NSMutableAttributedString *attributedString = [[NSMutableAttributedString alloc] initWithString:text];
        [attributedString addAttribute:NSFontAttributeName value:self.font range:NSMakeRange(0, [text length])];
    
        NSMutableParagraphStyle *paragraphStyle = [[NSMutableParagraphStyle alloc] init];
        [paragraphStyle setLineSpacing:lineSpacing];
        [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:NSMakeRange(0, [attributedText length])];
    
        self.attributedText = attributedString;
    }
    

    UITextView 的方法跟 UILabel 基本一样。

    使用
    [textView setText:text lineSpacing:2.0f];
    

    计算行高

    自定义行距之后,计算文本高度的方法也得相应改。很简单,只要利用 sizeToFit、sizeThatFits 之类的方法就可以了。

    UILabel+Utils.m
    + (CGFloat)text:(NSString*)text heightWithFontSize:(CGFloat)fontSize width:(CGFloat)width lineSpacing:(CGFloat)lineSpacing {
        UILabel* label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, width, MAXFLOAT)];
        label.font = [UIFont systemFontOfSize:fontSize];
        label.numberOfLines = 0;
        [label setText:text lineSpacing:lineSpacing];
        [label sizeToFit];
        return label.height;
    }
    
    UITextView+Utils.m
    + (CGFloat)text:(NSString*)text heightWithFontSize:(CGFloat)fontSize width:(CGFloat)width lineSpacing:(CGFloat)lineSpacing {
        UITextView* textView = [[UITextView alloc] initWithFrame:CGRectMake(0, 0, width, MAXFLOAT)];
        textView.font = [UIFont systemFontOfSize:fontSize];
        [textView setText:text lineSpacing:lineSpacing];
        [textView sizeToFit];
        return textView.height;
    }
    

    因为默认的 UITextView 有一点 inset,所以计算文本高度的方法要跟 UILabel 分开。

    这几个方法就能应付大多数需求了。根据自己需要,我还写了一些参数带有 numberOfLines、文本的参数为 attributedString 的变体。

    代码上的行距 vs 设计图上的行距

    如果只为贴上面几个方法,我可能也就懒得写这篇文章了。这篇文章的重点其实是分享下面这一点:代码传参数进去的行距与设计图上量出来的行距是有区别的,代码上要少几个像素,而减少的量跟字体大小有关。

    我感觉这一点有时容易被人忽视。例如一个 UILabel 字号为14,有些程序员可能就会把这个 Label 高度定为 14 像素了。而经验丰富的人就会知道不能这样,否则『h』『g』之类的字母都可能会被切掉一些。在 xib 里,选中 label 之后按『Command + =』会发现字号为 14 的 label 合适的高度应该是 17。

    为了给像『g』、『y』英文字母的尾巴留出空间,系统会给 UILabel 上的文字上下加一点默认的空白,这就是 font size 与 line height 的区别。而用代码设定paragraphStylelineSpacing,是叠加在原有空白之上的。

    别小看这点空白。如果设计师没有丧心病狂,设计出的行距往往也就是 4、5 个像素,而对 14 号字来说上下两行的空白就能占到 3 像素。如果不假思索地直接把设计图的标注传进去,结果就是行距放大到150%。视觉上出了偏差,我们也要负责任的。

    行距组成示意图

    由图所示,视觉上的行距其实由那 3 部分组成:上面一行的默认空白 + 行距 + 下面一行的默认空白。蓝色高度是我们写的 lineSpacing,而黄色和绿色加起来正好是一倍font.lineHeight - font.pointSize的值(黄色高度是上面一行的一半,为(font.lineHeight - font.pointSize) / 2,绿色是下面一行的一半)。

    简单打下 log 就可以看到这个差值大概是多少。下面列出常见的字号:

    font size font.lineHeight(近似) 差值
    10 12 2
    11 13 2
    12 14 2
    13 15.5 2.5
    14 17 3
    15 18 3
    16 19 3
    17 20 3
    18 21.5 3.5
    19 23 4
    20 24 4

    为了计算效率高,我们就不在运行时现算这个差值了;直接把设计图上量出的行距减去上面这个表里几个像素的差值,作为参数传进去即可。例如:14 号字的 label,设计图上量出的行距是 5 个像素,那就减去 3 个像素,写[label setText:text lineSpacing:2.0f];。不要忘了计算行高的时候也要用同样的参数~

    只有注意到了这些细节,才能做到『像素级的精确』,设计师们是不是都很喜欢我这样的程序员呢~:)

    相关文章

      网友评论

      • OlafChou:你这个表格没考虑到不同的字重吧,比如medium,regular,semibold???
      • 我本善良:感谢博主分享,让我们计算行高方面有了更好的准确度。现在有一个实际的问题,比如有两个lable,上面的label为lable_top字体为10,下面的label是label为lable_bottom字体为10,两者设计要求在垂直上有5pt的间距,用masonary布局的时候要写lable_bottom.top.equalto(lable_top.bottom).offset(5),但是正如文章提到,每行字的上面和下面都有一定的留白(跟字体大小相关),那么按照5pt布局的话,运行出来两个lable的垂直间距是5+留白了,那么平时布局如何能写个方法统一处理下呢?需要每次都计算出留白再用5-留白吗?
      • WELCommand:之前也遇见过。跟了offset,发现是textView的setBounds:方法产生的bug。category中重写了setBounds:直接call super就解决了.....
      • 这小子:你好,万一字符串中有\n分割段落呢?

      • e8b6cbadf7fb:楼主,不知道你有没有注意到这种方式,在只有一行文字时,也会把linespace加上,导致label的文字下方还留了一行空白,多行的时候就不会了,如果遇到了,并解决了,希望能告诉一下哦
        王洪亮ios:@意林 又看了下,还是有问题,你还有方法吗
        王洪亮ios:@意林 解决了我大问题啊
        e8b6cbadf7fb:郁闷,刚问玩,我找到答案了。。再加上一个属性
        attributedString.addAttribute(NSBaselineOffsetAttributeName, value: NSNumber(value: 0), range: NSMakeRange(0, text.characters.count))
      • TESTFIRER:解决问题了!
      • CNMD_LJ:“为了给像『g』、『y』英文字母的尾巴留出空间,系统会给 UILabel 上的文字上下加一点默认的空白”,我觉得不是这样,当你将“g y j p”和中文混合时则会显示完整,这应该和label屏幕渲染的方式有关系吧
        我本善良:的确是,字符上下的留白是为了一些英文字母。这样的留白导致我在按照UI标注,比如两个label垂直间隔要求是5,实际算出来会是5+留白这样的效果,设计会说间距比较大?这样的场景如何处理,难道需要自己每次计算留白,手动调整吗
      • 7df57db23a82:这个表格中的值,需要针对不同的字体,作出变化,比如说如果是思源字体,light的时候.
        fontSize =18对应的lingHeight为17.999999999999996
        戴仓薯:@新春_spring 哈哈,很有道理,我平常没用到别的字体~~确实有这个问题~~
      • 疯中飞舞:学习了 :smile:
      • 云逸枫林:这样临时创建一个UILabel,UITextView的做法 感觉不太合理, 比较浪费资源, 可以用一个单例的label或者textView, 或者直接用
        NSAttributedString的
        "- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options context:(nullable NSStringDrawingContext *)context NS_AVAILABLE(10_11, 6_0);
        "
        或者NSString的
        "- (CGRect)boundingRectWithSize:(CGSize)size options:(NSStringDrawingOptions)options attributes:(nullable NSDictionary<NSString *, id> *)attributes context:(nullable NSStringDrawingContext *)context NS_AVAILABLE(10_11, 7_0);
        "
        云逸枫林:@我本善良 上面也已经说过了,可以实例化一个单例的label或者textView 专用来计算,但是需要每次清空属性设置,即使这样 我也觉得比临时创建大量的label要合理。如果只是普通的计算,系统提供的两个方法足够了
        我本善良:用NSString或者NSAttributedString的方法计算的高度在字体比较大、行间距比较大、行数比较多的场景下,会比用sizeThatFits:方法返回的小很多,调用的时候,如果每次实例感觉不太好!怎么处理?
        Vine_Finer:@云逸枫林 有一定误差,比用label
      • kamous:楼主这种方法,可以解决仅作为展示控件时setText:的问题。
        但是UITextView作为输入控件动态输入文字时,行间距不生效了吧,因为没调用setText:
        使用NSLayoutManagerDelegate可解决输入文字时行间距问题
        - (CGFloat)layoutManager:(NSLayoutManager *)layoutManager lineSpacingAfterGlyphAtIndex:(NSUInteger)glyphIndex withProposedLineFragmentRect:(CGRect)rect
        戴仓薯:@kamous 嗯嗯很有道理,之前没遇到过这种需求,感谢补充:)
      • e94b5677b815:您好,我是一个有强迫症的设计湿,刚看了您的文章,觉得您是一个很有耐心的开发,也很喜欢钻研,但是文章里“行距组成示意图”好像是有点问题的,iOS系统上行高与行高之间是没有蓝色色块部分的,android上才会有!这些数据我都有,但是始终没有找到一个既让开发设计师省事,又能保证还原度的方案,你提出的直接减掉的方案对你们不是太友好,增加了写代码时的计算工作量,所以有空能一块研究一下么?我微信号是qingmei,期待您……
        kamous:@oqingmei 作者其实已经提出解决方法,不用按照那个表来每次计算减去指定像素了的。
        CGFloat space = self.lineSpace - (self.font.lineHeight - self.font.pointSize)
        开发只用直接把间距设置成space的值就行,lineSpace就是视觉稿上的标注。
        (使用autolayout,按这种方法仔细测了还是会有2个像素左右差距)
      • 托尼的夏天:感谢 解决了我的问题!!!
      • 框框的世界:很棒。
      • 16b7178e2388:呀,又看到仓鼠妹了
        戴仓薯:@Richard_Gao 哈哈~ 那你就知道我是女生了?~
        16b7178e2388:@戴仓薯 以前在segment fault 上跟你说过话~
        戴仓薯:@Richard_Gao 你是谁呀~~
      • 0eaeb79804e9:"例如:14 号字的 label,设计图上量出的行距是 5 个像素,那就减去 3 个像素"
        里面的像素应该换成point吧?
        戴仓薯:@Tiger_zh_ 是的,哈哈~ 习惯说像素了~其实应该是point
      • 罗火火:经验丰富啊

      本文标题:iOS 行距全攻略

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