Уменьшить ширину последней строки в многострочной 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, который позволяет добавить пользовательский маркер усечения, который реагирует на касание.