【问题标题】:Decrease the width of the last line in multiline UILabel减少多行 UILabel 中最后一行的宽度
【发布时间】:2013-02-27 13:36:57
【问题描述】:

我正在实现一个“阅读更多”功能,就像 Apple 的 AppStore 中的功能一样。但是,我使用的是多行 UILabel。看看 Apple 的 AppStore,他们如何减小最后一条可见行的宽度以适应“更多”文本并仍然截断尾部(见图)?

【问题讨论】:

  • 我认为您需要使用“UIWebview”并加载您的自定义 html 才能完成此操作
  • 好吧,我真的不想那样做。这样做似乎是一种丑陋的解决方案。我知道我可以调整UILabel 的大小并截断它的尾巴……最坏的情况甚至是UITextView.. 但不是UIWebView
  • 您在 Apple 的 AppStore 中的哪个位置看到了您所描绘的内容?我看到的是以省略号结尾的标签,以及文本下方的“...更多”,可能在不同的标签中。
  • 在我的示例中,它是瑞典的 iBooks AppStore。

标签: ios objective-c cocoa-touch uilabel


【解决方案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;
}

【讨论】:

  • 嗨,rdelmar,为什么需要 kNumberOfLines?这可以从 numberOfLinesNeeded 方法中获得吗?谢谢。
  • @LetBulletFlies,它们是不同的东西。 numberOfLinesNeeded 计算不截断字符串所需的行数。 kNumberOfLines 是您希望在标签中包含的行数,可能是某个固定数字。
  • 由于某种原因,加载类时我得到一个SIGABRT。我认为这是由于我将description 单元格(正式)链接到Storyboard 中的UILabel,现在它是RDLabelUILabel 子类。
  • @PaulPeelen,我不知道为什么这很重要。您可以尝试对项目进行清理,看看是否有帮助。
  • 只是想指出,在更高的 UILabel 级别上执行所有操作比使用 CoreText 路由要慢得多。我发现在做需要大量格式化的工作时,考虑 CoreText 并不是一个坏主意。
【解决方案2】:

由于这篇文章是从 2013 年开始的,所以我想提供我的 Swift 实现,来自 @rdelmar 的非常好的解决方案。

考虑到我们正在使用 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。

【讨论】:

  • 不错!感谢分享。我以后会更喜欢用它,然后也测试一下。
  • 查看我的modification below 以获取更高效的方法来修剪originalString(有助于表格视图中的滚动性能。)
【解决方案3】:

有多种方法可以做到这一点,最优雅的是专门使用 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%,但应该足够接近)。 然后我们创建一个framesetter和frame,并获取frame的所有行,从中我们提取标签的最后一个预期行的范围。

这显然是一种 hack,正如我所说,如果您想完全控制文本的外观,最好使用该标签的纯 CoreText 实现。

【讨论】:

  • 谢谢。似乎有点复杂..但没有其他方法可以尝试...除了这样做。我试试看。
  • 您的代码不起作用。 NSAttributedString 无法识别 addAttribute 方法。得到错误No visible @interface for 'NSAttributedString' declares the selector 'addAttribute:value:range:'
  • 我已经尝试过修改你的代码,但它在你的最后一个 EXC_BAD_ACCESS 上崩溃了,就像 CTLineGetStringRange 一样,我似乎无法解决。如果有人有的话,我想要更多的选择。
  • 抱歉,已修复。同样正如我所说,如果您已经开始这条路线,您可能想一直使用 CoreText。
【解决方案4】:

我刚刚在 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】:

    ResponsiveLabel 是 UILabel 的子类,它允许添加响应触摸的自定义截断标记。

    【讨论】:

      【解决方案6】:

      @paul-slm 的answer 上面是我最终使用的,但是我发现这是一个非常密集的过程,要一个一个地去除可能很长的字符串的最后一个字符,直到标签符合所需的数量线。相反,从原始字符串的开头一次复制一个字符到一个空白字符串,直到满足所需的行数更有意义。您还应该考虑不要一次踩一个字符,而是一次踩多个字符,以便更快地到达“最佳位置”。我将func getTruncatingText() -&gt; 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
          }
      }
      

      【讨论】:

        猜你喜欢
        • 2012-06-26
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-06-11
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多