Уменьшить ширину последней строки в многострочной UILabel

Я реализую функцию "читать больше", как и в Apple AppStore. Тем не менее, я использую многострочный UILabel. Посмотрев на Apple AppStore, как они уменьшают последнюю видимую ширину линии, чтобы соответствовать "более" тексту и все же усекать хвост (см. Изображение)?

iBooks example image from AppStore

Ответы

Ответ 1

Это, похоже, работает, по крайней мере, с ограниченным количеством тестов, которые я сделал. Существует два общедоступных метода. Вы можете использовать более короткий, если у вас есть несколько ярлыков с одинаковым количеством строк - просто измените kNumberOfLines вверху, чтобы соответствовать тому, что вы хотите. Используйте более длинный метод, если вам нужно передать количество строк для разных меток. Обязательно измените класс меток, которые вы делаете в IB, на RDLabel. Используйте эти методы вместо setText:. Эти методы расширяют высоту метки до kNumberOfLines, если это необходимо, и если она все еще усечена, она будет расширять ее, чтобы она соответствовала всей цепочке при касании. В настоящее время вы можете касаться в любом месте этикетки. Это не должно быть слишком сложно изменить, поэтому касается только приближения... Mer приведет к расширению.

#import "RDLabel.h"
#define kNumberOfLines 2
#define ellipsis @"...Mer ▾ "

@implementation RDLabel {
    NSString *string;
}

#pragma Public Methods

- (void)setTruncatingText:(NSString *) txt {
    [self setTruncatingText:txt forNumberOfLines:kNumberOfLines];
}

- (void)setTruncatingText:(NSString *) txt forNumberOfLines:(int) lines{
    string = txt;
    self.numberOfLines = 0;
    NSMutableString *truncatedString = [txt mutableCopy];
    if ([self numberOfLinesNeeded:truncatedString] > lines) {
        [truncatedString appendString:ellipsis];
        NSRange range = NSMakeRange(truncatedString.length - (ellipsis.length + 1), 1);
        while ([self numberOfLinesNeeded:truncatedString] > lines) {
            [truncatedString deleteCharactersInRange:range];
            range.location--;
        }
        [truncatedString deleteCharactersInRange:range];  //need to delete one more to make it fit
        CGRect labelFrame = self.frame;
        labelFrame.size.height = [@"A" sizeWithFont:self.font].height * lines;
        self.frame = labelFrame;
        self.text = truncatedString;
        self.userInteractionEnabled = YES;
        UITapGestureRecognizer *tapper = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(expand:)];
        [self addGestureRecognizer:tapper];
    }else{
        CGRect labelFrame = self.frame;
        labelFrame.size.height = [@"A" sizeWithFont:self.font].height * lines;
        self.frame = labelFrame;
        self.text = txt;
    }
}

#pragma Private Methods

-(int)numberOfLinesNeeded:(NSString *) s {
    float oneLineHeight = [@"A" sizeWithFont:self.font].height;
    float totalHeight = [s sizeWithFont:self.font constrainedToSize:CGSizeMake(self.bounds.size.width, CGFLOAT_MAX) lineBreakMode:NSLineBreakByWordWrapping].height;
    return nearbyint(totalHeight/oneLineHeight);
}

-(void)expand:(UITapGestureRecognizer *) tapper {
    int linesNeeded = [self numberOfLinesNeeded:string];
    CGRect labelFrame = self.frame;
    labelFrame.size.height = [@"A" sizeWithFont:self.font].height * linesNeeded;
    self.frame = labelFrame;
    self.text = string;
}

Ответ 2

Существует несколько способов сделать это, причем самым элегантным является использование CoreText исключительно, поскольку вы получаете полный контроль над тем, как отображать текст.

Вот гибридный вариант, в котором мы используем CoreText, чтобы воссоздать ярлык, определить, где он заканчивается, а затем мы вырезаем текстовую строку метки в нужном месте.

NSMutableAttributedString *atrStr = [[NSAttributedString alloc] initWithString:label.text];
NSNumber *kern = [NSNumber numberWithFloat:0];
NSRange full = NSMakeRange(0, [atrStr string].length);
[atrStr addAttribute:(id)kCTKernAttributeName value:kern range:full];

CTFramesetterRef framesetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)atrStr);  

CGMutablePathRef path = CGPathCreateMutable();
CGPathAddRect(path, NULL, label.frame);
CTFrameRef frame = CTFramesetterCreateFrame(framesetter, CFRangeMake(0, 0), path, NULL);

CFArrayRef lines = CTFrameGetLines(frame);
CTLineRef line = (CTLineRef)CFArrayGetValueAtIndex(lines, label.numberOfLines-1);
CFRange r = CTLineGetStringRange(line);

Это дает вам диапазон последней строки вашего текста ярлыка. Оттуда это тривиально, чтобы разрезать его и поместить эллипсис туда, где вы хотите.

Первая часть создает атрибутную строку со свойствами, необходимыми для репликации поведения UILabel (может быть не 100%, но должно быть достаточно близко). Затем мы создаем фреймсеттер и фрейм и получаем все строки фрейма, из которых извлекаем диапазон последней ожидаемой строки метки.

Это явно какой-то хак, и, как я сказал, если вы хотите полностью контролировать, как выглядит ваш текст, вам будет лучше с чистой реализацией CoreText этой метки.

Ответ 3

Поскольку этот пост с 2013 года, я хотел дать свою быструю реализацию очень приятного решения от @rdelmar.

Учитывая, что мы используем SubClass из UILabel:

private let kNumberOfLines = 2
private let ellipsis = " MORE"

private var originalString: String! // Store the original text in the init

private func getTruncatingText() -> String {
    var truncatedString = originalString.mutableCopy() as! String

    if numberOfLinesNeeded(truncatedString) > kNumberOfLines {
        truncatedString += ellipsis

        var range = Range<String.Index>(
            start: truncatedString.endIndex.advancedBy(-(ellipsis.characters.count + 1)),
            end: truncatedString.endIndex.advancedBy(-ellipsis.characters.count)
        )

        while numberOfLinesNeeded(truncatedString) > kNumberOfLines {
            truncatedString.removeRange(range)

            range.startIndex = range.startIndex.advancedBy(-1)
            range.endIndex = range.endIndex.advancedBy(-1)
        }
    }

    return truncatedString
}

private func getHeightForString(str: String) -> CGFloat {
    return str.boundingRectWithSize(
        CGSizeMake(self.bounds.size.width, CGFloat.max),
        options: [.UsesLineFragmentOrigin, .UsesFontLeading],
        attributes: [NSFontAttributeName: font],
        context: nil).height
}

private func numberOfLinesNeeded(s: String) -> Int {
    let oneLineHeight = "A".sizeWithAttributes([NSFontAttributeName: font]).height
    let totalHeight = getHeightForString(s)
    return Int(totalHeight / oneLineHeight)
}

func expend() {
    var labelFrame = self.frame
    labelFrame.size.height = getHeightForString(originalString)
    self.frame = labelFrame
    self.text = originalString
}

func collapse() {
    let truncatedText = getTruncatingText()
    var labelFrame = self.frame
    labelFrame.size.height = getHeightForString(truncatedText)
    self.frame = labelFrame
    self.text = truncatedText
}

В отличие от старого решения, это будет работать и для любого текстового атрибута (например, NSParagraphStyleAttributeName).

Пожалуйста, не стесняйтесь критиковать и комментировать. Еще раз спасибо @rdelmar.

Ответ 4

ResponsiveLabel является подклассом UILabel, который позволяет добавить пользовательский маркер усечения, который реагирует на касание.