带有adjustsFontSizeToFitWidth的多行UILabel

Posted

技术标签:

【中文标题】带有adjustsFontSizeToFitWidth的多行UILabel【英文标题】:Multiline UILabel with adjustsFontSizeToFitWidth 【发布时间】:2010-12-08 00:04:20 【问题描述】:

我有一个多行 UILabel,我想根据文本长度调整其字体大小。整个文本应该适合标签的框架而不截断它。

不幸的是,根据文档,adjustsFontSizeToFitWidth 属性“仅在 numberOfLines 属性设置为 1 时有效”。

我尝试使用来确定调整后的字体大小

-[NSString (CGSize)sizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size lineBreakMode:(UILineBreakMode)lineBreakMode]

然后减小字体大小直到合适为止。不幸的是,此方法在内部截断文本以适应指定的大小并返回生成的截断字符串的大小。

【问题讨论】:

【参考方案1】:

在this question 中,0x90 提供了一个解决方案——虽然有点难看——可以满足我的需求。具体来说,它正确地处理了单个单词不适合初始字体大小的宽度的情况。我稍微修改了代码,使其作为NSString 上的一个类别:

- (CGFloat)fontSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size 
    CGFloat fontSize = [font pointSize];
    CGFloat height = [self sizeWithFont:font constrainedToSize:CGSizeMake(size.width,FLT_MAX) lineBreakMode:UILineBreakModeWordWrap].height;
    UIFont *newFont = font;

    //Reduce font size while too large, break if no height (empty string)
    while (height > size.height && height != 0)    
        fontSize--;  
        newFont = [UIFont fontWithName:font.fontName size:fontSize];   
        height = [self sizeWithFont:newFont constrainedToSize:CGSizeMake(size.width,FLT_MAX) lineBreakMode:UILineBreakModeWordWrap].height;
    ;

    // Loop through words in string and resize to fit
    for (NSString *word in [self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]) 
        CGFloat width = [word sizeWithFont:newFont].width;
        while (width > size.width && width != 0) 
            fontSize--;
            newFont = [UIFont fontWithName:font.fontName size:fontSize];   
            width = [word sizeWithFont:newFont].width;
        
    
    return fontSize;

将其与UILabel 一起使用:

    CGFloat fontSize = [label.text fontSizeWithFont:[UIFont boldSystemFontOfSize:15] constrainedToSize:label.frame.size];
    label.font = [UIFont boldSystemFontOfSize:fontSize];

编辑:修复了使用font 初始化newFont 的代码。修复了某些情况下的崩溃问题。

【讨论】:

我发现使用 [self componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]] 而不是仅仅按空格分割更准确。 这很棒。为方便起见,我在UILabel 添加了对最小字体大小和类别的支持,并为感兴趣的人添加了uploaded it as a gist。 好方法。您可以考虑添加换行模式作为另一个参数。 @ObjectiveFlash,当然可能,虽然不是最好的风格。通常你会把它放在一个额外的 NSString+Additions.h/.m 文件中。然后你可以在任何你需要的地方导入它。 我尝试将其转换为对 ios 7 友好,但我无法获得相同的结果。 iOS 7 有什么类似的方法吗?【参考方案2】:

在某些情况下,如果您知道想要多少行(例如“2”),将“换行符”从“Word Wrap”更改为“Truncate Tail”可能就足够了:Credit:Becky Hansmeyer

【讨论】:

【参考方案3】:

要获得完全有效的解决方案,请参阅我的答案底部?

要手动测量 UILabeltext / attributedText 的尺寸,以便使用您自己的策略找到合适的字体大小,您有以下几种选择:

    使用NSStringsize(withAttributes:)NSAttributedStringsize() 函数。这些只是部分有用,因为它们假设文本是一行。

    使用NSAttributedStringboundingRect() 函数,它需要一些绘图选项,确保提供.usesLineFragmentOrigin 以支持多行:

    var textToMeasure = label.attributedText
    
    // Modify the font size in `textToMeasure` as necessary
    
    // Now measure
    let rect = textToMeasure.boundingRect(with: label.bounds, options: [. usesLineFragmentOrigin], context: nil)
    

    使用 TextKit 和你自己的 NSLayoutManager:

    var textToMeasure = label.attributedText
    
    // Modify the font size in `textToMeasure` as necessary
    
    // Now measure
    let layoutManager = NSLayoutManager()
    let textContainer = NSTextContainer(size: CGSize(width: label.bounds.width, height: .greatestFiniteMagnitude))
    let textStorage = NSTextStorage(attributedString: string)
    textStorage.addLayoutManager(layoutManager)
    layoutManager.addTextContainer(textContainer)
    let glyphRange = layoutManager.glyphRange(for: textContainer)
    let rect = layoutManager.boundingRect(forGlyphRange: glyphRange, in: textContainer)
    

    使用 CoreText,这是一种用于布局文本的功能更强大的低级 API。对于这项任务,这可能会变得不必要地复杂。

