我正在实现一个“阅读更多”功能,就像Apple的AppStore中那样。但是,我使用的是多行UILabel
。看看Apple的AppStore,它们如何减少最后一条可见线的宽度以适应“更多”文本并仍然截断尾部(见图)?
答案 0 :(得分:13)
这似乎有效,至少我已经完成了有限的测试。有两种公共方法。如果您有多个标签都具有相同的行数,则可以使用较短的标签 - 只需更改顶部的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;
}
答案 1 :(得分:5)
由于这篇文章是从2013年开始的,我想让我的Swift实现@rdelmar非常好的解决方案。
考虑到我们正在使用UILabel的SubClass:
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。
答案 2 :(得分:4)
有多种方法可以做到这一点,最优雅的是只使用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 :(得分:1)
ResponsiveLabel是UILabel的子类,允许添加响应触摸的自定义截断标记。
答案 4 :(得分:1)
我刚刚在Swift 4中编写了一个UILabel扩展,使用二进制搜索来加速子串计算
它最初是基于@ paul-slm的解决方案,但却有很大分歧
extension UILabel {
func getTruncatingText(originalString: String, newEllipsis: String, maxLines: Int?) -> String {
let maxLines = maxLines ?? self.numberOfLines
guard maxLines > 0 else {
return originalString
}
guard self.numberOfLinesNeeded(forString: originalString) > maxLines else {
return originalString
}
var truncatedString = originalString
var low = originalString.startIndex
var high = originalString.endIndex
// binary search substring
while low != high {
let mid = originalString.index(low, offsetBy: originalString.distance(from: low, to: high)/2)
truncatedString = String(originalString[..<mid])
if self.numberOfLinesNeeded(forString: truncatedString + newEllipsis) <= maxLines {
low = originalString.index(after: mid)
} else {
high = mid
}
}
// substring further to try and truncate at the end of a word
var tempString = truncatedString
var prevLastChar = "a"
for _ in 0..<15 {
if let lastChar = tempString.last {
if (prevLastChar == " " && String(lastChar) != "") || prevLastChar == "." {
truncatedString = tempString
break
}
else {
prevLastChar = String(lastChar)
tempString = String(tempString.dropLast())
}
}
else {
break
}
}
return truncatedString + newEllipsis
}
private func numberOfLinesNeeded(forString string: String) -> Int {
let oneLineHeight = "A".size(withAttributes: [NSAttributedStringKey.font: font]).height
let totalHeight = self.getHeight(forString: string)
let needed = Int(totalHeight / oneLineHeight)
return needed
}
private func getHeight(forString string: String) -> CGFloat {
return string.boundingRect(
with: CGSize(width: self.bounds.size.width, height: CGFloat.greatestFiniteMagnitude),
options: [.usesLineFragmentOrigin, .usesFontLeading],
attributes: [NSAttributedStringKey.font: font],
context: nil).height
}
}
答案 5 :(得分:1)
@ paul-slm的answer,但是我发现这是一个非常耗时的过程,要一个一个地剥离可能长的字符串的最后一个字符,直到标签符合要求为止。行数。取而代之的是,从原始字符串的开头到空白字符串,一次一次复制一个字符,直到达到所需的行数才有意义。您还应该考虑不是一次跨一个字符,而是一次跨多个字符,以便更快地到达“最佳位置”。我将func getTruncatingText() -> String
替换为以下内容:
private func getTruncatingText() -> String? {
guard let originalString = originalString else { return nil }
if numberOfLinesNeeded(originalString) > collapsedNumberOfLines {
var truncatedString = ""
var toyString = originalString
while numberOfLinesNeeded(truncatedString + ellipsis) != (collapsedNumberOfLines + 1) {
let toAdd = toyString.startIndex..<toyString.index(toyString.startIndex, offsetBy: 5)
let toAddString = toyString[toAdd]
toyString.removeSubrange(toAdd)
truncatedString.append(String(toAddString))
}
while numberOfLinesNeeded(truncatedString + ellipsis) > collapsedNumberOfLines {
truncatedString.removeSubrange(truncatedString.index(truncatedString.endIndex, offsetBy: -1)..<truncatedString.endIndex)
}
truncatedString += ellipsis
return truncatedString
} else {
return originalString
}
}