20

Apple の AppStore にあるような「続きを読む」機能を実装しています。ただし、複数行を使用していUILabelます。Apple の AppStore を見ると、最後に表示されている行の幅を「詳細」テキストに合わせて縮小し、末尾を切り捨てるにはどうすればよいでしょうか (画像を参照)。

AppStore からの iBooks サンプル画像

4

6 に答える 6

13

少なくとも私が行った限られた量のテストでは、これはうまくいくようです。public メソッドは 2 つあります。複数のラベルがすべて同じ行数である場合は、短い方を使用できます。上部の 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;
}
于 2013-02-27T17:31:02.243 に答える
6

この投稿は 2013 年のものなので、@rdelmar からの非常に優れたソリューションの Swift 実装を提供したいと思いました。

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に再び感謝します。

于 2015-12-05T04:45:42.970 に答える
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 実装を使用することをお勧めします。

于 2013-02-27T14:03:16.583 に答える
3

部分文字列の計算を高速化するために二分探索を使用して、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
}
}
于 2018-06-14T04:29:56.097 に答える
1

ResponsiveLabelは UILabel のサブクラスで、タッチに応答するカスタム トランケーション トークンを追加できます。

于 2015-05-29T11:44:29.503 に答える