简体   繁体   中英

Expanding Multiline UILabel

In my iPhone app, I have a multiline label that I would like to expand/contract with a "More" button. Like this:

Lorem ipsum dolor sit amet, consectetur 
adipiscing elit. Donec fringilla, turpis 
in porttitor imperdiet, eros turpis...

                                "<More>"

Should animate into this:

Lorem ipsum dolor sit amet, consectetur 
adipiscing elit. Donec fringilla, turpis 
in porttitor imperdiet, eros turpis laoreet 
magna, id tempor ante lorem pulvinar lacus.
Duis vitae nisl quis sapien dictum pellentesque.

                                "<Less>"

I am trying to get an effect where every line is revealed individually as the label grows, and then individually hidden as it shrinks. Growing works great, but it jumps to 3 lines during the shrink animation. Any ideas? Code and properties below:


Grow animation:

[UIView animateWithDuration:0.5 animations:^{
        view.frame = CGRectMake(startFrame.origin.x, startFrame.origin.y, startFrame.size.width, startFrame.size.height + 40.0);
    }];

Shrink animation:

[UIView animateWithDuration:0.5 animations:^{
        view.frame = CGRectMake(startFrame.origin.x, startFrame.origin.y, startFrame.size.width, startFrame.size.height - 40.0);
    }];

UILabel properties:

  • Lines: 0
  • Line Breaks: Truncate Tail
  • Content Mode: Top

You can animate this by using autolayout constraints and modifying the numberOfLines from 3 to 0. (0 is a special value that means show any number of lines).

The label has an intrinsic size that will change when you modify the numberOfLines and those will effect the constraints. The code looks like this:

@IBAction func buttonTapped(sender: UIButton) {
  let numberOfLines = label.numberOfLines == 0 ? 3 : 0
  label.numberOfLines = numberOfLines
  let newTitle = numberOfLines == 0 ? "Less" : "More"    
  sender.setTitle(newTitle, forState: .Normal)
  UIView.animateWithDuration(0.5) { self.view.layoutIfNeeded() }
}

You need to tell the view containing the label and the button that it needs to layout in the animation block.

在此输入图像描述

Animating a UILabel had a similar result to what you described, only the shrinking animation worked and the growing one jumped to the full size.

I experimented also with a UITextView, but that didn't work either.

My solution was to take another UIView, named coverView, and put it under the UILabel. When the UILabel is expanded it is resized without animating it and the coverView hides the expanded part, then the coverView's origin.y and size.height is modified in an animation to reveal the expanded UILabel. When the UILabel is contracted, the coverView's origin.y and size.height is modified in an animation to gradually hide the UILabel. In the animation's completion block the UILabel's frame is resized to the contracted dimensions.

I created a custom UIView for this: AnimatedLabel. You can find the complete code below. I wrote it quickly and didn't comment it, but it shouldn't be hard to understand.

How to use it (works with IB, too):

// Init
AnimatedLabel *animLabel = [[AnimatedLabel alloc] initWithFrame:/*desired CGRect*/];

// Set the surroundingBackgroundColor, this color will be used for the coverView
// you should set it to the container view's backgroundColor
// IMPORTANT: transparency is not supported! (if its transparent you'll see the UILabel)
animLabel.surroundingBackgroundColor = self.view.backgroundColor;

// Set the text
animLabel.label.text = /*text text text ...*/;

// Call sizeToFit to modify the label's size to fit the text perfectly
// so that the text is not moved after the label is expanded (UILabel always
// centers the text vertically and if the label
// is not resized you will see that the text is repositioned after the animation)
[animLabel sizeToFit];

// Open the label
[animLabel open];

// Close the label
[animLabel close];

// You can resize the animLabel and the subviews will be correctly resized
animLabel.frame = /*CGRect*/;

AnimatedLabel.h

//
//  AnimatedLabel.h
//  labeltest
//
//  Created by Alpar Szotyori on 20/06/2012.
//  Use it, modify it, improve it, share it!
//

#import <UIKit/UIKit.h>

@interface AnimatedLabel : UIView

@property (nonatomic, strong) UILabel *label;
@property (nonatomic, strong) UIColor *surroundingBackgroundColor;
@property (nonatomic) BOOL isOpen;

// Public methods

- (void)open;
- (void)close;

@end

AnimatedLabel.m:

//
//  AnimatedLabel.m
//  labeltest
//
//  Created by Alpar Szotyori on 20/06/2012.
//  Use it, modify it, improve it, share it!
//

