How to adjust font size of label to fit the rectangle?

前端 未结 14 577
独厮守ぢ
独厮守ぢ 2020-11-28 07:00

Yeah, there\'s this cool myLabel.adjustsFontSizeToFitWidth = YES; property. But as soon as the label has two lines or more, it won\'t resize the text to anythin

相关标签:
14条回答
  • 2020-11-28 07:00

    Here's Swift version according to @NielsCastle answer, using binary search

    extension UILabel{
    
        func adjustFontSizeToFitRect(rect : CGRect){
    
            if text == nil{
                return
            }
    
            frame = rect
    
            let maxFontSize: CGFloat = 100.0
            let minFontSize: CGFloat = 5.0
    
            var q = Int(maxFontSize)
            var p = Int(minFontSize)
    
            let constraintSize = CGSize(width: rect.width, height: CGFloat.max)
    
            while(p <= q){
                let currentSize = (p + q) / 2
                font = font.fontWithSize( CGFloat(currentSize) )
                let text = NSAttributedString(string: self.text!, attributes: [NSFontAttributeName:font])
                let textRect = text.boundingRectWithSize(constraintSize, options: .UsesLineFragmentOrigin, context: nil)
    
                let labelSize = textRect.size
    
                if labelSize.height < frame.height && labelSize.height >= frame.height-10 && labelSize.width < frame.width && labelSize.width >= frame.width-10 {
                    break
                }else if labelSize.height > frame.height || labelSize.width > frame.width{
                    q = currentSize - 1
                }else{
                    p = currentSize + 1
                }
            }
    
        }
    }
    

    Usage

    label.adjustFontSizeToFitRect(rect)
    

    often just

    label.adjustFontSizeToFitRect(rect.frame)
    
    0 讨论(0)
  • 2020-11-28 07:00

    All binary searches are good, but stop recursion using frame checks not so logically. More nicely check font size, cause UIFont supports float size and this font is more suitable. Plus using label paragraph style to calculate size morу exactly.

    If someone interesting, you can look bellow code:

    static UIFont * ___suitableFontInRangePrivate(const CGSize labelSize,
                                                NSParagraphStyle * paragraphStyle,
                                                NSString * fontName,
                                                NSString * text,
                                                const CGFloat minSize,
                                                const CGFloat maxSize)
    {
        // Font size in range, middle size between max & min.
        const CGFloat currentSize = minSize + ((maxSize - minSize) / 2);
    
        // Font with middle size.
        UIFont * currentFont = [UIFont fontWithName:fontName size:currentSize];
    
        // Calculate text height.
        const CGFloat textHeight = [text boundingRectWithSize:CGSizeMake(labelSize.width, CGFLOAT_MAX)
                                                    options:NSStringDrawingUsesLineFragmentOrigin
                                                attributes:@{ NSFontAttributeName : currentFont, NSParagraphStyleAttributeName : paragraphStyle }
                                                    context:nil].size.height;
        CGFloat min, max;
        if (textHeight > labelSize.height)
        {
            // Take left range part.
            min = minSize;
            max = currentSize;
        }
        else
        {
            // Take right range part.
            min = currentSize;
            max = maxSize;
        }
    
        // If font size in int range [0.0; 2.0] - got it, othervice continue search.
        return ((max - min) <= 2.0) ? currentFont : ___suitableFontInRangePrivate(labelSize, paragraphStyle, fontName, text, min, max);
    }
    
    void UILabelAdjustsFontSizeToFrame(UILabel * label)
    {
        if (!label) return;
    
        NSString * text = [label text];
    
        __block NSParagraphStyle * style = nil;
        [[label attributedText] enumerateAttributesInRange:NSMakeRange(0, [text length])
                                                options:(NSAttributedStringEnumerationOptions)0
                                                usingBlock:^(NSDictionary *attrs, NSRange range, BOOL *stop){
                                                    id paragraphStyle = [attrs objectForKey:@"NSParagraphStyle"];
                                                    if (paragraphStyle) style = [paragraphStyle retain];
                                                }];
    
        if (!style)
        {
            NSMutableParagraphStyle * paragraphStyle = [[NSParagraphStyle defaultParagraphStyle] mutableCopy];
            if (!paragraphStyle) paragraphStyle = [[NSMutableParagraphStyle alloc] init];
            if (paragraphStyle)
            {
                [paragraphStyle setLineBreakMode:[label lineBreakMode]];
                [paragraphStyle setAlignment:[label textAlignment]];
            }
            style = paragraphStyle;
        }
    
        UIFont * suitableFont = ___suitableFontInRangePrivate([label frame].size, style, [[label font] fontName], text, 0, 500);
        [label setFont:suitableFont];
        [style release];
    }
    
    0 讨论(0)
  • 2020-11-28 07:01

    This solution (based on this answer) works with auto-layout and performs a binary search to find the best font size.

    The only caveat I have found is that you can't specify the number of lines (because AFAIK you can't tell boundingRectWithSize how many lines you want).

    AdjustableLabel.h

    #import <UIKit/UIKit.h>
    
    @interface AdjustableLabel : UILabel
    /** 
      If set to YES, font size will be automatically adjusted to frame.
      Note: numberOfLines can't be specified so it will be set to 0.
    */
    @property(nonatomic) BOOL adjustsFontSizeToFitFrame;
    @end
    

    AdjustableLabel.m

    #import "AdjustableLabel.h"
    
    @interface AdjustableLabel ()
    @property(nonatomic) BOOL fontSizeAdjusted;
    @end
    
    // The size found S satisfies: S fits in the frame and and S+DELTA doesn't.
    #define DELTA 0.5
    
    @implementation AdjustableLabel
    
    - (void)setAdjustsFontSizeToFitFrame:(BOOL)adjustsFontSizeToFitFrame
    {
        _adjustsFontSizeToFitFrame = adjustsFontSizeToFitFrame;
    
        if (adjustsFontSizeToFitFrame) {
            self.numberOfLines = 0; // because boundingRectWithSize works like this was 0 anyway
        }
    }
    
    - (void)layoutSubviews
    {
        [super layoutSubviews];
    
        if (self.adjustsFontSizeToFitFrame && !self.fontSizeAdjusted)
        {
            self.fontSizeAdjusted = YES; // to avoid recursion, because adjustFontSizeToFrame will trigger this method again
    
            [self adjustFontSizeToFrame];
        }
    }
    
    - (void) adjustFontSizeToFrame
    {
        UILabel* label = self;
    
        if (label.text.length == 0) return;
    
        // Necessary or single-char texts won't be correctly adjusted
        BOOL checkWidth = label.text.length == 1;
    
        CGSize labelSize = label.frame.size;
    
        // Fit label width-wise
        CGSize constraintSize = CGSizeMake(checkWidth ? MAXFLOAT : labelSize.width, MAXFLOAT);
    
        // Try all font sizes from largest to smallest font size
        CGFloat maxFontSize = 300;
        CGFloat minFontSize = 5;
    
        NSString* text = label.text;
        UIFont* font = label.font;
    
        while (true)
        {
            // Binary search between min and max
            CGFloat fontSize = (maxFontSize + minFontSize) / 2;
    
            // Exit if approached minFontSize enough
            if (fontSize - minFontSize < DELTA/2) {
                font = [UIFont fontWithName:font.fontName size:minFontSize];
                break; // Exit because we reached the biggest font size that fits
            } else {
                font = [UIFont fontWithName:font.fontName size:fontSize];
            }
    
            // Find label size for current font size
            CGRect rect = [text boundingRectWithSize:constraintSize
                                             options:NSStringDrawingUsesLineFragmentOrigin
                                          attributes:@{NSFontAttributeName : font}
                                             context:nil];
    
            // Now we discard a half
            if( rect.size.height <= labelSize.height && (!checkWidth || rect.size.width <= labelSize.width) ) {
                minFontSize = fontSize; // the best size is in the bigger half
            } else {
                maxFontSize = fontSize; // the best size is in the smaller half
            }
        }
    
        label.font = font;
    }
    
    @end
    

    Usage

    AdjustableLabel* label = [[AdjustableLabel alloc] init];
    label.adjustsFontSizeToFitFrame = YES;
    
    // In case you change the font, the size you set doesn't matter
    label.font = [UIFont fontWithName:@"OpenSans-Light" size:20];
    
    0 讨论(0)
  • 2020-11-28 07:05

    If you want to make sure the label fits in the rectangle both width and height wise you can try different font size on the label to see if one will fit.

    This snippet starts at 300 pt and tries to fit the label in the targeted rectangle by reducing the font size.

    - (void) sizeLabel: (UILabel *) label toRect: (CGRect) labelRect {
    
        // Set the frame of the label to the targeted rectangle
        label.frame = labelRect;
    
        // Try all font sizes from largest to smallest font size
        int fontSize = 300;
        int minFontSize = 5;
    
        // Fit label width wize
        CGSize constraintSize = CGSizeMake(label.frame.size.width, MAXFLOAT);
    
        do {
            // Set current font size
            label.font = [UIFont fontWithName:label.font.fontName size:fontSize];
    
            // Find label size for current font size
            CGRect textRect = [[label text] boundingRectWithSize:constraintSize
                                                         options:NSStringDrawingUsesLineFragmentOrigin
                                                      attributes:@{NSFontAttributeName: label.font}
                                                         context:nil];
    
            CGSize labelSize = textRect.size;
    
            // Done, if created label is within target size
            if( labelSize.height <= label.frame.size.height )
                break;
    
            // Decrease the font size and try again
            fontSize -= 2;
    
        } while (fontSize > minFontSize);
    }
    

    I think the above explains what goes on. A faster implementation could use caching and argarcians binary search as follows

    + (CGFloat) fontSizeForString: (NSString*) s inRect: (CGRect) labelRect  {
        // Cache repeat queries
        static NSMutableDictionary* mutableDict = nil;
        static dispatch_once_t onceToken;
        dispatch_once(&onceToken, ^{
            mutableDict = [NSMutableDictionary dictionary];
        });
    
        NSString* key = [NSString stringWithFormat:@"%@_%d_%d", s, (int) labelRect.size.width, (int) labelRect.size.height];
        NSNumber* value = [mutableDict objectForKey:key];
        if (value)
            return value.doubleValue;
    
        // Set the frame of the label to the targeted rectangle
        UILabel* label = [[UILabel alloc] init];
        label.text = s;
        label.frame = labelRect;
    
        // Hopefully between 5 and 300
        CGFloat theSize = (CGFloat) [self binarySearchForFontSizeForLabel:label withMinFontSize:5 withMaxFontSize:300 withSize:label.frame.size];
        [mutableDict setObject:@(theSize) forKey:key];
        return  theSize;
    }
    
    
    + (NSInteger)binarySearchForFontSizeForLabel:(UILabel *)label withMinFontSize:(NSInteger)minFontSize withMaxFontSize:(NSInteger)maxFontSize withSize:(CGSize)size {
        // If the sizes are incorrect, return 0, or error, or an assertion.
        if (maxFontSize < minFontSize) {
            return maxFontSize;
        }
    
        // Find the middle
        NSInteger fontSize = (minFontSize + maxFontSize) / 2;
        // Create the font
        UIFont *font = [UIFont fontWithName:label.font.fontName size:fontSize];
        // Create a constraint size with max height
        CGSize constraintSize = CGSizeMake(size.width, MAXFLOAT);
        // Find label size for current font size
        CGRect rect = [label.text boundingRectWithSize:constraintSize
                                               options:NSStringDrawingUsesLineFragmentOrigin
                                            attributes:@{NSFontAttributeName : font}
                                               context:nil];
        CGSize labelSize = rect.size;
    
        // EDIT:  The next block is modified from the original answer posted in SO to consider the width in the decision. This works much better for certain labels that are too thin and were giving bad results.
        if (labelSize.height >= (size.height + 10) && labelSize.width >= (size.width + 10) && labelSize.height <= (size.height) && labelSize.width <= (size.width)) {
            return fontSize;
        } else if (labelSize.height > size.height || labelSize.width > size.width) {
            return [self binarySearchForFontSizeForLabel:label withMinFontSize:minFontSize withMaxFontSize:fontSize - 1 withSize:size];
        } else {
            return [self binarySearchForFontSizeForLabel:label withMinFontSize:fontSize + 1 withMaxFontSize:maxFontSize withSize:size];
        }
    }
    
    0 讨论(0)
  • 2020-11-28 07:07

    Also set myLabel.numberOfLines = 10 or to whatever the max number of lines you want.

    0 讨论(0)
  • 2020-11-28 07:07

    All these are interesting solutions to the original problem, however all of them are also missing an important thing: If you solely rely on the familyName to get the next font to test, you're losing the weight information and possibly more advanced attributes like small caps, figure style, etc.

    A better approach is instead of passing the font name around and doing [UIFont fontWithName:someFontName size:someFontSize], passing UIFontDescriptor objects along and then doing [UIFont fontWithDescriptor:someFontDescriptor size:someFontSize].

    0 讨论(0)
提交回复
热议问题