无论您选择使用什么来测量文本,您都可能需要执行两遍:第一遍是考虑不应分成多行的长词,您需要在其中找到最大的完全适合标签范围内的最大(~最长)单词的字体大小。在第二遍中,您可以从第一遍的结果继续向下搜索,以找到适合本次整个文本所需的更小的字体大小。

在进行最大字测量(而不是整个文本)时,您不希望将您提供的宽度参数限制为上述某些尺寸功能,否则系统将别无选择,只能分解您给它的单个单词并为您的目的返回不正确的结果。您需要将上述方法的宽度参数替换为CGFloat.greatestFiniteMagnitude

boundingRect() 的 size 参数的宽度部分。 NSTextContainer() 的 size 参数的宽度部分。

您还需要考虑用于搜索的实际算法,因为测量文本是一项昂贵的操作,而线性搜索可能会太慢,具体取决于您的需要。如果您想提高效率,可以改为应用二进制搜索。 ?

有关基于上述内容的强大工作解决方案,请参阅我的开源框架AccessibilityKit。 AKLabel 的几个例子:

希望这会有所帮助!

【讨论】:

【参考方案4】:

谢谢,我做了这个自定义 UILabel 的其他人的更多信息,这将尊重最小字体大小,并且有一个额外的选项可以将文本对齐到顶部。

h:

@interface EPCLabel : UILabel 
    float originalPointSize;
    CGSize originalSize;


@property (nonatomic, readwrite) BOOL alignTextOnTop;
@end

米:

#import "EPCLabel.h"

@implementation EPCLabel
@synthesize alignTextOnTop;

-(void)verticalAlignTop 
    CGSize maximumSize = originalSize;
    NSString *dateString = self.text;
    UIFont *dateFont = self.font;
    CGSize dateStringSize = [dateString sizeWithFont:dateFont 
                                   constrainedToSize:CGSizeMake(self.frame.size.width, maximumSize.height)
                                       lineBreakMode:self.lineBreakMode];

    CGRect dateFrame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, dateStringSize.height);

    self.frame = dateFrame;


- (CGFloat)fontSizeWithFont:(UIFont *)font constrainedToSize:(CGSize)size 
    CGFloat fontSize = [font pointSize];
    CGFloat height = [self.text sizeWithFont:font             
                           constrainedToSize:CGSizeMake(size.width,FLT_MAX)  
                               lineBreakMode:UILineBreakModeWordWrap].height;
    UIFont *newFont = font;

    //Reduce font size while too large, break if no height (empty string)
    while (height > size.height && height != 0 && fontSize > self.minimumFontSize)  
        fontSize--;  
        newFont = [UIFont fontWithName:font.fontName size:fontSize];   
        height = [self.text sizeWithFont:newFont  
                       constrainedToSize:CGSizeMake(size.width,FLT_MAX) 
                           lineBreakMode:UILineBreakModeWordWrap].height;
    ;

    // Loop through words in string and resize to fit
    if (fontSize > self.minimumFontSize) 
        for (NSString *word in [self.text componentsSeparatedByString:@" "]) 
            CGFloat width = [word sizeWithFont:newFont].width;
            while (width > size.width && width != 0 && fontSize > self.minimumFontSize) 
                fontSize--;
                newFont = [UIFont fontWithName:font.fontName size:fontSize];   
                width = [word sizeWithFont:newFont].width;
            
        
    
    return fontSize;


-(void)setText:(NSString *)text 
    [super setText:text];
    if (originalSize.height == 0) 
        originalPointSize = self.font.pointSize;
        originalSize = self.frame.size;
    

    if (self.adjustsFontSizeToFitWidth && self.numberOfLines > 1) 
        UIFont *origFont = [UIFont fontWithName:self.font.fontName size:originalPointSize];
        self.font = [UIFont fontWithName:origFont.fontName size:[self fontSizeWithFont:origFont constrainedToSize:originalSize]];
    

    if (self.alignTextOnTop) [self verticalAlignTop];


-(void)setAlignTextOnTop:(BOOL)flag 
    alignTextOnTop = YES;
    if (alignTextOnTop && self.text != nil)
        [self verticalAlignTop];


@end

希望对你有帮助。

【讨论】:

但是,使用所有这些方法,如果您的文本在起始(现在是最大)字体大小处不适合指定的行数,则 constrainedToSize 调用会根据您的行愉快地为您截断文本打破,以适应宽度。因此,您最终只会缩小以适应剩下的内容,而不是我认为您真正想要的内容,即“缩小以使所有这些文本适合 n 行”【参考方案5】:

cmets 中提供了一个 ObjC 扩展,用于计算将多行文本放入 UILabel 所需的字体大小。 它是用 Swift 重写的(从 2016 年开始):

//
//  NSString+KBAdditions.swift
//
//  Created by Alexander Mayatsky on 16/03/16.
//
//  Original code from http://***.com/a/4383281/463892 & http://***.com/a/18951386
//

import Foundation
import UIKit

protocol NSStringKBAdditions 
    func fontSizeWithFont(font: UIFont, constrainedToSize size: CGSize, minimumScaleFactor: CGFloat) -> CGFloat