#import "AnimatedLabel.h"

@interface NSString (visibleText)

- (NSString*)stringVisibleInRect:(CGRect)rect withFont:(UIFont*)font constrainedToSize:(CGSize)size;

@end

@implementation NSString (visibleText)

- (NSString*)stringVisibleInRect:(CGRect)rect withFont:(UIFont*)font constrainedToSize:(CGSize)size
{
    BOOL addEllipse = NO;
    NSString *visibleString = @"";
    for (int i = 2; i <= self.length; i++)
    {
        NSString *testString = [self substringToIndex:i];
        CGSize stringSize = [testString sizeWithFont:font constrainedToSize:size];
        if (stringSize.height > rect.size.height || stringSize.width > rect.size.width) {
            addEllipse = YES;
            break;
        }

        visibleString = testString;
    }

    if (addEllipse) {
        if (visibleString.length >= 3) {
            visibleString = [[visibleString substringToIndex:visibleString.length - 3] stringByAppendingString:@"…"];
        }
    }

    return visibleString;
}

@end


@interface AnimatedLabel()

@property (nonatomic, strong) UIView *coverView;
@property (nonatomic, strong) UIColor *backgrndColor;
@property (nonatomic) CGFloat origLabelHeight;
@property (nonatomic, strong) NSString *origLabelText;
@property (nonatomic) BOOL animating;
@property (nonatomic) BOOL labelKVOSet;

- (void)changeHeight:(CGFloat)height animated:(BOOL)animated;
- (void)addKVOToLabel;
- (void)removeKVOFromLabel;

@end


@implementation AnimatedLabel

#pragma mark - Properties

@synthesize label;
@synthesize surroundingBackgroundColor = i_surroundingBackgroundColor;
@synthesize isOpen;

- (void)setSurroundingBackgroundColor:(UIColor *)surroundingBackgroundColor {
    i_surroundingBackgroundColor = surroundingBackgroundColor;
    self.coverView.backgroundColor = i_surroundingBackgroundColor;
}

#pragma mark - Initialization

- (id)initWithCoder:(NSCoder *)aDecoder {
    self = [super initWithCoder:aDecoder];
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height)];
        self.label.numberOfLines = 0;
        self.label.backgroundColor = self.backgrndColor;
        self.origLabelHeight = self.label.frame.size.height;
        [self addKVOToLabel];

        self.coverView = [[UIView alloc] initWithFrame:CGRectZero];
        self.coverView.backgroundColor = [UIColor whiteColor];

        [self addSubview:self.label];
        [self addSubview:self.coverView];

        self.animating = NO;
        self.isOpen = NO;
    }
    return self;
}

- (id)initWithFrame:(CGRect)frame
{
    self = [super initWithFrame:frame];
    if (self) {
        self.label = [[UILabel alloc] initWithFrame:CGRectMake(0.0, 0.0, frame.size.width, frame.size.height)];
        self.label.numberOfLines = 0;
        self.label.backgroundColor = self.backgrndColor;
        self.origLabelHeight = self.label.frame.size.height;
        [self addKVOToLabel];

        self.coverView = [[UIView alloc] initWithFrame:CGRectZero];
        self.coverView.backgroundColor = [UIColor whiteColor];

        [self addSubview:self.label];
        [self addSubview:self.coverView];

        self.animating = NO;
        self.isOpen = NO;
    }
    return self;
}

#pragma mark - Overriden methods

- (void)setFrame:(CGRect)frame {
    if (!self.animating && self.isOpen) {
        self.isOpen = NO;
        [self changeHeight:self.origLabelHeight animated:NO];
        frame.size.height = self.origLabelHeight;
    }

    [super setFrame:frame];

    if (!self.animating) {        
        self.label.frame= CGRectMake(0.0, 0.0, frame.size.width, frame.size.height);
        self.origLabelHeight = self.label.frame.size.height;

        [self removeKVOFromLabel];

        NSString *visibleText = [self.origLabelText stringVisibleInRect:self.label.frame withFont:self.label.font constrainedToSize:CGSizeMake(frame.size.width, LONG_MAX)];
        self.label.text = visibleText;
        [self.label sizeToFit];

        [self addKVOToLabel];
    }
}

- (void)setBackgroundColor:(UIColor *)backgroundColor {
    [super setBackgroundColor:[UIColor clearColor]];

    self.backgrndColor = backgroundColor;

    self.label.backgroundColor = self.backgrndColor;
}

