有个需求需要在 UILabel 的最后一行后面放个按钮,但是需要和 UILabel 内对齐,所以最后一行能显示的文字就需要进行压缩。
场景是文字最多出现三行,如果文字遮挡按钮(不管文字需要几行显示),遮挡的文字及前面两个字符就都进行压缩,变成…
初始思路是直接取最后一行宽度,然后计算多少个文字会挡,然后替换最后的几个文字。。
调研了几个方法,对英文来说有个比较常见的思路是,每次截取一段文本,以空格为分界线,然后不断往里面加词,直到超出一行的宽度,把这一行文本加入到一个数组中,然后在这行文本末尾进行切分。直到切完整段文本。
效率比较低,而且不适合中文。
参考链接:https://stackoverflow.com/questions/10193073/ios-determine-last-line-width-of-uilabel
采用的是这个链接里另一个方法,使用了 NSLayoutManager,计算出整个文本的每行及每个字符的位置,然后通过取最后一个字符的 Index,使用 lineFragmentRectForGlyphAtIndex:effectiveRange:
方法即可取出该字符所在行的 rect,宽度即在其中。
这个方法看起来有完整的 API 支持,决定从这个角度入手。
从另一个回答中看到,提问者有个类似的需求,不过他们是截取第二行的最后几个字符,不需要考虑只有一行的情况,所以可以用 Magic Number 直接指定 UILabel 中某个 point 的值,然后取出这个 point 的字符 Index,进行替换即可。
由于我们不是要取所有文本的最后一行,而是取三行内的最后一行,所以这个 Index 就无法简单的直接取出来了。
解决办法是使用 enumerateLineFragmentsForGlyphRange
来获取三行内最后一行的 rect,然后和需要截取的位置进行比较,如果会发生文字和按钮的重叠,就取重叠点的字符 Index,往前倒推两个Index,然后把剩下的文字替换…就行了。
代码:
- (NSAttributedString *)truncatingMessage:(NSAttributedString *)message
forLabel:(UILabel *)label
truncatingTailWidth:(CGFloat)tailWidth {
CGSize labelSize = CGSizeMake(label.width, INFINITY);
NSLayoutManager *layoutManager = [[NSLayoutManager alloc] init];
NSTextContainer *container = [[NSTextContainer alloc] initWithSize:labelSize];
NSTextStorage *storage = [[NSTextStorage alloc] initWithAttributedString:message];
[layoutManager addTextContainer:container];
[storage addLayoutManager:layoutManager];
container.lineFragmentPadding = 0;
container.lineBreakMode = label.lineBreakMode;
container.maximumNumberOfLines = label.numberOfLines;
NSRange range;
[layoutManager glyphRangeForCharacterRange:NSMakeRange(0, message.length - 1) actualCharacterRange:&range];
__block NSUInteger i = 0;
__block CGRect lastUsedRect;
__block NSRange lastGlyphRange;
[layoutManager enumerateLineFragmentsForGlyphRange:range usingBlock:^(CGRect rect, CGRect usedRect, NSTextContainer * _Nonnull textContainer, NSRange glyphRange, BOOL * _Nonnull stop) {
if (i < container.maximumNumberOfLines) {
lastUsedRect = usedRect;
lastGlyphRange = glyphRange;
i += 1;
} else {
*stop = YES;
}
}];
NSAttributedString *stringToUse = message;
// 最后一行文字宽度会与按钮重叠才需要处理
if (lastUsedRect.size.width > label.width - tailWidth) {
// 算出重叠位置
CGPoint ellipsisPoint = CGPointMake(label.width - tailWidth, lastUsedRect.origin.y + lastUsedRect.size.height / 2);
// 重叠位置的字符 Index
NSUInteger characterIndex = [layoutManager characterIndexForPoint:ellipsisPoint inTextContainer:container fractionOfDistanceBetweenInsertionPoints:nil];
// 如果往后退两个字符就到了上一行,就不进行压缩了, 这里的 location 指的是最后一行第一个字符的 Index
if (characterIndex - 2 > lastGlyphRange.location) {
NSMutableAttributedString *tempString = [[message attributedSubstringFromRange:NSMakeRange(0, characterIndex - 2)] mutableCopy];
NSDictionary *attributes = [message attributesAtIndex:0 effectiveRange:nil];
[tempString appendAttributedString:[[NSAttributedString alloc] initWithString:@"…" attributes:attributes]];
stringToUse = [tempString copy];
}
}
return stringToUse;
}