extension NSString : NSStringKBAdditions 
    func fontSizeWithFont(font: UIFont, constrainedToSize size: CGSize, minimumScaleFactor: CGFloat) -> CGFloat 
        var fontSize = font.pointSize
        let minimumFontSize = fontSize * minimumScaleFactor


        var attributedText = NSAttributedString(string: self as String, attributes:[NSFontAttributeName: font])
        var height = attributedText.boundingRectWithSize(CGSize(width: size.width, height: CGFloat.max), options:NSStringDrawingOptions.UsesLineFragmentOrigin, context:nil).size.height

        var newFont = font
        //Reduce font size while too large, break if no height (empty string)
        while (height > size.height && height != 0 && fontSize > minimumFontSize) 
            fontSize--;
            newFont = UIFont(name: font.fontName, size: fontSize)!

            attributedText = NSAttributedString(string: self as String, attributes:[NSFontAttributeName: newFont])
            height = attributedText.boundingRectWithSize(CGSize(width: size.width, height: CGFloat.max), options:NSStringDrawingOptions.UsesLineFragmentOrigin, context:nil).size.height
        

        // Loop through words in string and resize to fit
        for word in self.componentsSeparatedByCharactersInSet(NSCharacterSet.whitespaceAndNewlineCharacterSet()) 
            var width = word.sizeWithAttributes([NSFontAttributeName:newFont]).width
            while (width > size.width && width != 0 && fontSize > minimumFontSize) 
                fontSize--
                newFont = UIFont(name: font.fontName, size: fontSize)!
                width = word.sizeWithAttributes([NSFontAttributeName:newFont]).width
            
        
        return fontSize;
    

完整代码链接:https://gist.github.com/amayatsky/e6125a2288cc2e4f1bbf

【讨论】:

【参考方案6】:

斯威夫特 4.2:

//
//  String+Utility.swift
//
//  Created by Philip Engberg on 29/11/2018.
//  Original code from http://***.com/a/4383281/463892 & http://***.com/a/18951386
//

import Foundation
import UIKit

extension String 
    func fontSize(with font: UIFont, constrainedTo size: CGSize, minimumScaleFactor: CGFloat) -> CGFloat 
        var fontSize = font.pointSize
        let minimumFontSize = fontSize * minimumScaleFactor

        var attributedText = NSAttributedString(string: self, attributes: [.font: font])
        var height = attributedText.boundingRect(with: CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], context: nil).size.height

        var newFont = font
        //Reduce font size while too large, break if no height (empty string)
        while height > size.height && height != 0 && fontSize > minimumFontSize 
            fontSize -= 1
            newFont = UIFont(name: font.fontName, size: fontSize)!

            attributedText = NSAttributedString(string: self, attributes: [.font: newFont])
            height = attributedText.boundingRect(with: CGSize(width: size.width, height: CGFloat.greatestFiniteMagnitude), options: [.usesLineFragmentOrigin], context: nil).size.height
        

        // Loop through words in string and resize to fit
        for word in self.components(separatedBy: NSCharacterSet.whitespacesAndNewlines) 
            var width = word.size(withAttributes: [.font: newFont]).width
            while width > size.width && width != 0 && fontSize > minimumFontSize 
                fontSize -= 1
                newFont = UIFont(name: font.fontName, size: fontSize)!
                width = word.size(withAttributes: [.font: newFont]).width
            
        
        return fontSize
    

【讨论】:

【参考方案7】:

在我看来,这个问题现在有了更好的答案

我发现当您设置 adjustsFontSizeToFitWidth true、numberOfLines 0、minimumScaleFactor 小到足以让字体根据需要缩小以及默认 lineBreakMode (byTruncatingTail) 时,字体大小会在多行标签上自动调整

PS:我在官方文档中找不到有关此更改的任何信息。我创建了一个问题以了解有关此主题的更多信息here

【讨论】:

【参考方案8】:

我想我会添加我自己的答案:

我认为这比其他一些答案略有改进,因为:

    在遍历单词时进行缓存以按宽度查找最长的单词,以防止不必要的计算 在进行尺寸计算时使用所有标签属性

https://gist.github.com/chrisjrex/c571056a4b621f7099bcbd5e179f184f

注意:仅当 UILabel 具有 .numberOfLines = 0.lineBreakMode = .byWordWrapping 时才应使用此解决方案

否则你最好使用标准 swift 库中的.adjustFontSizeForWidth = true

这个帖子上的其他答案对我提出解决方案非常有帮助,谢谢

【讨论】:

以上是关于带有adjustsFontSizeToFitWidth的多行UILabel的主要内容,如果未能解决你的问题,请参考以下文章

带有和不带有聚合的 sql 查询

如何翻转正面带有标签而背面带有另一个标签的视图 - 参见图片

CakePHP 如何处理带有/不带有 'id' 字段的 HABTM 表?

带有滚动的 Div 和带有绝对位置的内容

带有 RecyclerView 的 DialogFragment 比带有 Recyclerview 的 Fragment 慢

访问控制允许带有和不带有 www 的来源