- (UIColor *)backgroundColor {
    return self.backgrndColor;
}

- (void)sizeToFit {
    [self removeKVOFromLabel];

    NSString *visibleText = [self.origLabelText stringVisibleInRect:self.label.frame withFont:self.label.font constrainedToSize:CGSizeMake(self.frame.size.width, LONG_MAX)];
    self.label.text = visibleText;
    [self.label sizeToFit];
    self.origLabelHeight = self.label.frame.size.height;

    [self addKVOToLabel];

    self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.label.frame.size.width, self.label.frame.size.height);
}

#pragma mark - KVO methods

- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
     if ([keyPath isEqualToString:@"text"]) {
         self.origLabelText = [change objectForKey:NSKeyValueChangeNewKey];
     }
}

#pragma mark - Public methods

- (void)open {
    self.origLabelHeight = self.label.frame.size.height;
    [self removeKVOFromLabel];
    self.label.text = self.origLabelText;
    [self addKVOToLabel];
    [self.label sizeToFit];

    if (self.origLabelHeight == self.label.frame.size.height) {
        return;
    }

    [self changeHeight:self.label.frame.size.height animated:YES];
    self.isOpen = YES;
}

- (void)close {
    if (self.frame.size.height == self.origLabelHeight) {
        return;
    }

    [self changeHeight:self.origLabelHeight animated:YES];
    self.isOpen = NO;
}

#pragma mark - Private methods

#pragma mark - Properties

@synthesize coverView, backgrndColor, origLabelHeight, origLabelText, animating, labelKVOSet;

#pragma mark - Methods

- (void)changeHeight:(CGFloat)height animated:(BOOL)animated {
    if (self.frame.size.height == height) {
        return;
    }

    if (height > self.frame.size.height) {
        self.animating = YES;

        [self.label sizeToFit];
        height = self.label.frame.size.height;

        self.coverView.frame = CGRectMake(0.0, self.frame.size.height, self.frame.size.width, height - self.frame.size.height);
        self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, height);
        self.label.frame = CGRectMake(0.0, 0.0, self.frame.size.width, self.frame.size.height);

        if (animated) {
            [UIView animateWithDuration:0.5 animations:^(){
                self.coverView.frame = CGRectMake(0.0, self.frame.size.height, self.frame.size.width, 0.0); 
            } completion:^(BOOL completed){
                self.animating = NO;
            }];
        } else {
            self.coverView.frame = CGRectMake(0.0, self.frame.size.height, self.frame.size.width, 0.0); 
            self.animating = NO;
        }
    } else {
        self.animating = YES;

        if (animated) {
            [UIView animateWithDuration:0.5 animations:^(){
                self.coverView.frame = CGRectMake(0.0, height, self.frame.size.width, self.frame.size.height - height);
            } completion:^(BOOL completed){
                self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, height);
                self.coverView.frame = CGRectMake(0.0, height, self.frame.size.width, 0.0);
                self.label.frame = CGRectMake(0.0, 0.0, self.frame.size.width, height);
                self.animating = NO;
            }];
        } else {
            self.frame = CGRectMake(self.frame.origin.x, self.frame.origin.y, self.frame.size.width, height);
            self.coverView.frame = CGRectMake(0.0, height, self.frame.size.width, 0.0);
            self.label.frame = CGRectMake(0.0, 0.0, self.frame.size.width, height);
            self.animating = NO;            
        }
    }
}

- (void)addKVOToLabel {
    if (self.label == nil) {
        return;
    }

    if (!self.labelKVOSet) {
        self.labelKVOSet = YES;
        [self.label addObserver:self forKeyPath:@"text" options:NSKeyValueObservingOptionNew context:nil];
    }
}

- (void)removeKVOFromLabel {
    if (self.label == nil) {
        return;
    }

    if (self.labelKVOSet) {
        self.labelKVOSet = NO;
        [self.label removeObserver:self forKeyPath:@"text"];
    }
}

@end

I had a similar problem trying to make a view grow/shrink. Here is my SO post. Basically I had to animate the frame to grow and then bounds/center to shrink. A bit awkward I know but I got the effect I wanted.

The problem is not in the code you posted. More information is needed to solve this issue. I recommend that you check the following:

  1. That the view object you're resizing is the correct one.
  2. Add a colored background to the view object you're resizing to be able to see how its size changes in real time.

If that doesn't help, please post the complete methods that are executing these animation snippets you posted.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM